C++超詳細講解構造函數
類的6個默認成員函數
如果我們寫瞭一個類,這個類我們隻寫瞭成員變量沒有定義成員函數,那麼這個類中就沒有函數瞭嗎?並不是的,在我們定義類時即使我們沒有寫任何成員函數,編譯器會自動生成下面6個默認成員函數。
class S { public: int _a; };
這裡就來詳細介紹一下構造函數。
構造函數
使用C語言,我們用結構體創建一個變量時,變量的內容都是隨機值,要想要能正確的操作變量中存儲的數據,我們還需要調用對應的初始化函數,給成員變量賦一個合適的初值。那麼C++呢,我們仍然使用這個方法來試試。
class Date { public: void SetDate(int year, int month, int day) { _year = year; _month = month; _day = day; } void Display() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1, d2; d1.SetDate(2018, 5, 1);//初始化 d1.Display(); d2.SetDate(2018, 7, 1);//初始化 d2.Display(); return 0; }
完全沒問題的,畢竟C++完全兼容C嘛,不過這樣子做未免有點麻煩,能不能做到我一創建好對象,對象的成員變量就是已經被初始化瞭而不是我們主動去調用呢?C++提供瞭一個特殊的函數:構造函數。
構造函數是一個特殊的成員函數,名字與類名相同,無返回值,每次使用類實例化對象時會自動調用,保證每個數據成員都有一個合適的初值,方便我們的後續使用,構造函數在對象的生命周期內隻會被調用一次。
特性
雖然叫構造函數,但它的作用並不是構造一個對象(申請空間創建對象),而是初始化對象。
特征如下
- 函數名與類名相同;
- 無返回值;
- 對象實例化時編譯器自動調用對應的構造函數;
- 構造函數可以重載;
class Date { public: Date()//無參的構造函數 { } Date(int year, int month, int day)//帶參的構造函數 { _year = year; _month = month; _day = day; } void Display() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1;//調用無參的構造函數 Date d2(2001, 7, 28);//調用帶參的構造函數 d1.Display(); d2.Display(); Date d3();//這種寫法要不得,會被當做函數名為d3,無參、返回類型為Date的函數聲明。 //調用無參的構造函數,一定不要加()否則編譯器無法識別這是一個函數聲明還是調用的無參構造。 return 0; }
如果編譯器沒有顯式的定義構造函數,編譯器會自動生成一個無參的默認構造函數,一旦我們顯式定義,編譯器就不再生成;
class Date { public: void Display() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1;//同樣能創建一個對象 d1.Display(); return 0; }
輸出:
可以看到使用編譯器生成的默認構造函數我們的日期仍然是隨機值。
無參的構造函數和全缺省的構造函數都稱為默認構造函數,並且默認構造函數隻能有一個。註意:無參構造函數、全缺省構造函數、我們沒寫編譯器默認生成的構造函數,都可以認為是默認構造函數。
class Date { public: Date()//無參的默認構造函數 { } Date(int year = 2001, int month = 7, int day = 28)//全缺省的默認構造函數 { _year = year; _month = month; _day = day; } void Display() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1;//這裡無法編譯通過,因為調用不明確。 d1.Display(); return 0; }
一般我們使用全缺省的構造函數,既可以不傳參用缺省值去初始化對象,也可以顯式地去調用並用實參初始化對象。
編譯器生成的默認構造函數
欸好像這貨沒什麼用啊,我剛剛使用這個自動生成的,我的對象的初值還是隨機值啊,看起來就像這構造函數什麼事都沒有做。
其實C++把類型分成內置類型(基本類型)和自定義類型。
內置類型就是語法已經定義好的類型:如int/char…,自定義類型就是我們使用class/struct/union自己定義的類型,看看下面的程序,就會發現編譯器生成默認的構造函數會對自定類型成員_t調用的它的默認成員函數。
不過這涉及到我們還沒學過的知識——初始化列表,後面會講。
class Time { public: Time(int hour = 0, int minute = 0, int second = 0) { cout << "Time(int hour = 0, int minute = 0, int second = 0)" << endl; _hour = hour; _minute = minute; _second = second; } private: int _hour; int _minute; int _second; }; class Date { public: //使用編譯器默認的構造函數 void Display() { cout << _year << "-" << _month << "-" << _day << endl; } private: //內置類型 int _year; int _month; int _day; //自定義類型 Time _t; }; int main() { Date d1; d1.Display(); return 0; }
輸出:
Time(int hour = 0,int minute = 0,int second = 0)
-858993460–858993460–858993460
事實上編譯器生成的默認構造函數並不是什麼都沒有做,而是隻處理瞭成員變量中的自定義類型,而沒有去初始化內置類型,調用自定義類型成員的構造函數就是在初始化列表做的,下面會詳細講。
成員變量的命名風格
看看下面這種成員命名方式有什麼缺陷?
class Time { public: Time(int hour = 0, int minute = 0, int second = 0) { cout << "Time(int hour = 0, int minute = 0, int second = 0)" << endl; hour = hour; minute = minute; second = second; } private: int hour; int minute; int second; };
簡直太難看瞭,hour = hour這是什麼操作???到底哪個hour是成員變量,讓人去猜嗎,代碼實在醜陋,因此我們在對成員變量命名時,為瞭初始化對象不會因為發生命名沖突,又能一眼看出來哪個形參是對應初始化哪個成員變量的,我們通常在對成員變量命名方式統一成成員變量名前加上下劃線_。
class Time { public: Time(int hour = 0, int minute = 0, int second = 0) { cout << "Time(int hour = 0, int minute = 0, int second = 0)" << endl; _hour = hour; _minute = minute; _second = second; } private: int _hour; int _minute; int _second; };
當然不止是這樣,適合自己的才是最好的,不過一般都是采用加一個前綴或者加上一個後綴的方式來命名成員變量。
這裡很容易弄混兩個概念,在此強調一下:
默認構造函數是不需要參數的構造函數,有以下三種:
- 編譯器生成的;
- 顯式定義的無參的構造函數;
- 顯式定義的全缺省的構造函數;
默認成員函數是我們如果不寫,編譯器會自動生成的函數;
構造函數的初始化列表
前面說瞭,在實例化一個類的對象時會自動去調用類的構造函數進行對象的初始化操作,那在C++中一個自定義類型的過程可分為兩個過程:
- 為對象分配內存;
- 對成員變量賦值;
- 函數體內賦值;
那我們想想如果成員變量是具有常屬性的,那麼是不是2過程就無法生效瞭?那麼對於那些具有常性的變量我們以前是怎麼定義的呢?初始化操作可以完成這個問題。
初始化是什麼?變量在定義的同時為它設定一個初值,例如:引用必須初始化
int& a = 10;
,這就是一個典型的初始化操作,而我們要談的初始化列表也是相似的,那麼這種初始化操作有什麼與眾不同的呢?
答案是:對於那些一旦有初值就不能再被賦值的變量,初始化列表的作用就體現出來瞭,例如被const修飾的變量,或者是引用,這些都是具有常屬性的,因此就需要在它們創建的過程中就給它們一個初值,保證其可以被正常初始化。
class S { public: S(int i = 0, int j = 0) { _i = i; _j = j; } private: const int _i;//const修飾i具有常屬性 int _j; }; int main() { S s; return 0; }
報錯:
“S::_i”: 必須初始化常量限定類型的對象
error C2166: 左值指定 const 對象
即在編譯器看來,這個對象包括對象中的成員變量在進入構造函數的函數體後,就都已經初始化完成瞭,因此const修飾的變量i就無法再被賦值瞭。
既然初始化列表是在創建變量階段對變量進行的初始化,因此就可以使用初始化列表處理給那些無法修改的變量。
初始化列表格式如下:
class S { public: S(int i = 0, int j = 0) :_i(i), _j(j) {} private: const int _i; int _j; }; int main() { S s; return 0; }
程序正常運行
同時呢,建議能使用初始化列表就使用,盡量不在構造函數的函數體中為成員變量再賦值,養成好的習慣。
下面分析編譯器生成的默認構造函數到底做瞭什麼事情
class Time { public: Time(int hour = 0, int minute = 0, int second = 0) :_hour(hour), _minute(minute), _second(second) { cout << "Time(int hour = 0, int minute = 0, int second = 0)" << endl; } void DisPlay() { cout << _hour << "-" << _minute << "-" << _second << endl; } private: int _hour; int _minute; int _second; }; class Date { public: void Display() { cout << "Date:"; cout << _year << "-" << _month << "-" << _day << endl; cout << "Time:"; _t.DisPlay(); } private: int _year; int _month; int _day; Time _t; }; int main() { Date d1; d1.Display(); return 0; }
輸出:
可以看到,Date類中的內置類型都未被初始化,而對於自定義類型是去調用瞭其默認構造函數並且初始化成功瞭的。
這裡要記住一定是調用的自定義成員的默認構造函數,因為編譯器生成的Date的默認構造函數調用Time的構造時默認是不傳參的,畢竟它也不知道傳什麼嘛。
如果我們把Time構造函數的缺省值去掉,那麼Time就沒有默認構造函數,那麼創建Date對象時就無法調用Time的默認構造函數,就出錯瞭。如下
class Time { public: Time(int hour, int minute, int second) :_hour(hour), _minute(minute), _second(second) { cout << "Time(int hour = 1, int minute = 1, int second = 1)" << endl; } void DisPlay() { cout << _hour << "-" << _minute << "-" << _second << endl; } private: int _hour; int _minute; int _second; }; class Date { public: void Display() { cout << "Date:"; cout << _year << "-" << _month << "-" << _day << endl; cout << "Time:"; _t.DisPlay(); } private: int _year; int _month; int _day; Time _t; }; int main() { Date d1; d1.Display(); return 0; }
報錯:
message : “Date::Date(void)”: 由於 數據成員“Date::_t”不具備相應的 默認構造函數
那我們現在已經知道瞭編譯器生成的默認構造函數它能做什麼瞭,接下來我們顯式地去定義一個構造函數。
class Time { public: Time(int hour = 0, int minute = 0, int second = 0) :_hour(hour), _minute(minute), _second(second) { cout << "Time(int hour = 0, int minute = 0, int second = 0)" << endl; } void DisPlay() { cout << _hour << "-" << _minute << "-" << _second << endl; } private: int _hour; int _minute; int _second; }; class Date { public: Date() : _year(),//調用瞭int的默認構造函數,並且會給初始化為0 _month(), _day(), _t()//調用瞭Time的默認構造函數,這裡不寫也會自動調用 { cout << "Date()" << endl; } void Display() { cout << "Date:"; cout << _year << "-" << _month << "-" << _day << endl; cout << "Time:"; _t.DisPlay(); } private: int _year; int _month; int _day; Time _t; }; int main() { Date d1; d1.Display(); return 0; }
我們為Date定義一個無參的默認構造函數,在初始化列表我不止處理瞭內置類型還處理瞭自定義類型。
在C++中支持這樣一種定義變量的方法:
int a;//a是隨機值 int b(1); int();//創建匿名變量,調用默認構造
這和自定義類型的定義是一樣的格式,這是為瞭讓內置類型也能按照自定義類型的方式去定義和初始化,看起來是去調用瞭int的構造函數。
這樣,我們在初始化列表中調用瞭初始化瞭內置類型和自定義類型,Date的內置類型也都被初始化為0瞭,這也印證瞭編譯器生成的默認構造函數並沒有去調用int的默認構造,而隻調用瞭自定義的默認構造。
註意:就算我們顯式的定義瞭構造函數,如果在初始化列表中不顯式的調用Time的構造函數,那麼編譯器也會默認去調用它的默認構造(創建自定義類型成員變量時就一定會調用),而我們一旦顯式的去調用瞭,那麼走我們的調用。
C++中有這樣一個特性,編譯器能幫你做的,就算你不做它會自動幫你完成,而你一旦做瞭他就會按照你的方式去完成。
具體什麼意思呢?看代碼:
class Time { public: Time(int hour = 0, int minute = 0, int second = 0) :_hour(hour), _minute(minute), _second(second) { cout << "Time(int hour = 0, int minute = 0, int second = 0)" << endl; } void DisPlay() { cout << _hour << "-" << _minute << "-" << _second << endl; } private: int _hour; int _minute; int _second; }; class Date { public: Date() : _t()//這裡即使不寫,編譯器會自動去調 { cout << "Date()" << endl; } void Display() { cout << "Date:"; cout << _year << "-" << _month << "-" << _day << endl; cout << "Time:"; _t.DisPlay(); } private: int _year; int _month; int _day; Time _t; }; int main() { Date d1; d1.Display(); cout << int() << endl; return 0; }
其實這個Date類的構造函數的初始化過程可以說就是編譯器默認生成的構造相同瞭。
總之就是編譯器生成的默認構造函數會在初始化列表中調用成員中自定義類型的默認構造,而不會處理內置類型,不論是我們顯式定義的還是編譯器生成的,編譯器都會默認去調用自定義類的構造函數,除非我們在初始化列表中顯式的去調用瞭成員的構造函數。
再言簡意駭,就是無論什麼構造函數(自動生成,顯式定義)編譯器都會在初始化列表調用自定義類型成員的構造函數,而如果我們自己顯式調用瞭成員的構造,就執行我們所寫的。
可以不顯式定義構造函數的情況
如果成員變量都是自定義類型並且不需要顯式調用構造函數,那麼編譯器生成的默認構造函數就足以處理這種情況
其他情況都需要我們自己去定義構造函數。
到此這篇關於C++超詳細講解構造函數的文章就介紹到這瞭,更多相關C++構造函數內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!