當對象在創建時獲得了一個特定的值,我們說這個對象被初始化。初始化不是賦值,初始化的含義是創建變量賦予其一個初始值,而賦值的含義是把當前值擦除,而以一個新值來替代。對象初始化可以分爲默認初始化、直接初始化、拷貝初始化以及值初始化。
對於C++內置類型的變量來說,當在函數的外部定義的時候,編譯器會給出一個默認的值,而在函數內部的變量必須進行進行初始化在能夠使用。而對以用戶自己定義的類型,可以通過構造函數來完成。構造函數主要是默認構造函數和複製構造函數,下面對類的初始化進行一些探討。
1.類的構造函數
1)默認構造函數,類名();
2) copy 構造函數;
類的初始化方法:
1. 第一種情況: XX a; //直接聲明並定義類;
2. 第一種情況: XX aa = XX(); //直接用類來生命並定義;
3. 第二種情況: XX aa = a; //聲明並用已知的對象初始化;
4. 第二種情況: XX aa(a); //聲明並用已知的對象初始化;
5. 第三種情況: extern fun(XX aa); //fun(a)函數調用
6.第四種情況: XX fun(){...}; //XX a = fun();函數返回值的時候
下面我們用來自進行驗證:
#include <iostream>
#include <string.h>
using namespace std;
class ClassTest{
char s[10];
public:
ClassTest(){
s[0] = '\0';
cout << "default constructor" << endl;
}
ClassTest& operator = (const ClassTest& st){
strcpy(s, st.s);
cout << "this is assignment operator" << endl;
return *this;
}
ClassTest(const char *pc){
strcpy(s, pc);
cout << "this is setting constructor" << endl;
}
ClassTest(const ClassTest ©Class){
strcpy(s, copyClass.s);
cout << "call copy constructor" << endl;
}
};
int main() {
cout << "Initialize Class Analyze:" << endl;
ClassTest a; // 1
ClassTest b = ClassTest(); //2
ClassTest c = b; //3
ClassTest d(b); //4
ClassTest e("aa"); //5
ClassTest f = "a"; //6
return 0;
}
輸出結果:
Initialize Class Analyze:
default constructor //1
default constructor //2
call copy constructor //3
call copy constructor //4
this is setting constructor //5
this is setting constructor //6
可以從結果看出來:儘管2和1比較含有等號,所以有人覺得會調用符號的重載,但是從結果可以看出,1和2一樣都調用了默認的構造函數,5和6也是同樣的情況,調用了非默認的構造函數,而3和4一樣,都調用了複製構造函數。對於調用重載的時候,並不應用在初始化的情況,而是應用在賦值的過程中,比如我們增加 f = a;那麼這時候就會調用符號的重載。
2、初始化列表、構造函數與=賦值之間的區別
這部分轉載自: https://www.cnblogs.com/arxive/p/8418187.html
總所周知,C++對象在創建之時,會由構造函數進行一系列的初始化工作。以沒有繼承關係的單個類來看,除了構造函數本身的產生與指定,還涉及到初始化步驟,以及成員初始化方式等一些細節,本篇筆記主要對這些細節進行介紹,弄清C++對象在初始化過程中一些基本運行規則。
構造函數指定
通常,我們在設計一個類的時候,會爲這個類編寫對應的default constructor、copy constructor、copy assignment operator,還有一個deconstructor。即便我們僅僅編寫一個空類,編譯器在編譯時仍舊會爲其默認聲明一個default constructor、copy constructor、copy assignment operator與deconstructor,如果在代碼裏面存在着它們的使用場景,那麼這個時候編譯器纔會創建它們。
class MyCppClass {}
一旦我們爲一個類編寫了default constructor,那麼編譯器也就不會爲其默認生成default constructor,對於其他幾個函數也一樣。對於編譯器默認生成的constructor來說,它會以一定規則對每一個數據成員進行初始化。考慮到成員初始化的重要性,在編寫自己的constructor時就需要嚴謹認真了,特別是在類的派生與繼承情況下這點顯得尤爲重要。對於copy constructor和assignment operator的運用場景,這裏不得不多說一點,見如下代碼:
#include <iostream> using std::cout; using std::endl; class MyCppClass { public: MyCppClass() { std::cout <<"In Default Constructor!" <<std::endl; } MyCppClass(const MyCppClass& rhs) { std::cout <<"In Copy Constructor!" <<std::endl; } MyCppClass& operator= (const MyCppClass& rhs) { std::cout <<"In Copy Assignment Operator!" <<std::endl; return *this; } }; int main() { MyCppClass testClass1; // default constructor MyCppClass testClass2(testClass1); // copy constructor testClass1 = testClass2; // copy assignment operator MyCppClass testClass3 = testClass1; // copy constructor return 0; }
執行結果:
這裏需要注意的是,一般情況下我們總是以爲在‘=’運算符出現的地方都是調用copy assignment operator,上面這種情況卻是個例外。也就是,當一個新對象被定義的時候,即便這個時候是使用了'='運算符,它真實調用的是初始化函數copy constructor,而不是調用copy assignment operator去進行賦值操作。
Why初始化列表
一個對象在初始化時包括了兩個步驟:
首先,分配內存以保存這個對象;
其次,執行構造函數。
在執行構造函數的時候,如果存在有初始化列表,則先執行初始化列表,之後再執行構造函數的函數體。那麼,爲什麼會引入初始化列表呢?
C++與C相比,在程序組織上由“以函數爲基本組成單位的面向過程”變遷到“基於以類爲中心的面向對象”,與此同時類也作爲一種複合數據類型,而初始化列表無非就是進行一些數據的初始化工作。考慮到這裏,也可以較爲自然的推測初始化列表與類這種數據類型的初始化有着關聯。
在引入初始化列表之後,一個類對應數據成員的初始化就存在有兩種方式。下面是類的數據成員類型分別爲內置類型、自定義類型時的一個對比。
// 數據成員類型爲內置類型 class MyCppClass { public: // 賦值操作進行成員初始化 MyCppClass { counter = 0; } // 初始化列表進行成員初始化 MyCppClass : counter(0) { } private: int counter; }
當類的數據成員類型爲內置類型時,上面兩種初始化方式的效果一樣。當數據成員的類型同樣也爲一個類時,初始化的過程就會有不一樣的地方了,比如:
// 數據成員類型爲自定義類型:一個類 class MyCppClass { public: // 賦值操作進行成員初始化 MyCppClass(string name) { counter = 0; theName = name; } // 初始化列表進行成員初始化 MyCppClass : counter(0), theName(name) { } private: int counter; string theName; }
在構造函數體內的theName = name這條語句,theName先會調用string的default constructor進行初始化,之後再調用copy assignment opertor進行拷貝賦值。而對於初始化列表來說,直接通過copy constructor進行初始化。
明顯起見,可以通過如下的代碼進行測試。
#include <iostream> #include <string> class SubClass { public: SubClass() { std::cout <<" In SubClass Default Constructor!" <<std::endl; } SubClass(const SubClass& rhs) { std::cout <<" In SubClass Copy Constructor!" <<std::endl; } SubClass& operator= (const SubClass& rhs) { std::cout <<" In SubClass Copy Assignment Operator!" <<std::endl; return *this; } }; class BaseClass { public: BaseClass(const SubClass &rhs) { counter = 0; theBrother = rhs; std::cout <<" In BaseClass Default Constructor!" <<std::endl; } BaseClass(const SubClass &rhs, int cnt):theBrother(rhs),counter(cnt) { std::cout <<" In BaseClass Default Constructor!" <<std::endl; } BaseClass(const BaseClass& rhs) { std::cout <<" In BaseClass Copy Constructor!" <<std::endl; } BaseClass& operator= (const BaseClass& rhs) { std::cout <<" In BaseClass Copy Assignment Operator!" <<std::endl; return *this; } private: int counter; SubClass theBrother; }; int main() { SubClass subClass; std::cout <<"\nNo Member Initialization List: " <<std::endl; BaseClass BaseClass1(SubClass); std::cout <<"\nMember Initialization List: " <<std::endl; BaseClass BaseClass2(SubClass, 1); return 0; }
執行結果:
也就是,在涉及到自定義類型初始化的時候,使用初始化列表來完成初始化在效率上會有着更佳的表現。這也是初始化列表的一大閃光點。即便對於內置類型,在一些情況下也是需要使用初始化列表來完成初始化工作的,比如const、references成員變量。這裏有篇筆記,對初始化列表有着非常詳盡的描述。
幾個初始化名詞
在閱讀《Accelerated C++》中文版時,總是碰到“缺省初始化”、“隱式初始化”以及“數值初始化”,最初在理解這幾個名詞的時候幾費周折,總覺得爲什麼一個初始化操作造出瞭如此多的名詞,爲此沒少花時間來弄清楚它們之間的關係。
爲了更好的理解它們,先對C++當中的數據類型進行簡單劃分。在C++裏面,數據類型大致可以分爲兩種:第一種是內置類型,比如float, int, double等;第二種是自定義類型,也就是我們常用的class, struct定義的類。在對這些類型的數據進行初始化時,差別就體現出來了:對於內置類型,在使用之前必須進行顯示的初始化,而對於自定義類型,初始化責任則落在了構造函數身上。
int x = 0; // 顯示初始化x SubClass subClass; // 依賴SubClass的default constructor進行初始化
上面的名詞“缺省初始化”描述的就是當內置類型或者自定義類型的數據沒有進行顯示初始化時的一種初始化狀態。而“隱式初始化”描述的是在該狀態下面進行的具體操作方式,比如對於內置類型來說,缺省初始化狀態下進行的隱式初始化實際上是未定義的,而自定義類型的隱式初始化則依賴於其constructor。
前面提到過C++不保證內置類型的初始化,但是當內置類型在作爲一個類的成員時,在某些特定的條件下該內置類型的成員會被編譯器主動進行初始化,對於這個過程也就是所謂的數值初始化。在《Accelerated C++》當中列出瞭如下的幾種情況:
- 對象被用來初始化一個容器元素
- 爲映射表添加一個新元素,對象是這個添加動作的副作用
- 定義一個特定長度的容器,對象爲容器的元素
測試如下:
#include <iostream> #include <vector> #include <map> #include <string> using std::cout; using std::endl; using std::vector; using std::map; using std::string; class NumbericInitTestClass { public: void PrintCounter() { cout <<"counter = " <<counter <<endl; } private: int counter; }; int main() { NumbericInitTestClass tnc; tnc.PrintCounter(); map<string, int> mapTest; cout <<mapTest["me"] <<endl; vector<NumbericInitTestClass> vecNumbericTestClass(1); vecNumbericTestClass[0].PrintCounter(); return 0; }
對於沒有進行初始化的內置類型,是一個未定義的值2009095316,而對於2, 3種情況來說,均被初始化爲0,對於第1種情況我還沒有想到合適的場景。
回過頭想想,爲了書中的一些相似的名詞,去想辦法把它們湊在一起總是顯得有些牽強附會:)
一些規則
這裏附上幾條有關初始化的基本規則,它們多來源於《Effective C++》:
1. 爲內置型對象進行手工初始化,因爲C++不保證初始化它們。
2. 構造函數最好使用成員初值列(member initialization list),而不要在構造函數體內使用賦值操作。初值列列出的成員變量,其排列次序應該和它們在class中聲明的次序相同。
3. C++不喜歡析構函數吐出異常。
4. 在構造函數與析構函數期間不要調用virtual函數,因爲這類調用從不下降至derived class。
5. copying函數應該確保複製“對象內所有成員變量”及“所有base class成分”。