1. 構造函數基本概念
1)C++中的類可以定義與類名相同的特殊成員函數,這種與類名相同的成員函數叫做構造函數;
2)構造函數在定義時可以有參數;
3)沒有任何返回類型的聲明;
二個特殊的默認構造函數:
1)默認無參構造函數:當類中沒有定義構造函數時,編譯器提供一個默認的無參構造函數,並且其函數體為空。
2)默認拷貝構造函數:當類中沒有定義拷貝構造函數時,編譯器提供一個默認的拷貝構造函數,簡單的進行成員變量的值復制。
構造函數調用規則:
1)當類中沒定義任何構造函數時,C++編譯器會提供默認無參構造函數和默認拷貝構造函數。
2)當類中定義了拷貝構造函數時,C++編譯器不會提供無參數構造函數。
3)當類中定義了任意的非拷貝構造函數(即:當類中提供了有參構造函數或無參構造函數),C++編譯器不會提供默認無參構造函數。
4)默認拷貝構造函數進行的是淺拷貝。
5)當類中定義了拷貝構造函數時,C++編譯器不會提供移動構造函數了。
2. 構造函數的分類及調用
我們來看如下代碼:
class Test { private: int a, b; public: Test() {} // 無參數構造函數 Test(int a, int b) {} // 帶參數的構造函數 Test(const Test &obj) {} // 賦值構造函數 public: void init(int _a, int _b) { a = _a; b = _b; } };
1)無參數構造函數:調用方法如下
Test t1, t2; Test t1 = Test(); // 這樣才是調用默認構造函數,這時必須帶有括號
2)帶參數構造函數
Test t1(20, 10); // 括號法: C++編譯器默認調用有參構造函數 Test t2 = (20, 10); // 等號法: C++編譯器默認調用有參構造函數 Test t3 = Test(20, 10); // 直接調用構造構造函數法: 程序員手工調用構造函數產生了一個對象
3)賦值(拷貝)構造函數:顧名思義,即由其它對象來初始化自己。下面介紹賦值構造函數的三種調用場景(調用時機)。
a. 定義變量時,用對象1初始化對象2
class Test { public: Test() { cout << "我是構造函數,自動被調用了" << endl; } Test(int _a) : a(_a) {} Test(const Test &obj2) { cout << "我也是構造函數,我是通過另外一個對象obj2,來初始化我自己" << endl; } ~Test() { cout<<"我是析構函數,自動被調用了"<<endl; } private: int a; }; int main() { Test a1; Test a2 = a1; // 用 a1 初始化 a2 Test a3(a1); // 這樣寫也是用 a1 初始化 a3 return 0; }
b. 實參變量初始化形參變量
class Location { public: Location(int x = 0, int y = 0) : X(x), Y(y) { cout << "Constructor Object.\n"; } Location(const Location &p) : X(p.X), Y(p.Y) { cout << "Copy_constructor called." << endl; } ~Location() { cout << X << "," << Y << " Object destroyed." << endl; } int GetX() { return X; } int GetY() { return Y; } private: int X, Y; }; void f(Location p) { cout << "Funtion:" << p.GetX() << "," << p.GetY() << endl; } int main() { Location A(1, 2); f(A); // 調用f會構造一個臨時對象p,此時會調用拷貝構造函數 return 0; }
c. 函數返回匿名對象,會在棧上面通過拷貝構造函數產生一個臨時對象(一般會被編譯器優化),然后原來的棧變量被析構。
之后就取決於程序員怎么來接收這個匿名對象,不同的接法差別在於會不會多一次賦值運算符的調用。
注:可以在編譯時設置編譯選項-fno-elide-constructors用來關閉返回值優化效果。
class Location { public: Location(int x = 0, int y = 0) : X(x), Y(y) { cout << "Constructor Object.\n"; } Location(const Location &p) : X(p.X), Y(p.Y) { cout << "Copy_constructor called." << endl; } ~Location() { cout << X << "," << Y << " Object destroyed." << endl; } int GetX() { return X; } int GetY() { return Y; } private: int X, Y; }; void f(Location p) { cout << "Funtion:" << p.GetX() << "," << p.GetY() << endl; } /* * 當函數需要返回一個對象,他會在棧中創建一個臨時對象,存儲函數的返回值。 * 這個臨時對象也是匿名對象,構造它時會調用拷貝構造函數,用A來初始化這個匿名對象。 * 然后函數調用結束,A被銷毀. * 但是這個臨時對象的構造一般會被編譯器優化掉,所以自己測試的時候一般不會調用拷貝構造函數了。 */ Location g() { Location A(1, 2); return A; } int main() { Location B; B = g(); // 若返回的匿名對象,賦值給另外一個同類型的對象,那么匿名對象會被析構。(會調用賦值運算符) Location C = g(); // 若返回的匿名對象,來初始化另外一個同類型的對象,那么匿名對象會直接轉成新的對象。(啥也不調用) return 0; }
4)移動構造函數:C++11引入移動語義----臨時對象資源的控制權(堆內存)全部交給目標對象。注意一下,臨時對象和目標對象是兩個獨立的不同對象,
移動構造函數也不是說將臨時對象直接變成目標對象,只是將臨時對象所控制的資源進行淺拷貝(拷貝指針),而沒有了深拷貝,然后臨時對象就無法
訪問這個資源了,但臨時對象本身還是要被析構的。因為淺拷貝是難以避免的,所以類如果沒有堆上的資源,也就沒必要實現移動構造函數。
下面舉個例子:
static unsigned int cCount; //統計拷貝構造函數調用次數 static unsigned int mCount; //統計移動構造函數調用次數 class MyString { public: // 構造函數 MyString(const char* cstr = 0) { if (cstr) { m_data = new char[strlen(cstr) + 1]; strcpy(m_data, cstr); } else { m_data = new char[1]; *m_data = '\0'; } } // 拷貝構造函數 MyString(const MyString& str) { cCount++; m_data = new char[strlen(str.m_data) + 1]; strcpy(m_data, str.m_data); } // 移動構造函數 MyString(MyString&& str) { mCount++; m_data = str.m_data; // 目標對象接管堆上資源 str.m_data = nullptr; // 臨時對象不再指向那個資源了 } ~MyString() { delete[] m_data; } private: char* m_data; }; int main() { vector<MyString> vecStr; vecStr.reserve(1000); // 先分配好1000個空間 for(int i = 0; i < 1000; i++) { vecStr.push_back(MyString("hello")); } cout << "cCount: " << cCount << endl; cout << "mCount: " << mCount << endl; return 0; }
運行可知道程序調用了1000次的移動構造函數,這樣就不會去重新分配一塊新的空間,將要拷貝的對象復制過來,而是"偷"了過來,將自己的指針
指向別人的資源,然后將別人的指針修改為nullptr
,這一步很重要,如果不將別人的指針修改為空,那么臨時對象析構的時候就會釋放掉這個資源,"偷"也白偷了。
拋出一個問題:我們知道const引用也是能夠被右值初始化的,那編譯器怎么知道調用哪個構造函數呢?是拷貝還是移動?
編譯器判斷傳入的參數是一個右值,會認為移動構造函數是一個更好的匹配。
對於一個左值,肯定是調用拷貝構造函數了,但是有些左值是局部變量,生命周期也很短,能不能也移動而不是拷貝呢?C++11
為了解決這個問題,提供
了std::move()
方法來將左值轉換為右值,從而方便應用移動語義。我覺得它其實就是告訴編譯器,雖然我是一個左值,但是不要對我用拷貝構造函數,而是
用移動構造函數吧。。。
注意一下:將一個臨時對象賦值給 T &&x 是延長臨時對象的生命周期的做法(不會移動或者拷貝),是右值引用。若賦值給 T x 則會觸發移動或者賦值構造函數。
還是上面的類,現在改寫一下main函數。
int main() { vector<MyString> vecStr; vecStr.reserve(1000); //先分配好1000個空間 for(int i = 0; i < 1000; i++) { MyString tmp("hello"); vecStr.push_back(tmp); //調用的是拷貝構造函數 } cout << "cCount: " << cCount << endl; cout << "mCount: " << mCount << endl; cCount = 0; mCount = 0; vector<MyString> vecStr2; vecStr2.reserve(1000); //先分配好1000個空間 for(int i = 0; i < 1000; i++) { MyString tmp("hello"); /* * 調用的是移動構造函數 * 此時tmp指向的資源已經為null了,但對象在表達式結束時尚未析構,作用域結束后才析構 */ vecStr2.push_back(std::move(tmp)); //調用的是移動構造函數 } cout << "cCount: " << cCount << endl; cout << "mCount: " << mCount << endl; return 0; } // 輸出如下 cCount:1000 mCount:0 cCount:0 mCount:1000
需要注意一下:如果我們沒有提供移動構造函數,只提供了拷貝構造函數,std::move()
會失效但是不會發生錯誤,因為編譯器找不到移動構造函數就
去尋找拷貝構造函數,也這是拷貝構造函數的參數是const T&
常量左值引用的原因!
3. 構造函數隱式轉換
用單個實參(也可以有多個實參,但是除了第一個參數,其它參數必須有默認值)來調用的構造函數定義了從形參類型到類類型的一個隱式轉換。
隱式轉換沒有特別的語法,只要類型滿足構造函數的參數即可以觸發。簡單舉個例子
class Test { public: bool same(const Test &rbs) const { return isbn == rbs.isbn; } Test(const std::string &book = "7115145547") : isbn(book) {} private: std::string isbn; }; int main() { Test trans; string null_book = "9-999-99999-9"; trans.same(null_book); // 這里會發生隱式類型轉換,從string轉換為test(因為有構造函數可以用一個string做參數),建立一個臨時的類的對象 return 0; }
為了避免這個情況的發生,可以將類的構造函數聲明為explicit,然后顯示調用:
explicit Test(const std::string &book = "7115145547") : isbn(book) {} trans.same(Test(null_book));