目錄
1..類的6個默認成員函式
2.建構式
3.解構式
4.復制建構式
4.深淺拷貝
6..賦值運算子多載
1..類的6個默認成員函式
如果一個類中什么成員都沒有,簡稱為空類,空類中什么都沒有嗎?并不是的,任何一個類在我們不寫的情況下,都會自動生成下面6個默認成員函式,
class Date {};
上面這個就是一個空類
2.建構式
1.對于以下的日期類:
#include<iostream> using namespace std; 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; }
對于Date類,可以通過SetDate公有的方法給物件設定內容,但是如果每次創建物件都呼叫該方法設定資訊,未免有點麻煩,那能否在物件創建時,就將資訊設定進去呢?建構式是一個特殊的成員函式,名字與類名相同,創建型別別物件時由編譯器自動呼叫,保證每個資料成員都有 一個合適的初始值,并且在物件的生命周期內只呼叫一次,
2.建構式的特性:
建構式是特殊的成員函式,需要注意的是,建構式的雖然名稱叫構造,但是需要注意的是建構式的主要任務并不是開空間創建物件,而是初始化物件,
#include<iostream> using namespace std; class Date{ public: // 1.無參建構式 Date () {} // 2.帶參建構式 Date (int year, int month , int day ) { _year = year ; _month = month ; _day = day ; } private : int _year=0 ; int _month=0 ; int _day=0; }; void TestDate() { Date d1; // 呼叫無參建構式 Date d2 (2015, 1, 1); // 呼叫帶參的建構式 // 注意:如果通過無參建構式創建物件時,物件后面不用跟括號,否則就成了函式宣告 // 以下代碼的函式:宣告了d3函式,該函式無參,回傳一個日期型別的物件 Date d3(); } int main() { TestDate(); return 0; }
5. 如果類中沒有顯式定義建構式,則C++編譯器會自動生成一個無參的默認建構式,一旦用戶顯式定義編譯器將不再生成,
#include<iostream> using namespace std; class Date{ public: // 如果用戶顯式定義了建構式,編譯器將不再生成 /*Date(int year, int month, int day) { _year = year; _month = month; _day = day; } */ private: int _year; int _month; int _day; }; void Test() { Date d;// 沒有定義建構式,物件也可以創建成功,因此此處呼叫的是編譯器生成的默認建構式 } int main() { Test(); return 0; }
6. 無參的建構式和全預設的建構式都稱為默認建構式,并且默認建構式只能有一個,注意:無參建構式、全預設建構式、我們沒寫編譯器默認生成的建構式,都可以認為是默認成員函式,
#include<iostream> using namespace std; // 默認建構式 class Date{ public: Date() { _year = 1900 ; _month = 1 ; _day = 1; } Date (int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private : int _year ; int _month ; int _day ; }; // 以下測驗函式能通過編譯嗎? void Test(){ Date d1; } int main() { Test(); }顯然是不能夠通過編譯的
7.默認建構式的呼叫時機:
當不使用任何初始值定義一個類的非靜態變數時,會呼叫該類的默認建構式
A a;
此時,會呼叫類A的默認建構式如果類中沒有顯式地定義默認建構式,則C++編譯器會為其創造一個合成的默認建構式,如果類中已經定義了其他格式的建構式,此時C++編譯器不會再為其合成默認建構式,所以,如果此時類A的定義為
class A { public: A(int i){} };
此時,程式會報錯,報錯資訊為“error C2512: “A”: 沒有合適的默認建構式可用”這與上面那個例子是相同的
當類B含有類A的物件,并且使用類B的默認建構式時,會呼叫類A的默認建構式
class A { public: }; class B { A m_a; };
類本身含有類的物件且沒有在建構式中顯式初始化該物件時
class A { public: }; class B { public: B(int i){ } A m_a; };
8.有參建構式的三種呼叫方法
#include<iostream> using namespace std; class Test { public: //帶引數的建構式 Test(int a) { cout << "Test(a)" << endl; } Test(int a, int b) { cout << "Test(a,b)" << endl; } private: int a; int b; }; int main() { //1. 括號法:C++編譯器呼叫有參建構式 Test t1(10); //2. 等號法:C++編譯器呼叫有參建構式 Test t2 = (20, 10); //3. 建構式法:手動直接呼叫建構式//Test(30)這是一個匿名物件 Test t3 = Test(30); return 0; }
9.成員變數的命名風格
一般我們會在成員變數的前面加一個_ 我們來看一下下面這個代碼:
// 我們看看這個函式,是不是很僵硬? class Date{public: Date(int year) { // 這里的year到底是成員變數,還是函式形參? year = year; //雖然此處我們可以用this指標來區別但還是建議使用下面這種方式 } private: int year; }; class Date { public: Date(int year) { _year = year; } private: int _year; }; // 或者這樣, class Date { public: Date(int year) { m_year = year; } private: int m_year; };
3.解構式
1.前面通過建構式的學習,我們知道一個物件時怎么來的,那一個物件又是怎么沒呢的?解構式:與建構式功能相反,解構式不是完成物件的銷毀,區域物件銷毀作業是由編譯器完成的,而物件在銷毀時會自動呼叫解構式,完成類的一些資源清理作業
2.解構式物件消亡時即自動被呼叫,可以定義解構式來在物件消亡前做善后作業,比如釋放分配的空間等,
如果定義類時沒寫解構式,則編譯器生成預設解構式,預設解構式什么也不做,如果定義了解構式,則編譯器不生成預設解構式,
2.解構式的特性:
解構式是特殊的成員函式
特性如下:
1. 解構式名是在類名前加上字符 ~,
2. 無引數無回傳值,
3. 一個類有且只有一個解構式,若未顯式定義,系統會自動生成默認的解構式,
4. 物件生命周期結束時,C++編譯系統系統自動呼叫解構式
5.如果顯示定義了解構式那么編譯器將不在生成
如果我們需要在建構式里面干一些其他的事情則我們需要手動的寫解構式
#include<stdlib.h> #include<assert.h> #include<iostream> typedef int DataType; class SeqList { public: SeqList(int capacity = 10) { _pData = (DataType*)malloc(capacity * sizeof(DataType)); assert(_pData); _size = 0; _capacity = capacity; } ~SeqList() { if (_pData) { free(_pData); }// 釋放堆上的空間 _pData = NULL; // 將指標置為空 _capacity = 0; _size = 0; } private: int* _pData; size_t _size; size_t _capacity; };
3.在創建一類物件陣列時,對于每一個陣列中的元素都會執行預設的建構式,同樣物件聲生命周期結束時陣列中的每個元素的解構式都會被呼叫
#include<iostream> using namespace std; unsigned int count1 = 0; class A { public: A ( ) { i = ++count1; cout << "Creating A " << i <<endl; } ~A ( ) { cout << "A Destructor called " << i <<endl; } private : int i; }; int main( ) { A arr[3]; // 物件陣列 return 0; }
運行結果:
為什么解構式的呼叫和建構式相反了這是因為堆疊的性質:先進后出
4.解構式的呼叫時機:
如果出現以下幾種情況,程式就會執行解構式:
(1)如果在一個函式中定義了一個物件(它是自動區域物件),當這個函式被呼叫結束時,物件應該釋放,在物件釋放前自動執行解構式,
(2)static區域物件在函式呼叫結束時物件并不釋放,因此也不呼叫解構式,只在main函式結束或呼叫exit函式結束程式時,才呼叫static區域物件的解構式,
(3)如果定義了一個全域物件,則在程式的流程離開其作用域時(如main函式結束或呼叫exit函式) 時,呼叫該全域物件的解構式,
(4)如果用new運算子動態地建立了一個物件,當用delete運算子釋放該物件時,先呼叫該物件的解構式,
(5)呼叫復制建構式后,
#include <iostream> using namespace std; class CMyclass { public: ~CMyclass() { cout << "destructor" << endl; } }; CMyclass obj; CMyclass fun(CMyclass sobj) { return sobj; //函式呼叫回傳時生成臨時物件回傳 } int main() { obj = fun(obj); //函式呼叫的回傳值(臨時物件)被用過后,該臨時物件解構式被呼叫 }
運行結果:
destructor // 形參和實參結合,會呼叫復制建構式,臨時物件析構
destructor // return sobj函式呼叫回傳,會呼叫復制建構式,臨時物件析構
destructor // obj物件析構
建構式和解構式總結:
建構式的呼叫:
(1)全域變數:程式運行前;
(2)函式中靜態變數:函式開始前;
(3)函式引數:函式開始前;
(4)函式回傳值:函式回傳前;
解構式的呼叫:
(1)全域變數:程式結束前;
(2)main中變數:main結束前;
(3)函式中靜態變數:程式結束前;
(4)函式引數:函式結束前;
(5)函式中變數:函式結束前;
(6)函式回傳值:函式回傳值被使用后;注意:對于相同作用域和存盤類別的物件呼叫解構式和建構式的順序的呼叫次序剛好相反
3.復制建構式
1.復制建構式是一種特殊的建構式,具有一般建構式的所有特性,復制建構式創建一個新的物件,作為另一個物件的拷貝,復制建構式只含有一個形參,而且其形參是對應物件的參考,復制建構式形如 X::X( X& ), 只有一個引數即對同類物件的參考,如果沒有定義,那么編譯器生成預設復制建構式,
復制建構式的兩種原型(prototypes),以類Date為例,Date的復制建構式可以定義為如下形式:Date(Date & );或者:
Date( const Date & );
特別要注意的是:復制建構式的引數只有一個且必須使用參考傳參,使用傳值方式會引發無窮遞回呼叫 ,這是因為函式傳引數時實參是形參的一份拷貝下面我們來看這個例子:
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; }private: int _year; int _month; int _day; }; int main() { Date d1; Date d2(d1); return 0; }
復制建構式的特性:
1. 拷貝建構式是建構式的一個多載形式,
2. 拷貝建構式的引數只有一個且必須使用參考傳參,使用傳值方式會引發無窮遞回呼叫,
3.若未顯示定義,系統生成默認的拷貝建構式, 默認的拷貝建構式物件按記憶體存盤按位元組序完成拷貝,這種拷貝我們叫做淺拷貝,或者值拷貝,
3.復制建構式的呼叫時機:、
1.一個物件需要通過另外一個物件初始化
2.一個物件以值傳遞的方式傳入函式體
3.一個物件以傳值的方式從函式回傳
下面我們來看一下例子:
1.一個物件需要通過另外一個物件初始化
#include <iostream> using namespace std; class Complex { public: Complex(double r, double i) { real = r; imag = i; } Complex(Complex& c) { real = c.real; imag = c.imag; cout << "copy constructor!" << endl; } private: double real; double imag; }; int main() { Complex c1(1, 2); //呼叫建構式Complex(double r, double i) Complex c2(c1); // 呼叫復制建構式Complex( Complex & c) Complex c3 = c1; // 呼叫復制建構式Complex( Complex & c) return 0; }
2.一個物件以值傳遞的方式傳入函式體
函式的形參是類的物件,呼叫函式時,進行形參和實參的結合,
如果某函式有一個引數是類Complex的物件,那么該函式被呼叫時,類Complex的復制建構式將被呼叫,
復制代碼 void func(Complex c) { }; int main( ) { Complex c1(1,2); func(c1); // Complex的復制建構式被呼叫,生成形參傳入函式 return 0; }
3.一個物件以傳值的方式從函式回傳
當物件傳入函式的時候被隱式呼叫以外,復制建構式在物件被函式回傳的時候也同樣的被呼叫,換句話說,你從函式回傳得到的只是物件的一份拷貝
Complex func() { Complex c1(1,2); return c1; // Complex的復制建構式被呼叫,函式回傳時生成臨時物件 }; int main( ) { func(); return 0; }
注意:
物件之接用等號賦值并不會呼叫復制建構式,C++中,當一個新物件創建時,會有初始化的操作,而賦值是用來修改一個已經存在的物件的值,此時沒有任何新物件被創建,初始化出現在建構式中,而賦值出現在operator=運算子函式中,編譯器會區別這兩種情況,賦值的時候呼叫多載的賦值運算子,初始化的時候呼叫復制建構式,
#include <iostream> using namespace std; class Complex { public: Complex(double r, double i) { real = r; imag = i; } Complex(Complex& c) { real = c.real; imag = c.imag; cout << "copy constructor!" << endl; } private: double real; double imag; }; int main() { Complex c1(1, 2); Complex c2 = c1;//呼叫復制建構式 Complex c3(3, 4); c3 = c1;//呼叫賦值建構式 return 0; }
4.匿名物件的去和留:
我們知道在C++的創建物件是一個費時,費空間的一個操作,有些固然是必不可少,但還有一些物件卻在我們不知道的情況下被創建了,通常以下三種情況會產生臨時物件:
1,以值的方式給函式傳參;
2,型別轉換;
3,函式需要回傳一個物件時;
我們先來說結論:
結論1:函式的回傳值是一個元素(復雜型別),回傳的是一個新的匿名物件(所以會呼叫匿名物件類的copy建構式)
結論2:匿名物件的去和留 .如果用匿名物件初始化 另外一個同型別的物件,匿名物件轉成有名物件 //如果用匿名物件賦值給 另外一個同型別的物件,匿名物件被析構
3.如果直接定義一個匿名物件則定義完之后它就會被析構
下面我們來看代碼:
#include <iostream> using namespace std; class Complex { public: Complex(double r, double i) { real = r; imag = i; } Complex(Complex& c) { real = c.real; imag = c.imag; cout << "copy constructor!" << endl; } ~Complex() { cout << "解構式呼叫" << endl; } private: double real; double imag; }; int main() { Complex(1, 2); return 0; }
我們通過除錯可以發現當他定義完之后執行下一句陳述句的時候就呼叫了解構式
下面我們再來看:
#include <iostream> using namespace std; class Complex { public: Complex(double r, double i) { cout << "建構式呼叫" << endl; real = r; imag = i; } Complex(const Complex& c) { real = c.real; imag = c.imag; cout << "復制建構式呼叫!" << endl; } ~Complex() { cout << "解構式呼叫" << endl; } private: double real; double imag; }; Complex test() { Complex a(1, 2); return a; } int main() { Complex c1 = test(); return 0; }
此時在test里面定義了一個物件那么就會呼叫他的建構式初始化函式回傳時會生成一個匿名物件呼叫匿名物件的復制建構式,在析構a物件,而我們又恰好用一個未初始化的物件來接這個函式的回傳值那么他就會轉成有名物件也就是c1,進而在析構,
運行結果:
但如果我們用已經初始化過的物件來接那么他就會直接析構:
#include <iostream> using namespace std; class Complex { public: Complex(double r, double i) { cout << "建構式呼叫" << endl; real = r; imag = i; } Complex(const Complex& c) { real = c.real; imag = c.imag; cout << "復制建構式呼叫!" << endl; } ~Complex() { cout << "解構式呼叫" << endl; } private: double real; double imag; }; Complex test() { Complex a(1, 2); return a; } int main() { Complex c1(2, 1); c1 = test();//匿名物件會直接析構掉 return 0; }
運行結果:
4.深淺拷貝
什么是深拷貝和淺拷貝了?
淺拷貝就是通過賦值的方式進行拷貝,那為什么說這是淺拷貝呢?就是因為賦值的方式只會把物件的表層賦值給一個新的物件,如果里面有屬性值為陣列或者物件的屬性,那么就只會拷貝到該屬性在堆疊空間的指標地址,新物件的這些屬性資料就會跟舊物件公用一份,也就是說兩個地址指向同一份資料,一個改變就會都改變,析構時也會析構兩遍從而會讓程式崩潰
深拷貝則不會出現上述問題,重新開一塊空間,拷貝資料
下面我們來看一下例子:
#include <iostream> using namespace std; class Student { private: int num; char* name; public: Student(); ~Student(); }; Student::Student() { name = new char[2]{'2','0'}; cout << "Student" << endl; } Student::~Student() { cout << "~Student " << (int)name << endl; delete name; name = NULL; } int main() { {// 花括號讓s1和s2變成區域物件,方便測驗 Student s1; Student s2(s1);// 復制物件 } system("pause"); return 0; }
我們會發現此時程式崩潰了
由于s1和s2中的name同時指向了同一塊空間當我們析構的時候同一塊空間析構兩次此時程式會崩潰
此時我們就需要自己寫復制建構式重新開辟一塊新的空間:
#include <iostream> using namespace std; class Student { public: Student(const char*str); ~Student(); Student(const Student& s){ char* tmp = new char[strlen(s.name) + 1]; strcpy(tmp, s.name); name = tmp; } private: int num; char* name; }; Student::Student(const char*str) { num = 0; char*tmp = new char[strlen(str) + 1]; strcpy(tmp, str); name = tmp; cout << "Student" << endl; } Student::~Student() { //cout << "~Student " << name << endl; delete []name; name = NULL; } int main() { Student s1("hehe"); Student s2(s1);// 復制物件 return 0; }
當我們寫了復制建構式時重寫開辟了空間此時就不會出現崩潰的問題了
總結:
1.當物件中存在指標成員時,除了在復制物件時需要考慮自定義拷貝建構式
2.當函式的引數為物件時,實參傳遞給形參的實際上是實參的一個拷貝物件,系統自動通過拷貝建構式實作;
3.當函式的回傳值為一個物件時,該物件實際上是函式內物件的一個拷貝,用于回傳函式呼叫處,4.淺拷貝帶來問題的本質在于解構式釋放多次堆記憶體,使用std::shared_ptr,可以完美解決這個問題,
4..賦值運算子多載
C++為了增強代碼的可讀性引入了運算子多載,運算子多載是具有特殊函式名的函式,也具有其回傳值型別,函式名字以及引數串列,其回傳值型別與引數串列與普通的函式類似,函式名字為:關鍵字operator后面接需要多載的運算子符號,函式原型:回傳值型別 operator運算子(引數串列)注意:不能通過連接其他符號來創建新的運算子:比如operator@ 多載運算子必須有一個型別別或者列舉型別的運算元 用于內置型別的運算子,其含義不能改變,例如:內置的整型+,不 能改變其含義 作為類成員的多載函式時,其形參看起來比運算元數目少1成員函式的 運算子有一個默認的形參this,限定為第一個形參.* 、:: 、sizeof 、?: 、. 注意以上5個運算子不能多載,這個經常在筆試選擇題中出現
賦值運算子主要有四點:
1. 引數型別
2. 回傳值
3. 檢測是否自己給自己賦值
4. 回傳*this
5. 一個類如果沒有顯式定義賦值運算子多載,編譯器也會生成一個,完成物件按位元組序的值拷貝,(按位元組拷貝就是原封不動的拷貝過去,其實就是淺拷貝)
同樣賦值運算子也存在和賦值建構式相同的問題就是當成員變數的含有指標成員時使用編譯器自己生成的賦值運算子只是簡單的值拷貝當析構時會讓同一塊空間析構兩次從而導致程式崩潰
#include <iostream> using namespace std; class Student { public: Student(const char*str); ~Student(); Student(const Student& s){ char* tmp = new char[strlen(s.name) + 1]; strcpy(tmp, s.name); name = tmp; } private: int num; char* name; }; Student::Student(const char*str) { num = 0; char*tmp = new char[strlen(str) + 1]; strcpy(tmp, str); name = tmp; cout << "Student" << endl; } Student::~Student() { //cout << "~Student " << name << endl; delete []name; name = NULL; } int main() { Student s1("hehe"); Student s2("helloworld"); s1 = s2;//使用編譯器默認生成的賦值運算子 return 0; }
此時我們沒有寫operator=當前使用的時編譯器默認生成的,
運行結果:
與賦值建構式相同都是同一塊空間被析構兩次所造成的:
析構兩次:
此時我們需要自己寫operator=進行深拷貝來解決:
#include <iostream> using namespace std; class Student { public: Student(const char*str); ~Student(); Student(const Student& s){//復制建構式深拷貝 char* tmp = new char[strlen(s.name) + 1]; strcpy(tmp, s.name); name = tmp; } Student& operator=(const Student& s) { if (&s != this) { char* tmp = new char[strlen(s.name) + 1];//開空間 strcpy(tmp, s.name);//拷資料 delete[]name;//釋放舊空間 name = tmp; } return *this; } private: int num; char* name; }; Student::Student(const char*str) { num = 0; char*tmp = new char[strlen(str) + 1]; strcpy(tmp, str); name = tmp; cout << "Student" << endl; } Student::~Student() { //cout << "~Student " << name << endl; delete []name; name = NULL; } int main() { Student s1("hehe"); Student s2("helloworld"); s1 = s2; return 0; }
operator總結:
當類中的程式變數中含有指標變數時我們需要考慮是否要顯示的提供賦值運算子多載函式(即自定義賦值運算子多載函式):
用類 A 型別的值為類 A 的物件賦值,且類 A 的資料成員中含有指標的情況下,必須顯式提供賦值運算子多載函式
最后:
最后:🙌🙌🙌🙌
結語:對于個人來講,在編程上進行探索以及總結知識是一件有趣的時間,一個程式員,如果不喜歡編程,那么可能就失去了這份職業的樂趣,刷到我的文章的人,我希望你們可以駐足一小會,忙里偷閑的閱讀一下我的文章,可能文章的內容對你來說很簡單,(^▽^)不過文章中的每一個字都是我認真專注的見證!希望您看完之后,若是能幫到您,勞煩請您簡單動動手指鼓勵我,我必回報更大的付出~
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/375783.html
標籤:其他















我們會發現此時程式崩潰了 


