當對象在創建時獲得了一個特定的值,我們說這個對象被初始化。初始化不是賦值,初始化的含義是創建變量賦予其一個初始值,而賦值的含義是把當前值擦除,而以一個新值來替代。對象初始化可以分為默認初始化、直接初始化、拷貝初始化以及值初始化。
// new edit on 2020.7.23 #pragma once #include <iostream> using namespace std; class ClassTest { public: //定義默認構造函數 ClassTest() { c[0] = '\0'; cout << "1) ClassTest()" << endl; } // 直接初始化 ClassTest(const char* pc) { strcpy_s(c, pc); cout << "2) ClassTest (const char *pc)" << endl; } //復制/拷貝構造函數 ClassTest(const ClassTest& ct) { strcpy_s(c, ct.c); cout << "3) ClassTest(const ClassTest& ct)" << endl; } //重載賦值操作符 ClassTest& operator=(const ClassTest& ct) { strcpy_s(c, ct.c); cout << "4) ClassTest& operator=(const ClassTest &ct)" << endl; return *this; } private: char c[256]; }; ClassTest func(ClassTest temp) { return temp; } int demo_test() { cout << "ct1: "; ClassTest ct1("ab"); // 直接初始化 cout << "ct2: "; ClassTest ct2 = "ab"; // 直接初始化 /* 輸出說明:關於編譯優化: ClassTest ct2 = "ab"; 它本來是要這樣來構造對象的: 首先,調用構造函數ClassTest(const char *pc)函數創建一個臨時對象。 然后,調用復制構造函數,把這個臨時對象作為參數,構造對象ct2。然而,編譯也發現,復制構造函數是 公有的,即你明確地告訴了編譯器,你允許對象之間的復制,而且此時它發現可以通過直接調用重載的 構造函數ClassTest(const char *pc)來直接初始化對象,而達到相同的效果,所以就把這條語句優化為 ClassTest ct2("ab")。 */ cout << "ct3: "; ClassTest ct3 = ct1; // 復制初始化 cout << "ct4: "; ClassTest ct4(ct1); // 復制初始化 cout << "ct5: "; ClassTest ct5 = ClassTest(); // 默認構造函數 cout << "\nct6: "; // 依次調用 1)、2)、4),即默認、直接、重載 ClassTest ct6; ct6 = "caoyan is a good boy!"; cout << "\nct7: "; ClassTest ct7; // 依次調用 1)、3)、3)、4) ct7 = func(ct6); return 0; }
old code:

// (1)默認初始化 int i1;//默認初始化,在函數體之外(初始化為0) int f(void) { int i2;//不被初始化,如果使用此對象則報錯 } string empty;//empty非顯示的初始化為一個空串,調用的是默認構造函數 // (2)拷貝初始化 string str1(10,'9');//直接初始化 string str2(str1);//直接初始化 string str3 = str1;//拷貝初始化 // (3)值初始化 vector<int> v1(10);//10個元素,每個元素的初始化為0 vector<string> v2(10);//10個元素,每個元素都為空 int *pi = new int;//pi指向一個動態分配的,未初始化的無名對象 string *ps = new string;//初始化為空string int *pi = new int;//pi指向一個未初始化的int int *pi = new int(1024);//pi指向的對象的值為1024 string *ps = new string(10,'9');//*ps為"9999999999" string *ps1 = new string;//默認初始化為空string string *ps2 = new string();//值初始化為空string int *pi1 = new int;//默認初始化 int *pi2 = new int();//值初始化為0
1、C++ Copy初始化
在《inside the c++ object model》一書中談到copy constructor的構造操作,有三種情況下,會以一個object的內容作為另一個object的初值:
- 第一種情況: XX aa = a;
- 第二種情況: XX aa(a);
- 第三種情況: extern fun(XX aa); fun(a)函數調用
- 第四種情況: XX fun(){...}; XX a = fun();函數返回值的時候
下面我們就上述的四種情況來一一驗證
#include <iostream> using namespace std; class ClassTest { public: ClassTest() //定義默認構造函數 { c[0] = '\0'; cout << "ClassTest()" << endl; } ClassTest(const char *pc) // 直接初始化 { strcpy_s(c, pc); cout << "ClassTest (const char *pc)" << endl; } ClassTest(const ClassTest &ct) //復制構造函數 { strcpy_s(c, ct.c); cout << "ClassTest(const ClassTest& ct)" << endl; } ClassTest &operator=(const ClassTest &ct) //重載賦值操作符 { strcpy_s(c, ct.c); cout << "ClassTest& operator=(const ClassTest &ct)" << endl; return *this; } private: char c[256]; }; ClassTest func(ClassTest temp) { return temp; } int main() { cout << "ct1: "; ClassTest ct1("ab"); //直接初始化 cout << "ct2: "; ClassTest ct2 = "ab"; //復制初始化 /*輸出說明: ClassTest ct2 = "ab"; 它本來是要這樣來構造對象的: 首先,調用構造函數ClassTest(const char *pc)函數創建一個臨時對象。 然后,調用復制構造函數,把這個臨時對象作為參數,構造對象ct2。然而,編譯也發現,復制構造函數是 公有的,即你明確地告訴了編譯器,你允許對象之間的復制,而且此時它發現可以通過直接調用重載的 構造函數ClassTest(const char *pc)來直接初始化對象,而達到相同的效果,所以就把這條語句優化為 ClassTest ct2("ab")。 */ cout << "ct3: "; ClassTest ct3 = ct1; //復制初始化 cout << "ct4: "; ClassTest ct4(ct1); //直接初始化 cout << "ct5: "; ClassTest ct5 = ClassTest(); //復制初始化 cout << "ct6: "; ClassTest ct6; //復制初始化 ct6 = "caoyan is a good boy!"; cout << "ct7: "; ClassTest ct7; ct7 = func(ct6); return 0; }
測試結果:
我們可以看到,比較復雜的是ct6和ct7,其中ct6還是比較好理解的,ct7這種情況比較難懂,為什么會有兩個拷貝構造函數的調用????
第一次拷貝構造函數的調用:第一次很簡單,是因為函數參數的傳遞,將ct6作為參數傳遞給temp,用ct6的值初始化temp會調用拷貝構造函數;
第二次拷貝構造函數的調用:因為要返回一個ClassTest對象,我們的編譯器怎么做????首先它將temp對象拷貝到func函數的上一級棧幀中,它的上一級棧幀是main函數的棧幀,那么當函數返回時,參數出棧,temp對象的內存空間就會被收回,但是它的值已經被拷貝到main棧幀的一個預留空間中,所以從temp到預留空間的拷貝也是調用拷貝構造函數,最后一步就是給ct7賦值,毫無疑問調用賦值構造函數;對棧幀不同的同學可以看看《程序員的自我修養》一書,里面講得很詳細!
2、初始化列表、構造函數與=賦值之間的區別
總所周知,C++對象在創建之時,會由構造函數進行一系列的初始化工作。以沒有繼承關系的單個類來看,除了構造函數本身的產生與指定,還涉及到初始化步驟,以及成員初始化方式等一些細節,本篇筆記主要對這些細節進行介紹,弄清C++對象在初始化過程中一些基本運行規則。
class MyCppClass {}
#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; }
執行結果:

// 數據成員類型為內置類型
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; }
執行結果:

為了更好的理解它們,先對C++當中的數據類型進行簡單划分。在C++里面,數據類型大致可以分為兩種:第一種是內置類型,比如float, int, double等;第二種是自定義類型,也就是我們常用的class, struct定義的類。在對這些類型的數據進行初始化時,差別就體現出來了:對於內置類型,在使用之前必須進行顯示的初始化,而對於自定義類型,初始化責任則落在了構造函數身上。
int x = 0; // 顯示初始化x SubClass subClass; // 依賴SubClass的default constructor進行初始化
上面的名詞“缺省初始化”描述的就是當內置類型或者自定義類型的數據沒有進行顯示初始化時的一種初始化狀態。而“隱式初始化”描述的是在該狀態下面進行的具體操作方式,比如對於內置類型來說,缺省初始化狀態下進行的隱式初始化實際上是未定義的,而自定義類型的隱式初始化則依賴於其constructor。
- 對象被用來初始化一個容器元素
- 為映射表添加一個新元素,對象是這個添加動作的副作用
- 定義一個特定長度的容器,對象為容器的元素
#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; }

參考文章