title: 入門
date: 2019-03-13 21:10:33
tags:
- Cpp
categories:
- Cpp
toc: true
從 C 到 C++
-
cout
中的 c 指的是 console。 -
endl
=> end line。 -
作用域限定符
::
相當於中文的
,表示作用域或所屬關係。 -
extern "C"
有時候在 C++ 工程中可能需要將某些函數按照 C 的風格來編譯,在代碼前加 extern “C” ,意思是告訴編譯器,將該函數按照C語言規則來編譯。
extern “C” { something(); }
-
typeid(變量名).name
輸出變量的類型。 -
bool
類型,true 爲真,false 爲假。任何基本類型都可以隱士轉換爲 bool 類型,非 0 即真,0 即假。
/* boolalpha -> 相當於一個開關,表示開,打印 true / false noboolalpha -> 表示關,關閉後打印 0 / 1 */ bool a = false; cout << a << " " << boolalpha << a << endl; a = 20; cout << noboolalpha << a << endl; a = *("abc"+3); // 0->a 1->b 2->c 3->\0 cout << boolalpha << a << endl; /* 這三種意思相同 a = *("abc"+3); a = "abc"[3]; a = 3["abc"]; */
命名空間(namespace)
在 C/C++ 中,變量、函數和後面要學到的類都是大量存在的,這些變量、函數和類的名稱將都存在於全局作用域中,可能會導致很多衝突。使用命名空間的目的是對標識符的名稱進行本地化,以避免命名衝突或名字污染,namespace 關鍵字的出現就是針對這種問題的。
-
相同名字的 namespace 作用域相同,同一個工程中允許存在多個相同名稱的命名空間,編譯器最後會合成同一個命名空間中。
-
命名空間的作用:避免名字衝突、劃分邏輯單元,名字空間中的聲明和定義可以分開。
-
名字空間可以嵌套,使用時候得一層一層的扒皮。
-
名字空間也可以賦值取別名,🌰 栗子:
namespace A1 { namespace A11 { namespace A12 { something(); } } } namespace A = A1::A11::A12; // A1::A11::A12::something(); 等價於 A::something();
-
using namespace A1;
就相當於”裸奔“ ,把 A1 中的東西暴露在當前作用域下。using namespace std;
也是一樣,把 cout 暴露在全局下。風險:可能會出現命名衝突,一般還是帶上
::
。
函數
缺省參數
缺省參數是聲明或定義函數時爲函數的參數指定一個默認值。在調用該函數時,如果沒有指定實參則採用該默認值,否則使用指定的實參。
全缺省參數
void TestFunc(int a = 10, int b = 20, int c = 30);
半缺省參數
void TestFunc(int a, int b = 10, int c = 20);
-
缺省參數必須從右開始設置。
/*ERROR*/ void fun(int a = 3,char b,char *c = "ahoj");
-
缺省參數不能在函數聲明和定義中同時出現,建議聲明時指定。如果生命與定義位置同時出現,恰巧兩個位置提供的值不同,那編譯器就無法確定到底該用那個缺省值。
-
缺省值必須是常量或者全局變量。
-
C語言不支持(編譯器不支持)。
啞元
只指定類型而不指定名稱的函數,佔着茅坑不拉屎。
🌰 栗子:
void ya(int,int b)
{
cout << b << endl;
}
int main()
{
ya(10,100);
return 0;
}
- 兼容老版本。
- 支持函數重載。
重載
自然語言中,一個詞可以有多重含義,人們可以通過上下文來判斷該詞真實的含義,即該詞被重載了。
比如:以前有一個笑話,國有兩個體育項目大家根本不用看,也不用擔心。一個是乒乓球,一個是男足。前者是“誰也贏不了”,後者是“誰也贏不了” 。函數重載:是函數的一種特殊情況,C++ 允許在同一作用域中聲明幾個功能類似的同名函數,這些同名函數的形參列表(參數個數 / 類型 / 順序)必須不同,常用來處理實現功能類似數據類型不同的問題。
🌰 栗子:
void foo(){
cout << "void foo();" << endl;
}
void foo(int a){
cout << "void foo(int a);" << endl;
}
void foo(int a,int b){
cout << "void foo(int a,int b);" << endl;
}
void foo(int a,double b){
cout << "void foo(int a,double b);" << endl;
}
void foo(double a,int b){
cout << "void foo(double a,int b);" << endl;
}
int main()
{
foo();
foo(1);
foo(1 , 2);
foo(1 , 3.14);
foo(3.14 , 1);
}
- 同一作用域,函數名相同,參數表不同的函數。
- 參數表不同:
- 參數類型不同
- 參數個數不同
- 參數順序不同
- 重載和形參名無關。
- 重載和返回類型無關。
- 不同作用域同名函數遵循就近原則。
重載的原理
nm a.out
,查看 C++ 編譯器給看書取得名字:
00000001000010a0 T __Z3Maxii
0000000100000de0 T __Z3foodi
0000000100000cf0 T __Z3fooi
0000000100000d90 T __Z3fooid
0000000100000d40 T __Z3fooii
0000000100000b50 T __Z3foov
C++ 函數重載通過編譯器改名實現。
名字修飾(Name Mangling)
在 C/C++ 中,一個程序要運行起來,需要經歷:預處理、編譯、彙編、鏈接。
Name Mangling 是一種在編譯過程中,將函數、變量的名稱重新改編的機制,簡單來說就是編譯器爲了區分各個函數,將函數通過某種算法,重新修飾爲一個全局唯一的名稱。
C語言的名字修飾規則非常簡單,只是在函數名字前面添加了下劃線。
C++ 要支持函數重載、命名空間等,使得其修飾規則比較複雜,不同編譯器在底層的實現方式可能都有差
異。
被重新修飾後的名字中包含了:函數的名字以及參數類型。這就是爲什麼函數重載中幾個同名函數要求其參數
列表不同的原因。只要參數列表不同,編譯器在編譯時通過對函數名字進行重新修飾,將參數類型包含在最終
的名字中,就可保證名字在底層的全局唯一性。
📒 文章:
引用(&)
引用不是新定義一個變量,而是給已存在變量取了一個別名,編譯器不會爲引用變量開闢內存空間,它和它引用的變量共用同一塊內存空間。
李白 <=> 李太白 青蓮居士 詩仙 ……
🌰 栗子:
void foo(int& a)
{
a++;
}
int main()
{
int a = 20;
int& b = a;
int& c = b;
cout << a << b << c << endl;
c = 10;
cout << a << b << c << endl;
cout << &a << " " << &b << " " << &c << " " << endl;
cout << "==========" << endl;
foo(a);
cout << a << endl;
return 0;
}
- 引用必須初始化且不能爲空。
- 引用不能更換目標。
- 一個變量可以有多個引用。
- 引用不佔用額外的內存。
- 引用類型必須和引用實體是同種類型的。
常引用
void TestConstRef()
{
const int a = 10;
// int& ra = a; // 該語句編譯時會出錯,a爲常量 const int& ra = a;
// int& b = 10; // 該語句編譯時會出錯,b爲常量 const int& b = 10;
double d = 12.34;
// int& rd = d; // 該語句編譯時會出錯,類型不同 const int& rd = d;
}
使用場景
-
做參數
void Swap(int& left, int& right) { int temp = left; left = right; right = temp; }
-
做返回值
int& TestRefReturn(int& a) { a += 10; return a; }
⚠️
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :"<< ret <<endl;
return 0;
}
// 這段代碼輸出結果爲一個隨機值
如果函數返回時,離開函數作用域後,其棧上空間已經還給系統,因此不能用棧上的空間作爲引用類型
返回。如果以引用類型返回,返回值的生命週期必須不受函數的限制(即比函數生命週期長)。
傳值和傳引用(作爲參數 / 作爲返回值)在效率上的差距!
引用和指針的區別
-
在語法概念上引用就是一個別名,沒有獨立空間,和其引用實體共用同一塊空間。
-
在底層實現上實際是有空間的,因爲引用是按照指針方式來實現的。
// a.cpp int main() { int a = 10; int& ra = a; ra = 20; int* pa = &a; *pa = 20; return 0; }
查看彙編代碼:
保留編譯過程中生成的臨時文件:
g++ a.cpp -save-temps
其中
a.s
就是彙編文件,在 VS 裏面可以 DEBUG 起來,直接看彙編代碼,比較方便。 -
引用在定義時必須初始化,指針沒有要求。
-
沒有NULL引用,但有NULL指針。
-
在 sizeof 中含義不同:引用結果爲引用類型的大小,但指針始終是地址空間所佔字節個數( 32 位平臺下佔 4
個字節) 。
-
引用自加即引用的實體增加 1 ,指針自加即指針向後偏移一個類型的大小。
-
有多級指針,沒有多級引用。
-
訪問實體方式不同,指針需要顯式解引用,引用編譯器自己處理。
-
引用比指針使用起來相對更安全。
內聯函數(inline)
以 inline 修飾的函數叫做內聯函數,編譯時 C++ 編譯器會在調用內聯函數的地方展開,沒有函數壓棧的開銷,內聯函數提升程序運行的效率。
以下是沒有加 inline 的彙編代碼。
在 Add 前加了 inline 後,在編譯期間編譯器會用函數體替換函數的調用。
-
inline 是一種以空間換時間的做法,省去調用函數額開銷。所以代碼很長或者有循環 / 遞歸的函數不適宜使
用作爲內聯函數。 -
inline 對於編譯器而言只是一個建議,編譯器會自動優化,如果定義爲inline的函數體內有循環/遞歸等等,編譯器優化時會忽略掉內聯。
-
inline 不建議聲明和定義分離,分離會導致鏈接錯誤。因爲 inline 被展開,就沒有函數地址了,鏈接就會找不到。
// F.h #include <iostream> using namespace std; inline void f(int i); // F.cpp #include "F.h" void f(int i) { cout << i << endl; } // main.cpp #include "F.h" int main() { f(10); return 0; } // 鏈接錯誤:main.obj : error LNK2019: 無法解析的外部符號 "void __cdecl f(int)" (?f@@YAXH@Z),該符號在函數 _main 中被引用
auto(C++11)
在早期 C/C++ 中 auto 的含義是:使用 auto 修飾的變量,是具有自動存儲器的局部變量,但遺憾的是一直沒有人去使用它。
C++11 中,標準委員會賦予了 auto 全新的含義即:auto 不再是一個存儲類型指示符,而是作爲一個新的類型指示符來指示編譯器,auto 聲明的變量必須由編譯器在編譯時期推導而得。
🌰 栗子:
int TestAuto()
{
return 20;
}
auto b = 10;
auto c = 'a';
auto d = TestAuto();
//auto e; 無法通過編譯,使用auto定義變量時必須對其進行初始化
⚠️
-
使用 auto 定義變量時必須對其進行初始化,在編譯階段編譯器需要根據初始化表達式來推導 auto 的實際類
型。因此 auto 並非是一種“類型”的聲明,而是一個類型聲明時的“佔位符”,編譯器在編譯期會將 auto 替換爲變量實際的類型。 -
用 auto 聲明指針類型時,用 auto 和 auto* 沒有任何區別,但用 auto 聲明引用類型時則必須加 &。
int main() { int x = 10; auto a = &x; auto* b = &x; auto& c = x; cout << typeid(a).name() << endl; cout << typeid(b).name() << endl; cout << typeid(c).name() << endl; *a = 20; *b = 30; c = 40; return 0; }
-
當在同一行聲明多個變量時,這些變量必須是相同的類型,否則編譯器將會報錯,因爲編譯器實際只對第一個類型進行推導,然後用推導出來的類型定義其他變量。
auto c = 3, d = 4.0; // 該行代碼會編譯失敗,因爲c和d的初始化表達式類型不同
auto 不能推導的場景
-
auto 不能作爲函數的參數。
void fun(auto a) {}
-
auto 不能直接用來聲明數組。
-
爲了避免與 C++98 中的 auto 發生混淆,C++11 只保留了 auto 作爲類型指示符的用法。
-
auto 在實際中最常見的優勢用法就是跟以後會講到的 C++11 提供的新式 for 循環,還有 lambda 表達式等進
行配合使用。
-
auto 不能定義類的非靜態成員變量。
-
實例化模板時不能使用 auto 作爲模板參數。
基於範圍的 for 循環(C++11)
🌰 栗子:
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array)
{
e *= 2;
}
for(auto e : array)
{
cout << e << " ";
}
return 0;
}
⚠️
-
與普通循環類似,可以用 continue 來結束本次循環,也可以用 break 來跳出整個循環。
-
for 循環迭代的範圍必須是確定的,對於數組而言,就是數組中第一個元素和最後一個元素的範圍;對於類而言,應該提供 begin 和 end 的方法,begin 和 end 就是 for 循環迭代的範圍。
-
// 下面這段代碼就有問題 void TestFor(int array[]) { for(auto& e : array) { cout<< e <<endl; } }
nullptr(C++11)
NULL 實際是一個宏,在傳統的 C 頭文件stddef.h
中:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
NULL 可能被定義爲字面常量 0,或者被定義爲無類型指針 (void*) 的常量。不論採取何種定義,在
使用空值的指針時,都不可避免的會遇到一些麻煩,如下:
void f(int)
{
cout<<"f(int)"<<endl;
}
void f(int*)
{
cout<<"f(int*)"<<endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
return 0;
}
程序本意是想通過 f(NULL) 調用指針版本的 f(int*) 函數,但是由於 NULL 被定義成 0,因此與程序的初衷相悖。 在C++98 中,字面常量 0 既可以是一個整形數字,也可以是無類型的指針 (void*) 常量,但是編譯器默認情況下將其看成是一個整形常量,如果要將其按照指針方式來使用,必須對其進行強轉 (void *)0。
爲了避免混淆,C++11 提供了 nullptr ,即:nullptr 代表一個指針空值常量。nullptr 是有類型的,其類型爲nullptr_t ,僅僅可以被隱式轉化爲指針類型,nullptr_t 被定義在頭文件中:
typedef decltype(nullptr) nullptr_t;
⚠️
- 在使用 nullptr 表示指針空值時,不需要包含頭文件,因爲 nullptr 是 C++11 作爲新關鍵字引入的。
- 在 C++11 中,sizeof(nullptr) 與 sizeof((void*)0) 所佔的字節數相同。
- 爲了提高代碼的健壯性,在後續表示指針空值時建議最好使用nullptr。