C++中 類的構造函數理解(一)
寫在前面
這段時間完成三個方面的事情:
1、繼續鞏固基礎知識(主要是C++ 方面的知識)
2、嘗試實現一個iOS的app,通過完成app,學習iOS開發中要用到的知識
3、完善實驗室的研究項目,為畢業設計做准備
有了這三個安排之后,就可以把一天的時間大致分為三份了。對於C++ 知識點的學習這部分,主要是看《C++ Primer》以及本科使用的英文教材《C++:How to program》來進行,今天主要探索一下C++ 中類的構造函數。
類簡介
什么是類
類(Class)是C++ 提供的一種抽象數據類型,用戶可以定義自己的“類”來自定義數據類型。類可以說是C++ 中最重要的特征之一。為什么會出現類呢?我的理解是,C++ 中內置的數據類型(int、double、char等)不能滿足用戶處理復雜事務時的需求,因此C++ 中提供了一種抽象機制,允許用戶DIY自己的數據類型,在DIY自己的數據類型也就是類的時候,需要告訴編譯器,你的類名字是什么?它在哪里定義?它有哪些屬性(數據成員)以及它可以有哪些操作(成員函數),把這套規則都定義好之后,用戶就能在程序中操作自己的類以完成某些任務了。
類的數據成員
在上述簡介中,提到了類的數據成員,那么類的數據成員到底是什么呢?“數據成員”這個說法是我學習《C++: How to program》時接觸到的,很多人也將“數據成員”稱為“成員變量”。簡單的說,數據成員就是類的某些“屬性”,比如學生類,對應的屬性可能就有學號、姓名、專業、年級、課程等,這些屬性可以用C++ 內置的數據類型來表示,也可以用一些結構體來表示,甚至也可以用其他類類型來表示。換句話說,類的數據成員是可以被操作的,類中的變量。
類的成員函數
成員函數和普通的函數功能是差不多的,它主要是用來操作數據成員的,或者說用來完成類的某些功能的。
更多類的知識本文就不再擴展了,本文主要探索類的構造函數的一些特性。
構造函數
構造函數簡介
創建完類之后,我們在使用類之前,需要對類進行初始化。定義如何對類進行初始化的成員函數就稱為構造函數。構造函數在創建類類型的對象時被執行。它的工作是保證每個對象的數據成員具有合適的初始值。構造函數的名字必須與類名字相同,它可以接受參數(當然也可以沒有參數),同時也可以定義多個構造函數(也就是說構造函數允許重載)。但有一點要注意的是構造函數不能有返回值,就算是void也不行。另外,由於構造函數設計的初衷是為對象初始化,即初始化對象的數據成員,因此構造函數不能聲明為const,否則程序報錯。另外,一般情況下構造函數都是公有的,因為構造函數聲明為私有的話,其他類將不能生成該類的對象。只有在單例模式下,才將構造函數聲明為私有的,以防止類有多個實例出現。
默認構造函數
參數列表為空的構造函數稱為默認構造函數。(這個定義不全面,下文中有提到,所有參數都指定了默認值的帶參數的構造函數也是默認構造函數)
-
編譯器合成的默認構造函數
我們已經知道構造函數的作用是定義類的初始化,那么如果我們不顯示的定義構造函數,編譯器能為我們創建一個類的實例(對象)嗎?(這里啰嗦一句,類的對象也稱為類的實例,二者是等價的概念,本文兩種說法都有使用)。答案是肯定的,這是因為,當用戶沒有顯示定義類的構造函數時,編譯器會為該類生成一個默認的構造函數,也稱為合成的構造函數。默認構造函數不會初始化對象的數據成員,但如果當前對象的數據成員中包含其他類類型,那么默認構造函數會調用其他類的默認構造函數。其形式如下:
class MyClass { public: MyClass(){} // default constructor } ;
編譯器不合成默認構造函數的情況
(1)一旦用戶定了構造函數之后(只要有一個,無論是公有還是私有,無論哪種形式,哪怕用戶定義的形式與編譯器合成的形式相同),編譯器就不再為用戶合成默認的構造函數。
(2)如果類包含內置或復合類型的成員,則不能依賴編譯器來合成默認構造函數,因為編譯器無法初始化復合類型的成員
-
用戶自定義的默認構造函數
用戶也可以顯示的為自己的類定義默認構造函數,其形式如下:
class MyClass { public: MyClass() { cout<<"Default constructor called."<<endl; m1_int = 7; m2_double = 0.8; } // default constructor private: int m1_int; double m2_double; } ;
定義了上述形式的默認構造函數之后,如果聲明一個MyClass的myclass對象,則默認構造函數就會被調用,myclass的兩個數據成員就能被初始化。
-
類沒有默認構造函數時會出現的情況
假定有一個類NoDefault,它沒有默認構造函數,但有一個接受一個string實參的構造函數,因此編譯器不會為NoDefault類合成默認的構造函數,這意味着:
(1)具有NoDefault成員的每個類的每個構造函數,必須通過傳遞一個初始的string值給NoDefault構造函數來顯示地初始化NoDefault成員。
(2)編譯器不會為具有NoDefault成員的類合成默認構造函數。如果這樣的類希望提供默認構造函數,必須顯示地定義,並且默認構造函數必須顯示地初始化其NoDefault成員。
(3)NoDefault類型不能用作動態分配數組的元素類型。
(4)NoDefault類型的靜態分配數組必須為每個元素提供一個顯示的初始化式。
(5)如果有一個保存NoDefault對象的容器,例如vector,就不能使用接受容器大小而沒有同時提供一個元素初始化式的構造函數。
構造函數初始化列表
構造函數化初始化列表形式如下:
class MyClass { public: //第一種形式 MyClass() : m1_int(7), m2_double(0.8) { cout<<"Constructor 1 is called"<<endl; } //第二種形式 MyClass(int m1, double m2) : m1_int(m1), m2_double(m2) { cout<<"Constructor 2 is called"<<endl; } private: int m1_int; double m2_double; } ;
構造函數初始化列表在構造函數名后添加一個冒號,冒號后是以逗號分隔的數據成員列表,每個數據成員后跟一個放在圓括號中的初始化形式。圓括號中的初始化形式可以是任意復雜的表達式。
這里提一下,類的數據成員被初始化的順序是其定義的次序,而與構造函數初始化列表中的順序無關。
在main函數中生成兩個對象,觀察構造函數被調用情況
int main() { cout<<"Test in main"<<endl; MyClass myclass1;//調用第一種形式的構造函數 MyClass myclass(2,0.3);//調用第二種形式的構造函數 //或者用下面的方式創建對象 MyClass myclass1 = MyClass();//調用第一種形式的構造函數 MyClass myclass2 = MyClass(2,0.3);//調用第二種形式的構造函數 system("pause"); return 0; }
輸出結果如下:
值得一提的是,下面這句話的聲明是正確的,可以通過編譯
MyClass myclass1();
但這個時候我們並不是聲明了一個對象,而是聲明了類的一個函數。
默認實參的構造函數
默認實參的構造函數確保,在調用構造函數的時候,就算沒有提供參數值,構造函數仍然可以正確的初始化類的對象。如果構造函數的所有參數都指定了默認值,這時候的構造函數也屬於默認構造函數。同一個類只能有一個默認構造函數,例如下面的類Time
class Time { public: Time( int = 0, int = 0, int = 0); Time(){} private: int hour; int minute; int second; } ;
在Time中,提供了一個所有參數都指定了默認值的帶默認參數的構造函數,然后又提供了一個默認構造函數,當不創建任何對象的時候,是可以通過編譯的,一旦創建對象,將無法通過編譯,因為編譯器不知道應該使用哪個默認構造函數來初始化對象,因此報錯:
接下來繼續看下默認參數的構造函數的列子
class Time { public: Time( int h = 0, int m = 0, int s = 0) { setTime(h,m,s); } void setTime(int h, int m, int s) { setHour( h ); setMinute( m ); setSecond( s ); } void setHour(int h) { hour = h; } int getHour() { return hour; } ///////////////////// void setMinute( int m) { minute = m; } int getMinute() { return minute; } ///////////////////// void setSecond( int s ) { second = s; } int getSecond() { return second; } private: int hour; int minute; int second; } ;
測試代碼
int main() { cout<<"Test in main"<<endl; Time t1;//所有參數都使用默認值 Time t2( 2 );//時指定,分和秒為默認值 Time t3( 21, 24 );//時和分指定,秒為默認值 Time t4( 12, 25, 34 );//所有參數都指定 cout<<"Time of t1:"<<t1.getHour()<<" : "<<t1.getMinute()<<" : "<<t1.getSecond()<<endl; cout<<"Time of t2:"<<t2.getHour()<<" : "<<t2.getMinute()<<" : "<<t2.getSecond()<<endl; cout<<"Time of t3:"<<t3.getHour()<<" : "<<t3.getMinute()<<" : "<<t3.getSecond()<<endl; cout<<"Time of t4:"<<t4.getHour()<<" : "<<t4.getMinute()<<" : "<<t4.getSecond()<<endl; system("pause"); return 0; }
輸出如下:
可以看到,調用帶默認參數的構造函數時,對象的數據成員的缺省順序與構造函數中的順序相反。
總結
本文對構造函數的探索暫時就到這里了,通過今天的探索,發現構造函數這一個知識點中蘊藏着相當多的內容,本文只是做了一個簡單的概括。關於構造函數,還有很多都沒有探究,比如構造函數之間的調用、拷貝構造函數、有多種類型的數據成員時(如全局動態、全局靜態等)各成員被創建的時機、有類的繼承發生時,構造函數的處理情況等等,這些內容就留待下篇文章去探索了。