Effective C++筆記之一:聲明、定義、初始化與賦值
一.聲明(Declaration)
? ? ? ?聲明的作用是指定變量的類型和名稱,makes a name known to the program。區(qū)分聲明和定義可以讓C++支持分開編譯,比如A.cpp中定義了變量var1,在B.cpp中只需要聲明var1這個變量就可以直接使用。因?yàn)檫@樣的用法,聲明常常見于頭文件中。源文件包含頭文件之后,就可以使用這個變量,即使沒有看到該變量的定義。 聲明的語法如下:
extern?int?i;?//?object?declaration int?numDigits(int?number);?//?function?declaration class?Widget;?//?class?declaration template//?template?declaration class?GraphNode; extern?double?pi?=?3.1416;?//?definition
二.定義(Definition)
? ? ? ?定義是為變量分配存儲空間,并可能進(jìn)行初始化。定義是一種聲明,因?yàn)槎x的同時必然會指定變量的類型和名稱,然而聲明卻不是定義。C++中變量的定義必須有且僅有一次,而變量的聲明可以多次。變量一般不能定義在頭文件中,除了const變量(local to a file)。
? ? ? ?除了變量,類和函數(shù)也有定義的說法,總結(jié)如下:
1.對于類來說,一般定義在頭文件中。因?yàn)榫幾g器需要在每個源文件都看到類的定義才能編譯成功;
2.對于一般函數(shù)來說,函數(shù)聲明在頭文件中,函數(shù)定義則在源文件中;
3.對于inline和constexpr function,編譯器需要在每個源文件都看到定義,因此通常也定義在頭文件中。
int?x;?//?declaration?or?definition
? ? ? ?上面單獨(dú)的一行,是聲明還是定義,判斷的原則是看是否占用內(nèi)存(能否進(jìn)行初始化)。例如:
class?MyClass?//?類定義 { ???int?x;?//?它是聲明,以為C++11之前是不允許在類的定義內(nèi)部直接初始化數(shù)據(jù)成員的? ???float?y?=?10.0f;?//?C++11及以后支持這種寫法,但它仍然是個聲明 ???static?char?c;???//?這也是個聲明,因?yàn)槿绻麑懗蛇@樣?static?char?c?=?'A'; ????????????????????//?編譯器會報(bào)錯,你需要在類外進(jìn)行定義并初始化,因?yàn)轭惱锩娴闹皇锹暶鞫?};
? ? ? ?但是如果int x;出現(xiàn)在函數(shù)定義內(nèi)部,它就是一個定義了。例如:
int?nurnDigits(int?number)?//?function?definition { ????int?x;?//?object?definition,因?yàn)榇藭rx是可以被初始化或賦值的 ????x?=?number/10; ????return?x; } class?Widget?//?class?definition {? public: ????Widget();?//?function?declaration ????~Widget(); private: ????int?x;?//?object?declaration ????int?y; } template//?template?definition class?GraphNode? { public: ????GraphNode(); ????~GraphNode(); ????...... }
? ? ? ?這里有一個令人疑惑的地方,頭文件的的類MyClass既然是定義,按照“定義”的解釋,它應(yīng)該占有內(nèi)存,那為何類中包含的內(nèi)容反而是聲明。
? ? ? ?因?yàn)轭愂菍儆谟脩糇远x的數(shù)據(jù)類型,與內(nèi)置類型,比如說int,在使用上類似。類定義只是定義了一種類型,也即說明了一個類,并沒有實(shí)際定義類的對象,定義的是類,定義類描述的是新的類型,而描述新類型并不會開辟內(nèi)存空間去存儲這樣一種新類的對象。
三.初始化(Initialization)
? ? ? ?初始化是指變量在創(chuàng)建的同時獲得的初始值。雖然C++經(jīng)常用=來初始化一個變量,但是賦值和初始化是兩種不同的操作。賦值是變量定義后的操作,效果是改變變量的值,或者說是用新值來替換舊值;而初始化是在變量創(chuàng)建期獲得一個值。兩者具有本質(zhì)的區(qū)別。下面分別介紹一下C++常見的初始化方式:
default initialization
? ? ? ?當(dāng)我們定義一個變量時,不提供initializer,那么這個變量就是默認(rèn)初始化(default initialized)的。默認(rèn)值由變量的類型和變量的定義位置來決定。
對于built-in type,默認(rèn)值由變量的定義位置決定。在函數(shù)外部定義的全局變量(global variable),函數(shù)內(nèi)部定義的局部靜態(tài)變量(local static object)全部初始化為0。函數(shù)內(nèi)部定義的局部變量是未初始化的;使用未初始化的變量值的行為是未定義的,會帶來巨大的潛在風(fēng)險(xiǎn)。
? ? ? ?對于class type,由類里的默認(rèn)構(gòu)造函數(shù)初始化。如果類定義里沒有默認(rèn)構(gòu)造函數(shù)(顯示或隱示),則編譯出錯。
#includeusing?namespace?std; int?a; int?main() { ???static?int?b; ???int?c; ???cout?<<?a?<<?endl; ???cout?<<?b?<<?endl; ???cout?<<?c<<?endl; ???system("pause"); ???return?0; }
? ? ? ?在VS執(zhí)行這段代碼,輸出變量a的值0,b的值為0,同時VS會報(bào)錯:Run-Time Check Failure #3 — The variable 'c' is being used without being initialized。 變量a和b被默認(rèn)初始化為0,變量c未被初始化。
list initialization
? ? ? ?C++11中提供了一種新的初始化方式,list initialization,以大括號包圍。A tour of c++中寫到The = form is traditional and dates back to C, but if in doubt, use the general {}-list form。注意這種初始化方式要求提供的初始值與要初始化的變量類型嚴(yán)格統(tǒng)一,用法如下,
//?built-in?type?initialization double?d1{2.3};????//ok:?direct-list-initialization? double?d2?=?{2.3};?//ok:?copy-list-initialization //?class?type?initialization complexz2{d1,d2}; complexz3?=?{1,2};??//ok:?the?=?is?optional?with?{...} vectorvec{1,2,3,4,5,6};//ok:?a?vector?of?ints long?double?pi?=?3.1415; int?a{pi},?b?=?{pi};?????????//error:?narrowing?conversion?required int?c(pi),?d?=?pi; ?????//ok:?implict?conversion.
value initialization
? ? ? ?value initialization里,built-in type變量被初始化為0,class type的對象被默認(rèn)構(gòu)造(一定要有)初始化。這種方式通常見于STL里的vector和數(shù)組,且經(jīng)常與list initialization結(jié)合起來使用,為我們初始化全0數(shù)組提供了很大的便利。簡單用法如下:
vectorivec(10); //ten?elements,?each?initialized?to?0 vectorsvec(10); //ten?elmenets,?each?an?empty?string vectorv1?=?{"a",?"an",?"the"};?//list?initialized int?a[10]?=?{}; //ten?elements,?each?initialized?to?0 int?a2[]?=?{1,2,3}; //list?initialized int?a3[5]?=?{1,2,3}; //equivalent?to?a3[]?=?{1,2,3,0,0}
關(guān)于類的初始化比較復(fù)雜,整理幾點(diǎn):
1.編譯器首先編譯類成員的聲明,包括函數(shù)和變量
2.整個類可見后,才編譯函數(shù)體(所以不管定義順序,函數(shù)里可以用類里的任何變量和函數(shù))
3.C++11提供了in-class initializers機(jī)制,C++ Primer里面講如果編譯器支持,推薦使用in-class initializers機(jī)制。注意這種機(jī)制只支持=,{}形式,不支持()。Constructor Initializer List對變量進(jìn)行初始化后,才進(jìn)入構(gòu)造函數(shù)。Constructor Initializer List里忽略的成員變量(為空則相當(dāng)于全部忽略),會由in-class initializers初始化,或者采取default initialization,然后進(jìn)入構(gòu)造函數(shù)體,構(gòu)造函數(shù)體實(shí)際是給成員二次賦值
4.對于class type成員,會調(diào)用其默認(rèn)構(gòu)造函數(shù)進(jìn)行default initialization。
5.對于built-in type成員,要么in-class initialization,要么Constructor initializer list。是否會被default initialization與類定義的位置有關(guān),這點(diǎn)和“default initialization”小節(jié)中說的built-int type類似
6.類的靜態(tài)函數(shù)成員可以在類內(nèi)部或者外部定義,而靜態(tài)數(shù)據(jù)成員(const除外)則只能在外部定義以及初始化
#includeusing?namespace?std; class?testA { public: testA() { cout?<<?"A-x:"?<<?x?<<?endl; cout?<<?"A-y:"?<<?y?<<?endl; } private: int?x; int?y?=?10;?//?in-class?initializer }; class?testB { public: void?printf()?const { cout?<<?"B:"?<<?data?<<?endl; } private: int?data; testA?a; }; testB?b1; int?main() { b1.printf(); testB?b2; b2.printf(); system("pause"); return?0; }
如果是動態(tài)初始化的對象,輸出結(jié)果和上圖一樣,代碼如下:
#includeusing?namespace?std; class?testA { public: testA() { cout?<<?"A-x:"?<<?x?<<?endl; cout?<<?"A-y:"?<<?y?<<?endl; } private: int?x; int?y?=?10;?//?in-class?initializer }; class?testB { public: void?printf()?const { cout?<<?"B:"?<<?data?<<?endl; } private: int?data; testA?a; }; testB?*b1=new?testB(); int?main() { b1->printf(); testB?*b2?=?new?testB; b2->printf(); system("pause"); return?0; }
? ? ? ?但是如果在main函數(shù)中,對b2進(jìn)行value initialization,即將testB *b2 =new testB;改成testB *b2 =new testB();,那么類中的built-in type成員都會被default initialization了。輸出結(jié)果如下所示:
? ? ? ?還需注意的是數(shù)組的初始化。
? ? ? ?定義數(shù)組時,如果沒有顯示提供初始化列表,則數(shù)組元素的默認(rèn)化初始規(guī)則同普通變量一樣:函數(shù)體外定義的內(nèi)置類型數(shù)組,其元素初始為0;函數(shù)體內(nèi)定義的內(nèi)置類型數(shù)組,其元素?zé)o初始化;類類型數(shù)組無論在哪里定義,皆調(diào)用默認(rèn)構(gòu)造函數(shù)進(jìn)行初始化,無默認(rèn)構(gòu)造函數(shù)則必須提供顯示初始化列表。
? ? ? ?如果定義數(shù)組時,僅提供了部分元素的初始列表,其剩下的數(shù)組元素,若是類類型則調(diào)用默認(rèn)構(gòu)造函數(shù)進(jìn)行初始,若是內(nèi)置類型則初始為0(不論數(shù)組定義位置)。
? ? ? ?對于動態(tài)分配的數(shù)組,如果數(shù)組元素是內(nèi)置類型,其元素?zé)o初始化;如果數(shù)組元素是類類型,依然調(diào)用默認(rèn)構(gòu)造函數(shù)進(jìn)行初始化。也可以在使用跟在數(shù)組長度后面的一對空圓括號對數(shù)組元素做值初始化。
例如: int *ptrA = new int[10];
int *ptrB = new int[10] ();
? ? ? ?其中ptrA指向的動態(tài)數(shù)組其元素未初始化,而ptrB指向的動態(tài)數(shù)組元素被初始化為0。
四.賦值(Assignment)?
? ? ? ?賦值的結(jié)果是左邊的操作元,為左值,也就是說,下面的寫法語法正確
int?a?=?0; (a?=?0)?=?1;?//?the?final?value?of?a?is?1
? ? ? ?因?yàn)橘x值操作符的優(yōu)先級很低,該帶括號的時候不能遺漏。
? ? ? ?順便提一下++i和i++的區(qū)別:前者將操作元增加,并且返回改變后的操作元;后者將操作數(shù)增加,返回原先值得拷貝作為結(jié)果。前置自增返回的結(jié)果是左值,后置自增返回的是右值。前置自增操作符做的無用功少,雖然C++編譯器對int和指針類型的后置自增操作符作了優(yōu)化,C++ Primer推薦如無特殊需求,優(yōu)先使用前置自增操作符。
? ? ? ?數(shù)組不支持拷貝初始化或者將一個整體賦值給另一個數(shù)組。
int?a[]?=?{0,1,2}?int?a2[]?=?a;?//?error:?cannot?assign?one?array?to?another