類和對象(中)
1.類的6個默認成員函數
如果一個類中什么成員都沒有,簡稱為空類。
空類中真的什么都沒有嗎?并不是,任何類在什么都不寫時,編譯器會自動生成以下6個默認成員函數。
默認成員函數:用戶沒有顯式實現,編譯器會生成的成員函數稱為默認成員函數。
2.構造函數
2.1概念
對于以下Date類:
# include<iostream>
using namespace std;class Date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print() // void Print(Date* this) 編譯后{cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1,d2;d1.Init(2024, 5, 26);// d1.Init(&d1, 2024, 5, 26); // 編譯后d2.Init(2077, 5, 26);// d2.Init(&d2, 2024, 5, 26);d1.Print(); // 2024-5-26 // d1.Print(&d1)d2.Print(); // 2077-5-26 // d2.Print(&d2);return 0;
}
對于Date類,可以通過 Init 公有方法給對象設置日期,但如果每次創建對象時都調用該方法設置
信息,未免有點麻煩,那能否在對象創建時,就將信息設置進去呢?
構造函數是一個特殊的成員函數,名字與類名相同,創建類類型對象時由編譯器自動調用,以保證每個數據成員都有 一個合適的初始值,并且在對象整個生命周期內只調用一次。
2.2特性
構造函數是特殊的成員函數,需要注意的是,構造函數雖然名稱叫構造,但是構造函數的主要任務并不是開空間創建對象,而是初始化對象。
其特征如下:
-
函數名與類名相同。
-
無返回值。
-
對象實例化時編譯器自動調用對應的構造函數。
-
構造函數可以重載。
我們來看例子:
# include<iostream>
using namespace std;class Date
{
public:// 構造函數——在對象構建時調用的函數,這個函數完成初始化工作// [要注意這個函數只完成實例對象的初始化,不參與對象的構造,那是編譯器的工作]Date(int year, int month, int day) // 構造函數的名和類名相同{_year = year;_month = month;_day = day;}// 構造函數不要返回值// 構造函數也支持重載Date() // 無參構造函數{_year = 0;_month = 0;_day = 0;}// 這個函數按需使用void Init(int year, int month, int day)// void Init(Date* this, int year, int month, int day){_year = year; // 如果我們設置的成員變量名稱不加_或者m 這里就不好理解_month = month;_day = day;}void Print() // void Print(Date* this) 編譯后{cout << _year << "-" << _month << "-" << _day << endl;// cout << this->_year << "-" << this->_month << "-" << this->_day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1(2024, 5, 26); // 直接給實例對象傳參,由自動調用的構造函數完成初始化。Date d2(2077, 5, 26);Date d3; // 通過函數重載實現,調用無參構造函數// 注意這里不能Date d3();d1.Print(); // 2024-5-26 // d1.Print(&d1);d2.Print(); // 2077-5-26 // d2.Print(&d2);// 是通過隱含的this指針實現的d3.Print(); // 0 - 0 - 0return 0;
}
- 如果類中沒有顯式定義構造函數,則C++編譯器會自動生成一個無參的默認構造函數,一旦用戶顯式定義編譯器將不再生成。
// 如果類中沒有顯式定義構造函數,則C++編譯器會自動生成一個無參的默認構造函數,一旦用戶顯式定義編譯器將不再生成。
# include<iostream>
using namespace std;class Time
{
public:Time(){_hour = 0;_min = 0;_second = 0;cout << "Time()" << endl;}private:int _hour;int _min;int _second;
};class Date
{
public:// 這里我們沒有顯示定義構造函數,編譯器會生成無參默認構造函數// 一旦用戶定義了顯式構造函數。那么編譯器將不會生成無參構造函數// 即使只定義了帶參構造函數,編譯器也不會生成無參構造函數// 想要無參構造函數,要自己再定義一個//Date(int year, int month, int day) //{// _year = year;// _month = month;// _day = day;//}void Init(int year, int month, int day){_year = year; _month = month;_day = day;}void Print() // void Print(Date* this) 編譯后{cout << _year << "-" << _month << "-" << _day << endl;// cout << this->_year << "-" << this->_month << "-" << this->_day << endl;}private:int _year;int _month;int _day;Time _t;
};int main()
{Date d1; // 調用編譯器生成的無參默認構造函數,d1.Print();// -858993460--858993460--858993460// 我們發現,即使d1調用了編譯器的默認構造函數,打印出來還是隨機值,看起來這個構造函數什么都沒做一樣// 但是實際上,這個無參默認構造函數是有做事情的// 我們給Date一個自定義的成員變量,_t// d1.Print();的結果如下://Time()//-858993460--858993460--858993460// 這個說明了一個現象// 默認生成的無參構造函數(語法坑:雙標)// 1. 針對內置類型的成員變量不會做處理// 2. 針對自定義類型的成員變量,調用它的構造函數初始化return 0;
}
- 關于編譯器生成的默認成員函數,很多人會有疑惑:不實現構造函數的情況下,編譯器會生成默認的構造函數。但是看起來默認構造函數又沒什么用?d對象調用了編譯器生的默認構造函數,但是d對象_year/_month/_day,依舊是隨機值。也就說在這里編譯器生成的默認構造函數并沒有什么用??
解答:C++把類型分成內置類型(基本類型)和自定義類型。內置類型就是語言提供的數據類型,如:int/char…,自定義類型就是我們使用class/struct/union等自己定義的類型,看看下面的程序,就會發現編譯器生成默認的構造函數會對自定類型成員_t調用的它的默認成員函數。
總結:
默認生成的無參構造函數(語法坑:雙標)
-
針對內置類型的成員變量不會做處理
-
針對自定義類型的成員變量,調用它的構造函數初始化
-
一旦用戶定義了顯式構造函數。那么編譯器將不會生成無參構造函數
即使只定義了帶參構造函數,編譯器也不會生成無參構造函數
想要無參構造函數,要自己再定義一個
注意:C++11 中針對內置類型成員不初始化的缺陷,又打了補丁,即:內置類型成員變量在類中聲明時可以給默認值。
class Time
{
public:Time(){cout << "Time()" << endl;_hour = 0;_minute = 0;_second = 0;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本類型(內置類型)int _year = 1970;int _month = 1;int _day = 1;// 自定義類型Time _t;
};
int main()
{Date d;return 0;
}
但是這樣也不夠好,我們可以用之前學習的缺省參數——全缺省參數
- 無參的構造函數和全缺省的構造函數都稱為默認構造函數,并且默認構造函數只能有一個。注意:無參構造函數、全缺省構造函數、我們沒寫編譯器默認生成的構造函數,都可以認為是默認構造函數
# include<iostream>
using namespace std;
class Date
{
public: 帶參構造函數//Date(int year, int month, int day) //{// _year = year;// _month = month;// _day = day;//}// 構造函數不能有返回值 構造函數也支持重載//Date() // 無參構造函數//{// _year = 0;// _month = 0;// _day = 0;//}// 更好的方式// 構造函數——全缺省Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;} 注意,有了全缺省的構造函數,不能再有無參構造函數,編譯的時候會產生歧義//Date() // 無參構造函數//{// _year = 0;// _month = 0;// _day = 0;//}void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print() // void Print(Date* this) 編譯后{cout << _year << "-" << _month << "-" << _day << endl;// cout << this->_year << "-" << this->_month << "-" << this->_day << endl;}private:int _year;int _month;int _day;};int main()
{Date d1; // 調用默認構造函數// 1. 自己實現的無參構造函數// 2. 自己實現的全缺省構造函數// 3. 編譯器自動生成的默認構造函數// 這三個的特點都是無參,不傳參數// 因此這三個只能存在一個Date d2(2024, 5, 27);d1.Print();// 0-0-0d2.Print();// 2024-5-27return 0;
}
總結:
默認構造函數一共有三種:
- 自己實現的無參構造函數
- 自己實現的全缺省構造函數
- 編譯器自動生成的默認構造函數
這三種默認構造函數只能存在一種
3.析構函數
3.1 概念
通過前面構造函數的學習,我們知道一個對象是怎么來的,那一個對象又是怎么沒呢的?
析構函數:與構造函數功能相反,析構函數不是完成對對象本身的銷毀,局部對象銷毀工作是由編譯器完成的。而對象在銷毀時會自動調用析構函數,完成對象中資源的清理工作。
3.2 特性
析構函數是特殊的成員函數,其特征如下:
-
析構函數名是在類名前加上字符 ~。
-
無參數無返回值類型。
-
一個類只能有一個析構函數。若未顯式定義,系統會自動生成默認的析構函數。注意:析構函數不能重載
-
對象生命周期結束時,C++編譯系統系統自動調用析構函數
來看一段代碼:
// 析構函數——在實例對象的生命周期結束的時候,會自動調用析構函數
# include<iostream>
using namespace std;
class Date
{
public:// 構造函數——全缺省 [一種默認構造函數]Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;}void Print() // void Print(Date* this) 編譯后{cout << _year << "-" << _month << "-" << _day << endl;}~Date(){cout << "析構函數" << endl;}private:int _year;int _month;int _day;};int main()
{// 析構函數,會在這兩個對象生命周期結束之后自動調用,完成清理工作,不是完成對其的銷毀Date d1; // 調用默認構造函數Date d2(2024, 5, 27);// 執行代碼,會執行兩次析構函數//析構函數//析構函數//注意這個析構,先析構d2 在析構d1,因為是在棧區的變量return 0;
}
在上述代碼中要注意:析構,先析構d2 在析構d1,因為是在棧區的變量
而有了析構函數和構造函數的存在,我們來感受一下它們的使用。
這里我們來寫一個Stack類,這個Stack類,不在需要初始化和銷毀的接口,由構造函數和析構函數代替了。
// 實現Stack類——使用構造和析構函數
# include<iostream>
using namespace std;class Stack
{
public:// 構造函數Stack(int n = 10){_a = (int*)malloc(sizeof(int) * n);if (_a == NULL){perror("Stack()malloc()");exit(-1);}cout << "malloc:" << _a << endl; _size = 0;_capacity = n;}// ....... 棧的接口// 析構函數~Stack(){free(_a);cout << "free:" << _a << endl;_a = NULL;_size = _capacity = 0;}private:int* _a;int _size;int _capacity;
};int main()
{Stack s1; Stack s2; // 注意先析構s2 在析構s1。因為兩個局部變量在棧區上 ,后進先出//malloc:000001FC0C62BEF0//malloc : 000001FC0C636160//free : 000001FC0C636160//free : 000001FC0C62BEF0return 0;
}
- 關于編譯器自動生成的析構函數,是否會完成一些事情呢?下面的程序我們會看到,編譯器生成的默認析構函數,對自定類型成員調用它的析構函數。
# include<iostream>
using namespace std;class Time
{
public:~Time(){cout << "~Time()" << endl;}
private:int _hour;int _minute;int _second;
};class Date
{
private:// 基本類型(內置類型)int _year = 1970;int _month = 1;int _day = 1;// 自定義類型Time _t;
};
int main()
{Date d;return 0;
}
// 程序運行結束后輸出:~Time()
// 在main方法中根本沒有直接創建Time類的對象,為什么最后會調用Time類的析構函數?
// 因為:main方法中創建了Date對象d,而d中包含4個成員變量,其中_year, _month, _day三個是內置類型成員,銷毀時不需要資源清理,最后系統直接將其內存回收即可;而_t是Time類對象,所以在d銷毀時,要將其內部包含的Time類的_t對象銷毀,所以要調用Time類的析構函數。但是:
// main函數中不能直接調用Time類的析構函數,實際要釋放的是Date類對象,所以編譯器會調用Date類的析構函數,而Date沒有顯式提供,則編譯器會給Date類生成一個默認的析構函數,目的是在其內部調用Time類的析構函數,即當Date對象銷毀時,要保證其內部每個自定義對象都可以正確銷毀// main函數中并沒有直接調用Time類析構函數,而是顯式調用編譯器為Date類生成的默認析構函數
// 注意:創建哪個類的對象則調用該類的析構函數,銷毀那個類的對象則調用該類的析構函數
總結:
默認生成的析構函數(語法坑:雙標)
-
針對內置類型的成員變量不會做處理
-
針對自定義類型的成員變量,調用它的析構函數
-
一旦用戶定義了顯式析構函數。那么編譯器將不會生成默認析構函數
- 如果類中沒有申請資源時,析構函數可以不寫,直接使用編譯器生成的默認析構函數,比如Date類;有資源申請時,一定要寫,否則會造成資源泄漏,比如Stack類。
4.拷貝構造函數
4.1概念
在現實生活中,可能存在一個與你一樣的自己,我們稱其為雙胞胎
那在創建對象時,可否創建一個與已存在對象一某一樣的新對象呢?
拷貝構造函數:只有單個形參,該形參是對本類類型對象的引用(一般常用const修飾),在用已存在的類類型對象創建新對象時由編譯器自動調用
4.2特征
拷貝構造函數也是特殊的成員函數,其特征如下:
-
拷貝構造函數是構造函數的一個重載形式。
-
拷貝構造函數的參數只有一個且必須是類 類型對象的引用,使用傳值方式編譯器直接報錯,因為會引發無窮遞歸調用。
我們來看一個例子:
// 拷貝構造函數
# include<iostream>
using namespace std;class Date
{
public:// 構造函數Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;}// 這樣無法編譯通過,因為這里會造成遞歸拷貝,會無窮遞歸下去Date(Date d) // 構造函數的重載函數{_year = d._year;_month = d._month;_day = d._day;}// 析構函數~Date(){cout << "析構函數" << endl;}private:int _year;int _month;int _day;};int main()
{Date d1(2024, 5, 27);// 如果我們想創建一個跟d1一模一樣的對象,我們可以怎么做呢?//Date d2(2024, 5, 27);// 這樣可以,但是肯定不好,因為d1一變,我們還要手動更改d2// 這個時候我們就可以用到拷貝構造Date d2(d1); // 傳d1進去,// 這里會報錯return 0;
}
為什么會造成無窮遞歸呢?
// 這樣無法編譯通過,因為這里會造成遞歸拷貝,會無窮遞歸下去Date(Date d) // 構造函數的重載函數{_year = d._year;_month = d._month;_day = d._day;}
int main()
{// Date d2(d1);
}
因為我們這里是傳值給拷貝構造函數,我們知道,傳值調用的形參就是實參的一個臨時拷貝,既然是拷貝,我們就要創建一個跟實參一個類型的值,那實參是什么類型的值呢,是Date,相當于要把實參d1 拷貝給 形參d,那d的構造又需要調用拷貝構造函數,那又要傳參,又要拷貝實參,又要構造形參,又要調用拷貝構造函數。無限循環下去。
如圖所示:
解決方法是什么呢?
其實就是讓形參是實參的別名就好了。
// 拷貝構造函數
# include<iostream>
using namespace std;class Date
{
public:// 構造函數Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;} 這樣無法編譯通過,因為這里會造成遞歸拷貝,會無窮遞歸下去//Date(Date d)//{// _year = d._year;// _month = d._month;// _day = d._day;//}// 我們使用別名來解決這個問題Date(const Date& d) // 傳參的過程相當于 Date& d = d1;{_year = d._year;_month = d._month;_day = d._day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}// 析構函數~Date(){cout << "析構函數" << endl;}private:int _year;int _month;int _day;};int main()
{Date d1(2024, 5, 27);// 如果我們想創建一個跟d1一模一樣的對象,我們可以怎么做呢?//Date d2(2024, 5, 27);// 這樣可以,但是肯定不好,因為d1一變,我們還要手動更改d2// 這個時候我們就可以用到拷貝構造Date d2(d1); // 傳d1進去Date d3 = d1; // 這個也是拷貝構造。d2.Print();d3.Print();return 0;
}
上面這個代碼的運行結果
- 若未顯式定義,編譯器會生成默認的拷貝構造函數。 默認的拷貝構造函數對象按內存存儲按字節序完成拷貝,這種拷貝叫做淺拷貝,或者值拷貝。
class Time
{
public:Time(){_hour = 1;_minute = 1;_second = 1;}Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;cout << "Time::Time(const Time&)" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本類型(內置類型)int _year = 2024;int _month = 5;int _day = 28;// 自定義類型Time _t;
};int main()
{Date d1;// 用已經存在的d1拷貝構造d2,此處會調用Date類的拷貝構造函數// 但Date類并沒有顯式定義拷貝構造函數,則編譯器會給Date類生成一個默認的拷貝構造函數Date d2(d1);return 0;
}
- 編譯器生成的默認拷貝構造函數已經可以完成字節序的值拷貝了,還需要自己顯式實現嗎?當然像日期類這樣的類是沒必要的。那么下面的類呢?驗證一下試試?
// 涉及內存資源管理的Stack的拷貝構造
# include<iostream>
using namespace std;class Stack
{
public:// 構造函數Stack(int n = 10){_a = (int*)malloc(sizeof(int) * n);if (_a == NULL){perror("Stack()malloc()");exit(-1);}cout << "malloc:" << _a << endl;_size = 0;_capacity = n;}// ....... 棧的接口// 析構函數~Stack(){free(_a);cout << "free:" << _a << endl;_a = NULL;_size = _capacity = 0;}private:int* _a;int _size;int _capacity;
};int main()
{// 淺拷貝問題Stack s1;Stack s2(s1);return 0;
}
上面我們的Stack類中,沒有顯式實現的拷貝構造,因此我們在main函數中的拷貝構造 的使用,用的都是編譯器生成的默認的函數。這里用的是淺拷貝。因此造成了c++中比較經典的一個問題——淺拷貝問題
我們來分析一下代碼為何會崩潰。
- 首先就是我們在調用拷貝構造的時候,我們是將s1對象一個一個字節拷貝到s2中的,這就意味著我們兩個對象所存儲的數組指針_a是一樣的,也就是說指向的數組都是同一個空間的數組
- 這個時候我們的代碼還不會崩潰,并且也確實成功完成了拷貝的任務
- 但是問題會出現在析構函數上,我們知道在析構函數中,我們要完成對棧這個類對象的資源清理工作,我們要在析構函數中釋放掉數組的空間,由于s1 s2存儲在棧區,我們先釋放掉s2的數組空間,但是緊接著我們要釋放掉s1的數組空間,但是s1和s2指向的數組是同一個數組,這就會造成會同一個空間的重復釋放,這就會導致代碼崩潰
因此對于這種涉及到內存資源管理的類,我們需要自己實現深拷貝的拷貝構造
但是這里有人會說,那我不自己實現析構函數不就不會報錯了嗎?
因為我們的析構函數中存在對數組空間的釋放,但是不釋放就會造成內存泄漏的問題,釋放了就會造成淺拷貝問題。
- 拷貝構造函數典型調用場景:
- 使用已存在對象創建新對象
- 函數參數類型為類類型對象
- 函數返回值類型為類類型對象
class Date
{
public:Date(int year, int minute, int day){cout << "Date(int,int,int):" << this << endl;}Date(const Date& d){cout << "Date(const Date& d):" << this << endl;}~Date(){cout << "~Date():" << this << endl;}
private:int _year;int _month;int _day;
};
Date Test(Date d) // 調用拷貝構造函數
{Date temp(d);return temp; // 調用拷貝構造函數
}
int main()
{Date d1(2022, 1, 13); // 調用拷貝構造函數Test(d1);return 0;
}
5.賦值運算符重載
5.1 運算符重載
為什么要搞運算符重載呢?
其實很簡單,就是因為內置類型,我們在使用運算符的時候。編譯器知道怎么去比較,但是如果我們自己定義了一個類,在用內置的運算符,編譯器就不知道怎么比較了,因此,為了能夠方便的比較我們自己定義的類對象,我們需要對運算符重載
在之前我們是通過自定義函數,來解決這個問題的,但是這個解決方式也沒有運算符重載好。因為可讀性不是很好。
C++為了增強代碼的可讀性引入了運算符重載,運算符重載是具有特殊函數名的函數,也具有其返回值類型,函數名字以及參數列表,其返回值類型與參數列表與普通的函數類似。
函數名字為:關鍵字operator后面接需要重載的運算符符號。
函數原型:返回值類型 operator操作符(參數列表)
注意:
-
不能通過連接其他符號來創建新的操作符:比如operator@
-
重載操作符必須有一個類類型參數
-
用于內置類型的運算符,其含義不能改變,例如:內置的整型+,不能改變其含義
-
作為類成員函數重載時,其形參看起來比操作數數目少1,因為成員函數的第一個參數為隱藏的this指針形參
-
.* :: sizeof ?: .
注意以上5個運算符不能重載。這個經常在筆試選擇題中出現。注意是 **.**不能重載,不是 (解引用)
我們來看一段代碼:
// 運算符重載
# include<iostream>
using namespace std;class Date
{
public:// 默認構造函數Date(int year = 0, int month = 0, int day = 0){_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;};// 運算符有幾個操作數, operator重載的函數就有幾個參數
bool operator==(const Date& d1, const Date& d2)
{return d1._year == d2._year&& d1._month == d2._month&& d1._day == d2._day;
}int main()
{Date d1(2024, 5, 27);Date d2(d1);// 如果我們相對Date類的 對象 d1 d2 進行比較// 我們可以通過定義函數實現//IsDateEqual(d1, d2); // 可讀性不好// 為了讓可讀性更好,c++推出了運算符重載d1 == d2;// 編譯過后會變成, operator==(d1, d2)// 如果不對== 進行重載,那么這里就會編譯出錯operator==(d1, d2); // 等價d1 == d2 ,但是不推薦這樣寫,這樣可讀性又下降了if (d1 == d2)cout << "相等" << endl;return 0;
}
上述的寫法不好,因為我們為了實現運算符重載,我們犧牲了Date類中成員變量的私有性。把成員變量變成公有的,才能讓運算符重載函數訪問到其成員變量。
要如何解決這個問題呢?
這里其實可以用友元解決,這個后面學習
我們還可以通過把運算符重載函數寫進類中來解決
# include<iostream>
using namespace std;class Date
{
public:// 默認構造函數Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;}Date(const Date& d) // 拷貝構造函數{_year = d._year;_month = d._month;_day = d._day;}// 運算符重載// 我們知道成員函數會自帶this指針形參,因此這里要修改函數的形參個數。// 我們知道誰調用這個運算符重載函數,那么this指針就指向誰// d1 == d2;// d1.operator==(d2);bool operator==(const Date& d) // bool operator==(Date* this, const Date& d){return _year == d._year&& _month == d._month&& _day == d._day;}// d1 > d2bool operator>(const Date& d){if (_year > d._year){return true;}else if (_year == d._year && _month > d._month){return true;}else if (_year == d._year && _month == d._month && _day > d._day){return true;}elsereturn false;}private:int _year;int _month;int _day;};int main()
{Date d1(2024, 5, 27);Date d2(d1);d1 == d2; // 編譯后變成 d1.operator==(d2)if (d1 > d2)cout << "大于" << endl;elsecout << "其他" << endl;return 0;
}
5.2賦值運算符重載
- 賦值運算符重載格式
- 參數類型:const T&,傳遞引用可以提高傳參效率
- 返回值類型:T&,返回引用可以提高返回的效率,有返回值目的是為了支持連續賦值
- 檢測是否自己給自己賦值
- 返回*this :要復合連續賦值的含義
來看代碼:
// 賦值運算符重載
# include<iostream>
using namespace std;class Date
{
public:// 默認構造函數Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;}// 拷貝構造函數Date(const Date& d) {_year = d._year;_month = d._month;_day = d._day;}// 運算符重載// d1 == d2 -> d1.operator==(d2);bool operator==(const Date& d) // bool operator==(Date* this, const Date& d){return _year == d._year&& _month == d._month&& _day == d._day;}// 賦值運算符重載//d2 = d1 -> d2.operator(&d2, d1)Date& operator=(const Date& d) // 其實這里不用引用傳參也不會報錯,但是使用引用傳參效率更高// void operator(Date* this, const Date d){// 自己給自己賦值是沒有意義的if (this == &d) // d是引用,d的地址和this相同就說明是自己給自己賦值return *this; _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(2024, 5, 27);Date d2 = d1; //注意這里這個=不是賦值運算符,而是拷貝構造Date d3;d3 = d1;d3.Print();// 2024-5-27d3 = d3; // 自己賦值給自己//我們的=除了要實現單個的賦值,還要能實現連續的賦值d2 = d3 = d1;// 先執行d3 = d1,也就是d3.operator=(d1), 其返回值得是d3 才可以d2.Print();return 0;
}
在來看看拷貝構造和 賦值運算符重載的區別:
- 賦值運算符只能重載成類的成員函數不能重載成全局函數
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}int _year;int _month;int _day;
};// 賦值運算符重載成全局函數,注意重載成全局函數時沒有this指針了,需要給兩個參數
Date& operator=(Date& left, const Date& right)
{if (&left != &right){left._year = right._year;left._month = right._month;left._day = right._day;}return left;
}// 編譯失敗:
// error C2801: “operator =”必須是非靜態成員
編譯出錯的原因是:
賦值運算符如果不顯式實現,編譯器會生成默認的賦值運算符重載。此時我們類外邊在定義一個賦值運算符重載,就會和編譯器的產生沖突。因此,賦值運算符重載只能是類的成員函數
- 用戶沒有顯式實現時,編譯器會生成一個默認賦值運算符重載,以值的方式逐字節拷貝。注意:內置類型成員變量是直接賦值的,而自定義類型成員變量需要調用對應類的賦值運算符重載完成賦值。
// 如果沒有顯式實現 拷貝構造 和 賦值運算符重載,編譯器會生成默認的
# include<iostream>
using namespace std;class Date
{
public:// 默認構造函數Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;} 拷貝構造函數//Date(const Date& d)//{// _year = d._year;// _month = d._month;// _day = d._day;//} 賦值運算符重載d2 = d1 -> d2.operator(&d2, d1)//Date& operator=(const Date& d) // 其實這里不用引用傳參也不會報錯,但是使用引用傳參效率更高// // void operator(Date* this, const Date d)//{// // 自己給自己賦值是沒有意義的// if (this == &d) // d是引用,d的地址和this相同就說明是自己給自己賦值// return *this;// _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(2024, 5, 10);Date d2(2024, 5, 28);d1 = d2; // 即使我們類中屏蔽了賦值運算符重載,這里不會報錯// 因為我們不實現的時候,編譯器會生成拷貝構造和 operator= // 會完成按字節的值拷貝(淺拷貝)。// 也就是說有些類,比如這個日期類,我們不需要去實現拷貝構造和賦值運算符重載。// 但是涉及到 內存管理 的類 我們就需要去實現拷貝構造和賦值運算符重載d1.Print();// 2024-5-28d2.Print();// 2024-5-28Date d3(d1); // 屏蔽了拷貝構造函數,也不會報錯,這里調用了編譯器生成的默認拷貝構造函數Date d4 = d3;d3.Print();// 2024-5-28d4.Print();// 2024-5-28return 0;
}
- 值拷貝(淺拷貝):將對象按照一個字節一個字節的傳過去。
注意: 只有六個成員函數我們不顯式實現,編譯器會自動生成。
拷貝構造函數和 賦值運算符函數就是其中之二,其他運算符編譯器在沒有顯式實現的時候不會默認生成
但是這樣就產生了一個問題:
- 我們還需要去自己實現拷貝構造函數和賦值運算符重載嗎?編譯器不是已經幫我們實現了嗎?
答案當時是需要的,因為除了我們的日期類,還有許多類我們會涉及到**(內存的資源管理)**。
我們來看一段代碼來感受一下:
// 涉及內存資源管理的Stack的拷貝構造和賦值運算符重載
# include<iostream>
using namespace std;class Stack
{
public:// 構造函數Stack(int n = 10){_a = (int*)malloc(sizeof(int) * n);if (_a == NULL){perror("Stack()malloc()");exit(-1);}cout << "malloc:" << _a << endl;_size = 0;_capacity = n;}// ....... 棧的接口// 析構函數~Stack(){free(_a);cout << "free:" << _a << endl;_a = NULL;_size = _capacity = 0;}private:int* _a;int _size;int _capacity;
};int main()
{// 淺拷貝問題Stack s1;Stack s3(30);s1 = s3;return 0;
}
上面我們的Stack類中,沒有顯式實現的賦值運算符重載,因此我們再main函數中的賦值運算符的使用,用的都是編譯器生成的默認的函數。這里用的是淺拷貝。造成了淺拷貝問題
前面我們已經知道了拷貝構造函數造成淺拷貝的分析和原理了
對于賦值運算符也是同樣的道理,我們將s3賦值給s1用的是淺拷貝,s1和s3的_a數組指針,指向的都是同一個數組空間,在調用析構函數的時候,也會造成對同一個空間的重復釋放,代碼崩潰.
我們來看圖片來更好的理解一下:
因此對于這種涉及到內存資源管理的類,我們需要自己實現深拷貝的賦值運算符重載
但是這里有人會說,那我不自己實現析構函數不就不會報錯了嗎?
因為我們的析構函數中存在對數組空間的釋放,但是不釋放就會造成內存泄漏的問題,釋放了就會造成淺拷貝問題。
5.3前置++和后置++的重載
直接來看代碼:
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// 前置++:返回+1之后的結果// 注意:this指向的對象函數結束后不會銷毀,故以引用方式返回提高效率Date& operator++(){_day += 1;return *this;}// 后置++:// 前置++和后置++都是一元運算符,為了讓前置++與后置++形成能正確重載// C++規定:后置++重載時多增加一個int類型的參數,但調用函數時該參數不用傳遞,編譯器自動傳遞// 注意:后置++是先使用后+1,因此需要返回+1之前的舊值,故需在實現時需要先將this保存一份,然后給this + 1// 而temp是臨時對象,因此只能以值的方式返回,不能返回引用Date operator++(int){Date temp(*this);_day += 1;return temp;}private:int _year;int _month;int _day;
};int main()
{Date d;Date d1(2022, 1, 13);d = d1++; // d: 2022,1,13 d1:2022,1,14d = ++d1; // d: 2022,1,15 d1:2022,1,15return 0;
}
6.日期類的實現
實際上完整的完善的日期類,我們應該讓聲明和定義分離,并且還要優化代碼。這里為了方便,我們就不讓聲明和定義分離了。
// 實現一個完善的日期類
# include<iostream>
using namespace std;class Date
{
public:int GetMonthDay(int year, int month){// 給13 的原因是為了剛好讓下標對上月份static int monthday[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };// 給static的原因是 每次訪問的數組都不變并且都是這個數組,那每次訪問都要開辟,不如直接放靜態區去// 閏年的2月份 是 29天if (month == 2 && (year % 4 == 0 && year % 100 != 0) || year % 400 == 0)monthday[2] = 29;elsemonthday[2] = 28;int day = monthday[month];return day;}// 默認構造函數Date(int year = 0, int month = 0, int day = 0){// 對傳進來的年 月 日 進行判斷,是否合法,合法才構造if (year >= 0 && month > 0 && month < 13 && day <= GetMonthDay(year, month)){_year = year;_month = month;_day = day;}else{cout << "非法日期" << endl;}}// 拷貝構造函數 (其實日期類這種不涉及內存資源管理的類,不需要我們顯式實現拷貝構造,但是實現了也沒問題)// Date(Date* this, const Date& d) Date(const Date& d) // 一定要有別名 & ,不然會無限遞歸{_year = d._year;_month = d._month;_day = d._day;}// 析構函數~Date() // 其實類似日期類這種類,是不需要我們去編寫顯式析構函數的{cout << " 析構函數" << endl;}// 運算符重載//d1 < d2 編譯器處理后 d1.operator<(&d1, d2)bool operator<(const Date& d)// bool operator(Date* this, const Date& d){if (_year < d._year)return true;else if (_year == d._year && _month < d._month)return true;else if (_year == d._year && _month == d._month && _day < d._day)return true;elsereturn false;}// d1 == d2 編譯器處理后 d1.operator==(&d1, d2)bool operator==(const Date& d)// bool operator==(Date* this, const Date& d){return _year == d._year&& _month == d._month&& _day == d._day;}// d1 <= d2 編譯器處理 d1.operator<=(&d1, d2)bool operator<=(const Date& d) // bool operator<=(Date* this, const Date& d){// 這里我們采用復用上面的代碼來實現,以減少代碼重復return *this < d || *this == d; // *this 解引用就是 d1// (*this).operator<(&(*this), d) || (*this).operator==(&(*this), d);}// d1 > d2 編譯器處理后 d1.operator>(&d1, d2)bool operator>(const Date& d){// 不再采取之前那種代碼,代碼重復性太強,并且一旦成員變量改變,代碼也要跟著大量修改// 我們采取函數復用return !(*this <= d); // 只要*this 不<= d 那就是> d// 編譯器處理后 !((*this).operator<=(&(*this), d))}// d1 >= d2 編譯器處理 d1.operator>=(&d1, d2)bool operator>=(const Date& d)// bool operator>=(Date* this, const Date& d){// 也采用函數復用。復用前面已經實現的 > 和 == 的運算符重載函數return *this > d || *this == d;}// d1 != d2 -> d1.operator!=(&d1, d2)bool operator!=(const Date& d)// bool operator!=(Date* this, const Date& d){// 函數復用return !(*this == d);} d1 + 10(天數) -> d1.operator+(&d1, 10)//Date operator+(int day) // Date operator+(Date* this, int day) 這里不能使用引用返回,因為拷貝構造的ret在函數結束之后就會銷毀//{// Date ret = *this; // 拷貝構造一個d1 ,這里等價于 ret(*this)// ret._day += day;// // 對day進行判斷是否合法,合法就輸出,不合法要進位// while (ret._day > GetMonthDay(ret._year, ret._month))// 有可能不止進一次位,所以給一個循環// {// ret._day -= GetMonthDay(ret._year, ret._month);// ret._month++;// // 要注意月份是否合法// if (ret._month == 13)// {// ret._year++;// ret._month = 1;// }// }// return ret;//}//d1 += 10 -> d1.operator+=(&d1, 10)Date& operator+=(int day)// 這里可以使用引用返回,因為this指向的本來就是外面的d1,這個函數結束之后,d1不會銷毀{// 如果day是負數。要處理if (day < 0){return *this -= -day; // 加負數 相當于 減正數}// += 和 + 的區別是, + 不改變d1本身,但是+=要改變d1本身_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);_month++;// 判斷月份是否合法if (_month == 13){_year++;_month = 1;}}return *this; // 返回的是d1本身}// 上面我們對+的重載和 對+=的重載的代碼重復性很高,那我們就可以考慮采用復用// d1 + 10Date operator+(int day){Date ret(*this); // 拷貝構造d1ret += day; // 復用我們實現的+=的重載// ret.operator+=(day)return ret;} d1 - 10(天數) -> d1.operator-(&d1, 10)//Date operator-(int day)//{// Date ret = *this; // 拷貝構造一個d1// ret._day -= day;// // 判斷-=day之后的 day是否合法,合法就返回,不合法就要退位// while (ret._day <= 0)// {// ret._month--; // 先讓月份退到上一個月// // 判斷月份是否合法// if (ret._month == 0)// {// ret._month = 12; // ret._year--;// }// ret._day += GetMonthDay(ret._year, ret._month); // + 上一個月份的天數,看看是否_day是否合法// }// return ret;//}// d1 -= 10 -> d1.operator(&d1, 10)Date& operator-=(int day) // Date& operator(Date* this, int day){// 判斷day是否是負數if (day < 0){return *this += -day; // 減負數 相當于 加正數}// -= 改變的是d1自己,也就是this指針指向的對象_day -= day;// 判斷_day是否合法,不合法要退位,直至合法while (_day <= 0){// 先讓月份退一位_month--;// 判斷月份是否合法if (_month == 0){_month = 12;_year--;}_day += GetMonthDay(_year, _month); // += 上一個月份的天數,看看是否_day是否合法}return *this; // 返回自身}// 對-的重載采用代碼復用,減少對代碼的重復性,提升維護性// d1 - 10Date operator-(int day){Date ret(*this); // 拷貝構造d1ret -= day; // 函數復用return ret; // 返回ret的時候要創建一個臨時變量, 會調用一次拷貝構造}// ++d1 -> d1.operator++(&d1)Date& operator++() // Date& operator(Date* this){// ++也可以像之前一樣,每次調用+1天,+完之后判斷day是否需要進位,月是否需要進位,年是否需要進位// 但是為了提升類的維護性,和減少代碼重復度,我們采取復用*this += 1;// ++d1就是讓自己去 + 1return *this;}// d1++ -> d1.operator++(&d1, 0)Date operator++(int) // 加int是為了代表是后置++,如果不加就是默認前置++{// 前置++和后置++的區別就是 前置++返回+之后的 后置++返回+之前的Date tmp = *this;*this += 1;return tmp; // 返回+之前的}//--d1Date& operator--(){*this -= 1; // 函數復用return *this;}//d1--Date operator--(int){// 前置-- 要返回--之后的 后置-- 要返回-之前的Date tmp(*this);*this -= 1;return tmp;}// 賦值運算符重載 operator=// d1 = d2 -> d1.operator(&d1, d2)Date& operator=(const Date& d){// 防止自己給自己賦值if (this != &d) // this指向的地址 d是別名,d的地址和this指針相等就說明是自己給自己賦值{_year = d._year;_month = d._month;_day = d._day;}return *this;}// 日期 - 日期 d1 - d2// 我們讓小的日期++ 直至 == 大的日期, + 了多少次,就差了多少天int operator-(const Date& d){int flag = 1;// 默認大的是 this指針指向的 小的是 dDate max = *this; // 拷貝構造Date min = d; // 拷貝構造if (max < min){// 走到這里說明 默認的情況是錯的min = *this;max = d;flag = -1; // 說明是小的 - 大的 最后應該是個負數}int n = 0;while (max > min) // 讓小日期一直加 直至 == 大日期{++min;++n;}// 加了多少次,n就是多少,n就是相差的天數return n * flag;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1(2024, 5, 28);Date d2 = d1; // 等價于 d2(d1)cout << (d1 < d2) << endl; // 0cout << (d1 == d2) << endl;// 1cout << (d1 > d2) << endl;// 0cout << (d1 != d2) << endl;// 0cout << (d1 <= d2) << endl;// 1cout << (d1 >= d2) << endl;// 1// 是否要重載一個運算符,看的是這個運算符是否對該類的對象有意義// 比如日期 + 日期沒有意義, 但是日期 - 日期有意義,是兩日期相隔的天數// 比如 日期 + 天數 有意義,是多少天之后, 日期 - 天數有意義 是多少天之前// 而有+ 就有 += , 有-就有-= , 并且日期 * 日期 和 日期 / 日期是沒有意義的Date d3 = d1 + 10; // 將d1 + 10后的日期 拷貝構造到d3對象d3.Print();// 2024-6-7Date d4(d1 + 100);d4.Print();// 2024-9-5Date d5(d1 + 1000);d5.Print();// 2027-2-22Date d6(d1 - 1000);d6.Print();// 2021-9-1Date d7(d1 -= 1000);d7.Print();// 2021-9-1Date d8(d1++);d8.Print(); // 2021-9-1Date d9(++d1);d9.Print();//2021-9-3Date d10(d1--);d10.Print();// 2021-9-3Date d11(--d1);d11.Print();//2021-9-1cout << d3 - d1 << endl; // 1010 cout << d1 - d3 << endl;// -1010return 0;
}
中間兩次調用析構函數是因為,在日期 - 日期函數中,我們創建了兩個Date類的局部變量,函數棧幀結束后要銷毀變量。調用了析構函數
7.const成員
將const修飾的“成員函數”稱之為const成員函數,const修飾類成員函數,實際修飾該成員函數隱含的this指針,表明在該成員函數中不能對類的任何成員進行修改
我們來看一段代碼:
// const成員
# include<iostream>
using namespace std;class Date
{
public:// 默認構造函數Date(int year = 1, 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;}// 由于this指針是隱含的 所以不能這樣加 void Print(const Date* this)// 因此我們使用const修飾成員函數, 實際上修飾的是 *this, 也就是this指針指向的對象void Print() const // 編譯后 -> void Print(const Date* this){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;};void f(const Date& d)
{d.Print();// 編譯無法通過 編譯后 -> d.Print(&d)// 因為這里的&d 是const Date*類型, 但是Print函數的this形參是 Date* 類型// 涉及到了權限放大的問題,就無法編譯通過
}int main()
{Date d1;f(d1);return 0;
}
這里可以在回憶一下const修飾的用法
-
const Date p1;*
-
Date const p2;*
-
Date const p3;*
第一第二種const都在*的左邊,修飾的都是指針指向的對象,也就是不能修改指針指向的對象。
第三種 const在* 的右邊,修飾的是指針本身,也就是不能讓修改指針本身指向其他地方
上面代碼屬于是對象調用成員函數,那我們再來看看成員函數之間的相互調用,是否也有const之間的關系
來看代碼:
// 成員函數之間調用
# include<iostream>
using namespace std;class Date
{
public:// 默認構造函數Date(int year = 1, 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;}void f1()// void f1(Date* this){f2(); // 編譯后 this->f2(this);// 這里的*this是可讀可寫的,傳進去的*this要變成const Date類型,只能讀// 屬于權限縮小,不會報錯}void f2() const // void f2(const Date* this){}void f3()// void f3(Date* this){}void f4() const // void f4(const Date* this){f3(); // this->f3(this) 會報錯// 這里的*this 只能讀不能改, 但是傳給f3之后會變成可讀可寫的// 屬于權限放大,因此會報錯}void Print() const // 編譯后 -> void Print(const Date* this){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;};int main()
{return 0;
}
看了上面兩段代碼之后,請思考下面的幾個問題:
-
const對象可以調用非const成員函數嗎?【不可以】
-
非const對象可以調用const成員函數嗎?【可以】
-
const成員函數內可以調用其它的非const成員函數嗎?【不可以】
-
非const成員函數內可以調用其它的const成員函數嗎?【可以】
注意:
成員函數之間調用,實際上就是this指針指向的對象在調用。
總結:
只要成員函數中不需要修改成員變量,最好都加上const修飾成const成員函數
不然有const類型的對象傳進來就會報錯,因為權限縮小了。上面我們也有講。
如圖所示:
8.取地址及const取地址操作符重載
這兩個默認成員函數一般不用重新定義 ,編譯器默認會生成。
//取地址及const取地址操作符重載
# include<iostream>
using namespace std;class Date
{
public:// 默認構造函數Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// 取地址操作符重載Date* operator&(){cout << "取地址操作符重載" << endl;return this;}// 取地址操作符重載const Date* operator&() const{cout << "const取地址操作符重載" << endl;return this;}void Print() const // 編譯后 -> void Print(const Date* this){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;};int main()
{Date d1, d2;const Date d3, d4;cout << &d1 << endl;//取地址操作符重載//00000008B33CF528cout << &d2 << endl;//取地址操作符重載//00000008B33CF558//cout << &d3 << endl; // 這里不會調用我們實現的&重載函數,因為d3是const Date類 權限放大了。 仍然獲得了d3的地址是因為調用了編譯器默認生成的const取地址操作符重載函數000000A4BA3AF568// const取地址操作符重載函數我們也可以自己實現cout << &d4 << endl; // 這里調用的就是我們自己實現的//const取地址操作符重載//00000008B33CF5B8return 0;
}
這兩個運算符一般不需要重載,使用編譯器生成的默認取地址的重載即可,只有特殊情況,才需要重載,比如想讓別人獲取到指定的內容!