1.類的默認成員函數
默認成員函數就是??沒有顯式實現,編譯器會?動?成的成員函數稱為默認成員函數。?個類,我們不寫的情況下編譯器會默認?成以下6個默認成員函數,我們主要需要掌握前4個,后兩個了解以下即可,默認成員函數很重要,也?較復雜,我們要從兩個??去學習:
? 第?:我們不寫時,編譯器默認?成的函數?為是什么,是否滿?我們的需求。
? 第?:編譯器默認?成的函數不滿?我們的需求,我們需要??實現,那么如何??實現?
2.構造函數
構造函數的主要任務是對象實例化時初始化對象。構造函數的本質是要代替我們之前Stack和Data類中寫的Init函數的功能,構造函數的自動調用的特點對我們很有幫助,因為我們在寫代碼的時候難免會忘記從而初始化導致程序運行錯誤或者奔潰。
構造函數的特點:
1.函數名與類名相同。
2.無返回值(返回值什么都不用寫,也不需要寫void)
3.對象實例化時系統會自動調用對應的構造函數。
4.構造函數可以重載。
5.如果類中沒有顯示定義構造函數,則C++編譯器會自動生成一個無參的默認構造函數,如果類中有定義構造函數的話,編譯器就不會生成。
6.無參構造函數、全缺省構造函數、編譯器默認生成的構造函數,這三個都成為默認構造函數。但這三個函數有且只能有一個存在,不能同時存在。無參構造函數與全缺省構造函數雖然構成函數重載,但是調用的時候會存在歧義(不給實參的話編譯器不知道調用哪個函數
7.構造順序是按照語句的順序進行構造
//構造函數
#include<iostream>
using namespace std;class Date
{
public://無參構造函數Date(){_year = 1;_month = 1;_day = 1;}//帶參構造函數時Date(int year, int month, int day){_year = year;_month = month;_day = day;}//全缺省構造函數//Date(int year = 2025, int month = 2, int day = 23)//{// _year = year;// _month = month;// _day = day;//}private:int _year;int _month;int _day;
};int main()
{Date d1; //調用默認構造函數Date d2(2025, 3, 23);//調用帶參構造函數// 注意:如果通過?參構造函數創建對象時,對象后?不?跟括號,否則編譯器?法 // 區分這?是函數聲明還是實例化對象 // warning C4930: “Date d3(void)”: 未調?原型函數(是否是有意?變量定義的?) Date d3();return 0;
}
7.編譯器默認生成的構造函數,對內置類型成員變量(比如:char、int等)的初始化沒有要求,也就是說是否初始化取決于編譯器。對于自定義類型成員變量(通過class、struct、enum等關鍵字定義的),要求調用這個成員變量的默認構造函數初始化。如果這個自定義類型成員變量沒有默認構造函數,編譯器會報錯。
#include<iostream>
using namespace std;class Stack
{
public://默認構造函數Stack(int n = 4){int* _a = (int*)malloc(sizeof(int) * n);if (_a == nullptr){perror("malloc,fail!");return;}_capacity = n;_top = 0;}private:int* _a;int _capacity;int _top;
};//兩個棧實現隊列
class MyQueue
{
public://因為Stack是自定義類型成員變量//編譯器默認生成MyQueue的構造函數會調用Stack的構造函數,從而完成兩個成員的初始化
private:Stack pushST;Stack popST;
};int main()
{MyQueue mq;return 0;
}
(說明:C++把類型分成內置類型(基本類型)和?定義類型。內置類型就是語?提供的原?數據類型,如:int/char/double/指針等,?定義類型就是我們使?class/struct等關鍵字??定義的類型。)
3.析構函數
析構函數與構造函數功能相反,析構函數不是完成對對象本?的銷毀,?如局部對象是存在棧幀的,函數結束棧幀銷毀,他就釋放了,不需要我們管,C++規定對象在銷毀時會?動調?析構函數,完成對象中資源的清理釋放?作。析構函數的功能類?我們之前Stack實現的Destroy功能,?像Date沒有Destroy,其實就是沒有資源需要釋放,所以嚴格說Date是不需要析構函數的。
析構函數的特點:
1.析構函數名是在類名前加上字符~
2.析構函數無參數無返回值(與構造函數一樣不需要家void)
3.一個類只能有一個析構函數。如果沒有顯示定義,系統會自動生成默認的析構函數。
4.對象生命周期結束時,系統會自動調用析構函數。
5.編譯器自動生成的默認析構函數不會對內置類型成員做處理,自定義類型成員會調用他的析構函數。
6.我們顯示寫析構函數,對于自定義類型成員依舊會調用他的析構,也就是說自定義類型無論什么情況都會自動調用析構函數。
7.如果類中沒有申請資源時,析構函數可以不寫,直接使用編譯器生成的默認析構函數即可,如Date;如果默認?成的析構就可以?,也就不需要顯?寫析構,如MyQueue;但是有資源申請時,?定要??寫析構,否則會造成資源泄漏,如Stack。
8.一個局部域的多個對象,C++規定后定義的先析構。
//析構函數
#include<iostream>
using namespace std;class Stack
{
public://構造函數Stack(int n = 4){int* _a = (int*)malloc(sizeof(int) * n);if (_a == nullptr){perror("malloc fail!");return;}_capacity = n;_top = 0;}//析構函數~Stack(){cout << "~Stack" << endl;free(_a);_a = nullptr;_capacity = _top = 0;}private:int* _a;int _capacity;int _top;
};//兩個棧實現隊列
class MyQueue
{
public://編譯器默認生成MyQueue的析構函數調用了Stack的析構,釋放了Stack內部的資源//顯示寫析構也會自動調用Stack的析構//~MyQueue();
private:Stack pushst;Stack popst;
};int main()
{Stack st;MyQueue mq;return 0;
}
構造函數和析構函數的調用順序
構造順序是按照語句的順序進行的,析構是按照構造的相反順序進行的。
因此代碼1中變量a和b的構造函數和析構函數的調用順序是:a構造,b構造,b析構,a析構
//代碼1
Class A;
Class B;void F() {A a;B b;
}
代碼2涉及static對象,需要注意的是:
1.全局對象先于局部對象進行構造。
2.局部對象按照出現的順序進行構造,無論是否是static。
3.static會改變對象的生存作用域,需要等待程序結束后才會析構對象。
4.static改變對象的生存作用域后,會放在局部對象之后進行析構。
所以代碼2的構造順序是:c構造、a構造、b構造、d構造
析構順序是:b析構、a析構、d析構、c析構
//代碼2
C c;int main()
{A a;B b;static D d;return 0;
}
4.拷貝構造函數
如果一個構造函數的第一個參數是自身類類型對象的引用,且任何額外的參數都有默認值,則這個構造函數也叫做拷貝構造函數,也就是說拷貝構造函數是一個特殊的構造函數。
class Date
{
public://構造函數Date(int year = 2024, int month = 3, int day = 24){_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;
};
拷貝構造函數的特點:
1.拷貝構造函數是構造函數的一個重載函數。(名字相同,參數不同的函數)
2.拷貝構造函數的第一個參數必須是自身類類型對象的引用,使用傳值方式編譯器會報錯,因為語法邏輯上會引發無窮遞歸調用。
因為每次調用拷貝構造函數之前要先傳值傳參,而傳值傳參是一種拷貝,這樣又會形成一個新的拷貝構造,調用這個新的拷貝構造函數之前也要先傳值傳參,傳值傳參是一種拷貝,就又會形成一個新的拷貝構造,以此類推,就形成了無窮遞歸。
3.拷貝構造函數也可以有多個參數,大那是第一個參數必須是類類型的對象的引用,后面的參數必須有缺省值。
4.C++規定自定義類型對象進行拷貝行為必須調用拷貝構造,所以這里自定義類型傳值傳參和傳值返回都會調用拷貝構造完成。
//拷貝構造函數
#include<iostream>
using namespace std;class Date
{
public://構造函數Date(int year = 2024, int month = 3, int day = 24){_year = year;_month = month;_day = day;}//拷貝構造函數Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}void Print(){cout << _year << "/" << _month << "/" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{//構造d1Date d1(2025, 2, 24);//寫法1,拷貝構造d2Date d2(d1);//寫法2,拷貝構造d3Date d3 = d1;d1.Print();d2.Print();d3.Print();return 0;
}
5.若未顯示定義拷貝構造,編譯器會自動生成拷貝構造函數。自動生成的拷貝構造對內置類型成員變量會完成值拷貝/淺拷貝(一個字節一個字節的拷貝),對自定義類型成員變量會調用他的拷貝構造。
6. 像Date這樣的類成員變量全是內置類型且沒有指向什么資源,編譯器?動?成的拷 ?構造就可以完成需要的拷?,所以不需要我們顯?實現拷?構造。像Stack這樣的類,雖然也都是內置類型,但是_a指向了資源,編譯器?動?成的拷?構造完成的值拷?/淺拷?不符合我們的需求,所以需要我們??實現深拷?(對指向的資源也進?拷?)。像MyQueue這樣的類型內部主要是?定義類型Stack成員,編譯器?動?成的拷?構造會調?Stack的拷?構造,也不需要我們顯?實現MyQueue的拷?構造。這?還有?個?技巧,如果?個類顯?實現了析構并釋放資源,那么他就需要顯?寫拷?構造,否則就不需要。
// 兩個Stack實現隊列
class MyQueue
{
public:
private:Stack pushst;Stack popst;
};
int main()
{Stack st1;st1.Push(1);st1.Push(2);// Stack不顯?實現拷?構造,??動?成的拷?構造完成淺拷? // 會導致st1和st2??的_a指針指向同?塊資源,析構時會析構兩次,程序崩潰 Stack st2 = st1;MyQueue mq1;// MyQueue?動?成的拷?構造,會?動調?Stack拷?構造完成pushst/popst // 的拷?,只要Stack拷?構造??實現了深拷?,他就沒問題 MyQueue mq2 = mq1;return 0;
}
7.傳值返回會產??個臨時對象調?拷?構造,傳值引?返回,返回的是返回對象的別名(引?),沒有產?拷?。但是如果返回對象是?個當前函數局部域的局部對象,函數結束就銷毀了,那么使?引?返回是有問題的,這時的引?相當于?個野引?,類似?個野指針?樣。傳引?返回可以減少拷?,但是?定要確保返回對象,在當前函數結束后還在,才能?引?返回。
//傳引用返回
Date& Func1()
{Date tmp(2024, 2, 12);return tmp;
}int main()
{//Func1返回了一個局部對象tmp的引用作為返回值//Func1函數結束,tmp對象就銷毀了,相當于一個野引用Date ret = Func1();ret.Print();return 0;
}
5.賦值運算符重載
5.1運算符重載
? 運算符重載是具有特殊名字的函數,他的名字是由operator和后面要定義的運算符共同構成。他同樣具有返回類型和參數列表以及函數體。
? 重載運算符函數的參數個數和該運算符作用的運算對象數量一樣多。一元運算符(如:++,–,!)有一個參數,二元運算符(如==,<,>)有兩個參數,二元運算符左側運算對象傳給第一個參數,右側運算對象傳給第二個參數。
? 如果運算符重載函數是成員函數,則他和其他成員函數一樣第一個運算對象默認傳給隱式指針(this指針),所以運算符重載函數作為成員函數時,參數會少一個。
//寫法1:在類中寫運算符重載函數
#include<iostream>
using namespace std;class Date
{
public://默認構造函數Date(int year = 2023, int month = 2, int day = 24){_year = year;_month = month;_day = day;}//第一個參數是this指針bool operator==(Date d2){return _year == d2._year&& _month == d2._month&& _day == d2._day;}private:int _year;int _month;int _day;
};int main()
{Date d1;Date d2;d1.operator==(d2);d1 == d2;return 0;
}
//寫法2:開放成員變量(不建議這種寫法)
#include<iostream>
using namespace std;class Date
{
public://默認構造函數Date(int year = 2023, int month = 2, int day = 24){_year = year;_month = month;_day = day;}
//將成員變量共有
//private:int _year;int _month;int _day;
};bool operator==(Date d1, Date d2)
{return d1._year == d2._year&& d1._month == d2._month&& d1._day == d2._day;
}int main()
{Date d1;Date d2;operator==(d1, d2);d1 == d2;//兩種調用寫法都可return 0;
}
//寫法3,在類中寫獲取成員變量的成員函數(較麻煩,也不建議)
#include<iostream>
using namespace std;class Date
{
public://默認構造函數Date(int year = 2023, int month = 2, int day = 24){_year = year;_month = month;_day = day;}int Get_year(){return _year;}int Get_month(){return _month;}int Get_day(){return _day;}private:int _year;int _month;int _day;
};bool operator==(Date d1, Date d2)
{return d1.Get_year() == d2.Get_year()&& d1.Get_month() == d2.Get_month()&& d1.Get_day() == d2.Get_day();
}int main()
{Date d1;Date d2;operator==(d1, d2);d1 == d2;return 0;
}
? 運算符重載以后,其優先級和結合性與對應的內置類型運算符保持?致。
? 不能通過連接語法中沒有的符號來創建新的操作符:?如operator@。
?
注意以上5個運算符不能重載。
//.*運算符
#include<iostream>
using namespace std;class A
{
public:void func(){cout << "A::func()" << endl;}
};typedef void(A::* PF)();//成員函數指針類型
//定義了一個名為PF的類型
//它是指向類A,不接受任何參數且返回值為void的成員函數的指針類型int main()
{//C++規定成員函數要加&才能取到函數指針PF pf = &A::func;A obj;//對象調用成員函數指針時,使用.*運算符(obj.*pf)();return 0;
}
? 重載操作符?少有?個類類型參數,不能通過運算符重載改變內置類型對象的含義,如:
int operator+(int x, int y)//錯誤寫法
? ?個類需要重載哪些運算符,是看哪些運算符重載后有意義,?如Date類重載operator-就有意義,但是重載operator+就沒有意義。
? 重載++運算符時,有前置++和后置++,運算符重載函數名都是operator++,?法很好的區分。C++規定,后置++重載時,增加?個int形參,跟前置++構成函數重載,?便區分。
//定義的時候
//前置++
類名 operator++();//后置++
類名 operator++(int);//引用的時候
//前置++
類名.operator++();//后置++
類名.operator++(0);//0或者其他整數都行
? 重載<<和>>時,需要重載為全局函數,因為重載為成員函數,this指針默認搶占了第?個形參位置,第?個形參位置是左側運算對象,調?時就變成了對象<<cout,不符合使?習慣和可讀性。重載為全局函數把ostream/istream放到第?個形參位置就可以了,第?個形參位置當類類型對象。
5.2賦值運算符重載
賦值運算符重載是?個默認成員函數,?于完成兩個已經存在的對象直接的拷?賦值,這?要注意跟拷?構造區分,拷?構造?于?個對象拷?初始化給另?個要創建的對象。
賦值運算符重載的特點:
- 賦值運算符重載是?個運算符重載,規定必須重載為成員函數。賦值運算重載的參數建議寫成const當前類類型引?,否則會傳值傳參會有拷?。
- 有返回值,且建議寫成當前類類型引?,引?返回可以提?效率,有返回值?的是為了?持連續賦值場景。
- 沒有顯式實現時,編譯器會?動?成?個默認賦值運算符重載,默認賦值運算符重載?為跟默認拷?構造函數類似,對內置類型成員變量會完成值拷?/淺拷?(?個字節?個字節的拷?),對?定義類型成員變量會調?他的賦值重載函數。
- 像Date這樣的類成員變量全是內置類型且沒有指向什么資源,編譯器?動?成的賦值運算符重載就可以完成需要的拷?,所以不需要我們顯?實現賦值運算符重載。像Stack這樣的類,雖然也都是內置類型,但是_a指向了資源,編譯器?動?成的賦值運算符重載完成的值拷?/淺拷?不符合我們的需求,所以需要我們??實現深拷?(對指向的資源也進?拷?)。像MyQueue這樣的類型內部主要是?定義類型Stack成員,編譯器?動?成的賦值運算符重載會調?Stack的賦值運算符重載,也不需要我們顯?實現MyQueue的賦值運算符重載。這?還有?個?技巧,如果?個類顯?實現了析構并釋放資源,那么他就需要顯?寫賦值運算符重載,否則就不需要。 因為有寫析構函數說明類成員函數有指向資源,淺拷貝就不能滿足功能的實現。
//賦值運算符重載
#include<iostream>
using namespace std;class Date
{
public://構造函數Date(int year = 2025, int month = 3, int day = 25){_year = year;_month = month;_day = day;}//賦值運算符重載Date& operator=(const Date& d){_year = d._year;_month = d._month;_day = d._day;return *this;}void Print(){cout << _year << "/" << _month << "/" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1(2025, 3, 6);d1.Print();Date d2(2025, 3, 10);d2.Print();//兩種寫法都可以d1 = d2;d1.operator=(d2);d1.Print();return 0;
}
6.取地址運算符重載
6.1const成員函數
? 將const修飾的成員函數稱之為const成員函數,const修飾成員函數放到成員函數參數列表的后?。
返回值 函數名(參數1,參數2...) const
? const實際修飾該成員函數隱含的this指針,表明在該成員函數中不能對類的任何成員進?修改。const修飾Date類的Print成員函數,Print隱含的this指針由 Date* const this 變為 const Date* const this
//const成員函數
#include<iostream>
using namespace std;class Date
{
public://構造函數Date(int year = 2024, int month = 3, int day = 25){_year = year;_month = month;_day = day;}//void Print(const Date* const this) constvoid Print() const{cout << _year << "/" << _month << "/" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{//非const對象調用const成員函數是一種權限的縮小Date d1(2025, 3, 25);d1.Print();Date d2(2024, 1, 3);d2.Print();return 0;
}
6.2取地址運算符重載
取地址運算符重載分為普通取地址運算符重載和const取地址運算符重載,?般這兩個函數編譯器?動?成的就可以夠我們?了,不需要去顯?實現。除??些很特殊的場景,?如我們不想讓別?取到當前類對象的地址,就可以??實現?份,胡亂返回?個地址。
class Date
{
public:Date* operator&(){return this;//return nullptr;//return (Date*)0x0012FF40;}const Date* operator&() const{return this;//return nullptr;//return (Date*)0x0012FF41;}private:int _year;int _month;int _day;
};