類與對象(中)(詳解)

【本節目標】

1. 類的6個默認成員函數

2. 構造函數

3. 析構函數

4. 拷貝構造函數

5. 賦值運算符重載

6. const成員函數

7. 取地址及const取地址操作符重載

1.類的6個默認成員函數?

如果一個類中什么成員都沒有,簡稱為空類。

空類中真的什么都沒有嗎?并不是,任何類在什么都不寫時,編譯器會自動生成以下6個默認成員 函數。

默認成員函數:用戶沒有顯式實現,編譯器會生成的成員函數稱為默認成員函數。?

class Date {};

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(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d1;d1.Init(2022, 7, 5);d1.Print();Date d2;d2.Init(2022, 7, 6);d2.Print();return 0;
}

對于Date類,可以通過 Init 公有方法給對象設置日期,但如果每次創建對象時都調用該方法設置 信息,未免有點麻煩,那能否在對象創建時,就將信息設置進去呢?

構造函數是一個特殊的成員函數,名字與類名相同,創建類類型對象時由編譯器自動調用,以保證 每個數據成員都有 一個合適的初始值,并且在對象整個生命周期內只調用一次。

2.2 特性

構造函數是特殊的成員函數,需要注意的是,構造函數雖然名稱叫構造,但是構造函數的主要任 務并不是開空間創建對象,而是初始化對象。

其特征如下:

1. 函數名與類名相同。

2. 無返回值。

3. 對象實例化時編譯器自動調用對應的構造函數。

4. 構造函數可以重載。

class Date
{
public:// 1.無參構造函數Date(){}// 2.帶參構造函數Date(int year, int month, int day){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};
void TestDate()
{Date d1; // 調用無參構造函數Date d2(2015, 1, 1); // 調用帶參的構造函數// 注意:如果通過無參構造函數創建對象時,對象后面不用跟括號,否則就成了函數聲明// 以下代碼的函數:聲明了d3函數,該函數無參,返回一個日期類型的對象// warning C4930: “Date d3(void)”: 未調用原型函數(是否是有意用變量定義的?)Date d3();
}

5. 如果類中沒有顯式定義構造函數,則C++編譯器會自動生成一個無參的默認構造函數,一旦 用戶顯式定義編譯器將不再生成。

class Date
{
public:/*// 如果用戶顯式定義了構造函數,編譯器將不再生成Date(int year, int month, int day){_year = year;_month = month;_day = day;}*/void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{// 將Date類中構造函數屏蔽后,代碼可以通過編譯,因為編譯器生成了一個無參的默認構造函// 將Date類中構造函數放開,代碼編譯失敗,因為一旦顯式定義任何構造函數,編譯器將不再// 無參構造函數,放開后報錯:error C2512: “Date”: 沒有合適的默認構造函數可用Date d1;return 0;}

放開前:

放開后:

6. 關于編譯器生成的默認成員函數,很多童鞋會有疑惑:不實現構造函數的情況下,編譯器會 生成默認的構造函數。但是看起來默認構造函數又沒什么用?d對象調用了編譯器生成的默 認構造函數,但是d對象_year/_month/_day,依舊是隨機值。也就說在這里編譯器生成的 默認構造函數并沒有什么用??

解答:C++把類型分成內置類型(基本類型)和自定義類型。內置類型就是語言提供的數據類 型,如:int/char...,自定義類型就是我們使用class/struct/union等自己定義的類型,看看 下面的程序,就會發現編譯器生成默認的構造函數會對自定類型成員_t調用的它的默認成員 函數。

#include  <iostream>
using namespace std;
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;int _month;int _day;// 自定義類型Time _t;
};
int main()
{Date d;return 0;
}

注意:C++11 中針對內置類型成員不初始化的缺陷,又打了補丁,即:內置類型成員變量在 類中聲明時可以給默認值。

class Time
{
public:Time(){cout << "Time()" << endl;_hour = 0;_minute = 0;_second = 0;}
private:int _hour;int _minute;int _second;
};
class Date
{
public:void Print(){cout << _year << endl;cout << _month << endl;cout << _day << endl;}
private:// 基本類型(內置類型)int _year = 1970;int _month = 1;int _day = 1;// 自定義類型Time _t;
};
int main()
{Date d;d.Print();return 0;
}

7. 無參的構造函數和全缺省的構造函數都稱為默認構造函數,并且默認構造函數只能有一個。 注意:無參構造函數、全缺省構造函數、我們沒寫編譯器默認生成的構造函數,都可以認為 是默認構造函數。

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;
}

答案是不能,會產生歧義編譯器不知道調用哪個Date。

3.析構函數

3.1 概念

通過前面構造函數的學習,我們知道一個對象是怎么來的,那一個對象又是怎么沒呢的? 析構函數:與構造函數功能相反,析構函數不是完成對對象本身的銷毀,局部對象銷毀工作是由 編譯器完成的。而對象在銷毀時會自動調用析構函數,完成對象中資源的清理工作。

3.2 特性

析構函數是特殊的成員函數,其特征如下:

1. 析構函數名是在類名前加上字符 ~。

2. 無參數無返回值類型。

3. 一個類只能有一個析構函數。若未顯式定義,系統會自動生成默認的析構函數。注意:析構 函數不能重載

4. 對象生命周期結束時,C++編譯系統系統自動調用析構函數。

#include <iostream>
using namespace std;
#include <stdlib.h>
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 3){_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("malloc申請空間失敗!!!");return;}_capacity = capacity;_size = 0;}void Push(DataType data){// CheckCapacity();_array[_size] = data;_size++;}// 其他方法...~Stack(){if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:DataType* _array;int _capacity;int _size;
};
int  main()
{Stack s;s.Push(1);s.Push(2);return 0;
}

這里我們可以看到就像構造函數一樣,析構函數也是自動調用。只不過構造函數是在創建變量時會初始化變量。而析構函數則是在程序結束時會自動調用析構函數。這里再說一下,多個對象時,多個對象調用構造函數和析構函數的先后順序。構造函數是創建一個變量就調用一次,誰先創建就先調用誰,而析構函數的順序是構造函數的順序的相反。全局與局部也是有差別的,包括static。一般來說是局部的先調用完析構函數再到全局,static改變了對象的生存作用域,需要等待程序結束時才會析構釋放對象。

5. 關于編譯器自動生成的析構函數,是否會完成一些事情呢?下面的程序我們會看到,編譯器 生成的默認析構函數,對自定類型成員調用它的析構函數。

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類生成的默認析構函數
// 注意:創建哪個類的對象則調用該類的析構函數,銷毀那個類的對象則調用該類

我們可以嘗試一下,編譯器自己生成的析構函數會不會幫我們釋放堆上面的值。

#include <iostream>
using namespace std;
#include <stdlib.h>
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 3){_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("malloc申請空間失敗!!!");return;}_capacity = capacity;_size = 0;}void Push(DataType data){// CheckCapacity();_array[_size] = data;_size++;}// 其他方法...~Stack(){if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:DataType* _array;int _capacity;int _size;
};
int  main()
{Stack s;s.Push(1);s.Push(2);return 0;
}

?這里我們發現并沒有釋放堆上面的值。

總結一下:

為什么自定義類型不需要寫析構,因為自定義類型會調用它們自己的析構函數。

6. 如果類中沒有申請資源時,析構函數可以不寫,直接使用編譯器生成的默認析構函數,比如 Date類;有資源申請時,一定要寫,否則會造成資源泄漏,比如Stack類。

4. 拷貝構造函數

4.1 概念

在現實生活中,可能存在一個與你一樣的自己,我們稱其為雙胞胎。

那在創建對象時,可否創建一個與已存在對象一某一樣的新對象呢? 拷貝構造函數:只有單個形參,該形參是對本類類型對象的引用(一般常用const修飾),在用已存 在的類類型對象創建新對象時由編譯器自動調用。

4.2 特征

拷貝構造函數也是特殊的成員函數,其特征如下:

1. 拷貝構造函數是構造函數的一個重載形式。

2. 拷貝構造函數的參數只有一個且必須是類類型對象的引用,使用傳值方式編譯器直接報錯, 因為會引發無窮遞歸調用。

#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// Date(const Date& d)   // 正確寫法//Date (const Date d)//錯誤寫法,這樣寫會引發無限遞歸。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;
}

這里會解釋發生無限遞歸的原因。這里我們先創建一個什么都沒有的func1函數,用調試走到func1的位置。

按f11我們就能發現我們進入了拷貝構造函數內部。

那么為什么會發生這種事呢?原來在函數中自定義類型的傳參,每次都會調用一次拷貝函數。那么如果此時拷貝函數也是傳值調用,那就會出現無限調用。只有內置類型的傳參才不會調用拷貝構造函數,而是直接傳參。因此我們這里可以用引用也可以用指針,不過一般推薦用引用。

如果還不理解的朋友可以看下面的圖:

總結一下:

3. 若未顯式定義,編譯器會生成默認的拷貝構造函數。 默認的拷貝構造函數對象按內存存儲按 字節序完成拷貝,這種拷貝叫做淺拷貝,或者值拷貝。

這里注意區分一下拷貝構造函數與上面的構造函數與析構函數在未顯示定義時處理內置類型時有差別,構造函數與析構函數不處理內置類型,拷貝構造函數會用淺拷貝處理內置類型。

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 = 1970;int _month = 1;int _day = 1;// 自定義類型Time _t;
};
int main()
{Date d1;// 用已經存在的d1拷貝構造d2,此處會調用Date類的拷貝構造函數// 但Date類并沒有顯式定義拷貝構造函數,則編譯器會給Date類生成一個默認的拷貝構造函數Date d2(d1);return 0;
}

這里我們看到編譯器自己生成的拷貝構造函數也能完成我們的任務。那這樣是不是就不用自己寫拷貝構造函數了?其實不然,這里我們再用一個棧來舉一個例子。

class Stack
{
public:Stack(int capacity = 4){cout << "Stack()" << endl;_a = (int*)malloc(sizeof(int) * capacity);if (nullptr == _a){perror("malloc申請空間失敗");return;}_capacity = capacity;_top = 0;}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_capacity = _top = 0;}private:int* _a = nullptr;int _top = 0;int _capacity;
};
int main()
{Stack st1;Stack st2(st1);return 0;
}

這里我們可以看到st1的_a數組與st2的_a數組居然指向同一個空間,這時我們才發現淺拷貝的問題很大。首先當st1棧與st2棧中的數組都指向同一塊空間時,如果這時候st1把值壓入棧中,那么st2中也會有st1的值,因為它們兩個共用一個空間了。如果我們此時運行程序程序會崩潰,為什么呢?別忘了當程序結束時它會自動調用析構函數來釋放我們_a數組的空間。這時候st1就會釋放一次_a空間,st2釋放一次_a空間。同一塊空間兩次釋放,那么程序就崩潰了。這時候我們就必須自己實現深拷了。

像這樣

class Stack
{
public:Stack(int capacity = 4){cout << "Stack()" << endl;_a = (int*)malloc(sizeof(int) * capacity);if (nullptr == _a){perror("malloc申請空間失敗");return;}_capacity = capacity;_top = 0;}Stack(const Stack& st){_a = (int*)malloc(sizeof(int) * st._capacity);if (nullptr == _a){perror("malloc申請空間失敗");return;}memcpy(_a, st._a, sizeof(int) * st._top);_top = st._top;_capacity = st._capacity;}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_capacity = _top = 0;}private:int* _a = nullptr;int _top = 0;int _capacity;
};
int main()
{Stack st1;Stack st2(st1);return 0;
}

這就是為什么C++在自定義類型傳值調用的時候,要調用拷貝構造了。

注意:在編譯器生成的默認拷貝構造函數中,內置類型是按照字節方式直接拷貝的,而自定 義類型是調用其拷貝構造函數完成拷貝的。

注意:類中如果沒有涉及資源申請時,拷貝構造函數是否寫都可以;一旦涉及到資源申請 時,則拷貝構造函數是一定要寫的,否則就是淺拷貝。

4. 拷貝構造函數典型調用場景:

使用已存在對象創建新對象

函數參數類型為類類型對象

函數返回值類型為類類型對象

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;
#include <stdbool.h>
// 全局的operator==
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// private:int _year;int _month;int _day;
};
// 這里會發現運算符重載成全局的就需要成員變量是公有的,那么問題來了,封裝性如何保證?
// 這里其實可以用我們后面學習的友元解決,或者干脆重載成成員函數。
bool operator==(const Date& d1, const Date& d2)
{return d1._year == d2._year&& d1._month == d2._month&& d1._day == d2._day;
}
bool operator<(const Date& d1, const Date& d2)
{if (d1._year < d2._year){return true;}else if (d1._year== d2._year && d1._month < d2._month){return true;}else if (d1._year == d2._year && d1._month == d2._month&&d1._day<d2._day){return true;}return false;
}
int main()
{Date d1(2018, 9, 26);Date d2(2018, 9, 27);d1 < d2;//會轉換成operator<(d1,d2);cout << (d1 == d2) << endl;cout << (d1 < d2) << endl;return 0;
}

當operator<變成成員函數時:

class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}bool operator<(const Date& x){if (_year < x._year){return true;}else if (_year == x._year && _month < x._month){return true;}else if (_year == x._year && _month == x._month && _day < x._day){return true;}return false;}bool operator==( const Date& x){return _year == x._year&& _month == x._month&& _day == x._day;}private:int _year;int _month;int _day;
};int main()
{Date d1(2018, 9, 26);Date d2(2018, 9, 27);d1 < d2;//會轉換成d1.operator<(d2);cout << (d1 == d2) << endl;cout << (d1 < d2) << endl;return 0;
}

這時候要注意幾個問題,因為成員函數中會蘊含一個this指針。那么如果像全局那樣傳參就會出現參數過多的問題。(內置類型是沒有運算符重載的) 對于你有意義的你就能重載運算符。

5.2 賦值運算符重載

1. 賦值運算符重載格式 參數類型:const T&,傳遞引用可以提高傳參效率

返回值類型:T&,返回引用可以提高返回的效率,有返回值目的是為了支持連續賦值

檢測是否自己給自己賦值

返回*this :要復合連續賦值的含義

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;}void operator=(const Date& d){if (this != &d){_year = d._year;_month = d._month;_day = d._day;}}
private:int _year;int _month;int _day;
};
int main()
{Date d1(2025,3,22);Date d2;//賦值運算符重載是已經存在的兩個對象之間的復制拷貝d2 = d1;//拷貝構造函數用一個已經存在的對象初始化另一個對象Date d3(d1);return 0;
}

這里注意一下與拷貝構造函數做區分,賦值運算符重載是已經存在的兩個對象之間的復制拷貝。拷貝構造函數用一個已經存在的對象初始化另一個對象。

要滿足連續賦值的條件就需要有返回值,例如:

 Date d4, d5;
d5 = d4 = d1;

d4=d1的返回值為d4,再把返回值賦給d5。如果賦值重載函數沒有返回值那么我們就無法連續賦值。??

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;}Date& operator=(const Date& d){if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;}
private:int _year;int _month;int _day;
};
int main()
{Date d1(2025,3,22);Date d2;//賦值運算符重載是已經存在的兩個對象之間的復制拷貝d2 = d1;//拷貝構造函數用一個已經存在的對象初始化另一個對象Date d3(d1);Date d4, d5;d5 = d4 = d1;return 0;
}

因為出了作用域this指針指向的值不會銷毀。所以這里可以用引用返回,這樣就可以減少拷貝,有拷貝函數的時候還能減少拷貝函數的調用次數。

2. 賦值運算符只能重載成類的成員函數不能重載成全局函數

class Date
{
public:Date(int year = 1900, 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 =”必須是非靜態成員}

原因:賦值運算符如果不顯式實現,編譯器會生成一個默認的。此時用戶再在類外自己實現 一個全局的賦值運算符重載,就和編譯器在類中生成的默認賦值運算符重載沖突了,故賦值 運算符重載只能是類的成員函數。

3. 用戶沒有顯式實現時,編譯器會生成一個默認賦值運算符重載,以值的方式逐字節拷貝。注 意:內置類型成員變量是直接賦值的,而自定義類型成員變量需要調用對應類的賦值運算符 重載完成賦值。

class Time
{
public:Time(){_hour = 1;_minute = 1;_second = 1;}Time& operator=(const Time& t){if (this != &t){_hour = t._hour;_minute = t._minute;_second = t._second;}return *this;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本類型(內置類型)int _year = 1970;int _month = 1;int _day = 1;// 自定義類型Time _t;
};
int main()
{Date d1;Date d2;d1 = d2;return 0;
}

賦值重載函數與拷貝構造函數的行為是一樣的:
1.內置類型值拷貝/淺拷貝

2.自定義類型調用它的賦值重載函數。

既然編譯器生成的默認賦值運算符重載函數已經可以完成字節序的值拷貝了,還需要自己實 現嗎?當然像日期類這樣的類是沒必要的。那么下面的類呢?驗證一下試試?

// 這里會發現下面的程序會崩潰掉?這里就需要我們以后講的深拷貝去解決。
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 10){_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申請空間失敗");return;}_size = 0;_capacity = capacity;}void Push(const DataType& data){// CheckCapacity();_array[_size] = data;_size++;}~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}
private:DataType* _array;size_t _size;size_t _capacity;
};
int main()
{Stack s1;s1.Push(1);s1.Push(2);s1.Push(3);s1.Push(4);Stack s2;s2 = s1;return 0;
}

注意:如果類中未涉及到資源管理,賦值運算符是否實現都可以;一旦涉及到資源管理則必 須要實現。

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;return 0;
}

6.日期類的實現

#include <iostream>
using namespace std;
class Date
{
public:// 獲取某年某月的天數int GetMonthDay(int year, int month){static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30,31 };int day = days[month];if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))){day += 1;}return day;}// 全缺省的構造函數Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// 拷貝構造函數// d2(d1)Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}// 賦值運算符重載// d2 = d3 -> d2.operator=(&d2, d3)Date& operator=(const Date& d){if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;}// 析構函數~Date(){_year = 0;_month = 0;_day = 0;}void print(){cout << _year <<"-" << _month <<"-" << _day << endl;}// 日期+=天數Date& operator+=(int day){if (day < 0){return *this -= -day;}_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);++_month;if (_month == 13){_month = 1;++_year;}}return *this;}// 日期+天數Date operator+(int day){Date tmp(*this);tmp += day;return tmp;}// 日期-=天數Date& operator-=(int day){if (day < 0){return *this += -day;}_day -= day;while (_day<1){--_month;if (_month == 0){--_year;_month = 12;}_day+= GetMonthDay(_year, _month);}return *this;}// 日期-天數Date operator-(int day){Date tmp(*this);tmp -= day;return tmp;}// 前置++Date& operator++(){*this = *this + 1;return *this;}// 后置++Date operator++(int){Date tmp (*this);*this = *this + 1;return tmp;}// 后置--Date operator--(int){Date tmp(*this);*this = *this - 1;return tmp;}// 前置--Date& operator--(){*this = *this - 1;return *this;}// >運算符重載bool 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;}return false;}// ==運算符重載bool operator==(const Date& d){return _year == d._year && _month == d._month && _day == d._day;}// >=運算符重載bool operator >= (const Date& d){return ((*this)>d)||((*this)==d);}// <運算符重載bool operator < (const Date& d){return !((*this)>=d);}// <=運算符重載bool operator <= (const Date& d){return !((*this)>d);}// !=運算符重載bool operator != (const Date& d){return !(*this==d);}// 日期-日期  返回天數int operator-(const Date& d){int flag = 1;Date more = *this;Date less = d;if (*this < d){more = d;less = *this;flag = -1;}int day = 0;while (less!=more){++less;++day;}return day*flag;}
private:int _year;int _month;int _day;
};
int main()
{Date d1(2025,5,22);Date d2(2000, 4, 5);Date ret= d1 -10 ;ret.print();int ret1 = d1 - d2;cout << ret1 << endl;return 0;
}
//自定義類型的流插入實現
void operator<< (ostream& out)
{}

在日期類我們還能實現自定義類型的流提取,想實現自定義類型的流提取,我們就需要了解內置類型的流提取。

可以看到為什么內置類型的流提取能自動識別,第一運用了運算符重載流符號,第二還用了函數重載,來重載運算符重載。這樣流提取的神秘面紗也就被我們揭露了。

那么根據上圖我們就能把自定義類型的流提取給實現了。(這里是用日期類的對象實現)

	void operator<< (ostream& out){out << _year << "年" << _month << "月" << _day << "日"<<endl;}
注:這里的函數是成員函數。
void test2()
{Date d1(2025, 5, 24);cout << d1;
}

結果發現編譯器報了一個這樣的錯誤:

?這是為什么呢?

首先流提取符號是一個二元符號,而在成員函數中,第一個參數是左操作數,第二個參數是右操作數。也就是Date對象默認做了左操作數,像這樣:void operator<< (Date *this,ostream& out);因此我們需要反過來寫。像這樣:

void test2()
{Date d1(2025, 5, 24);//cout << d1;d1 << cout;
}

但是這樣就看起來十分奇怪了,那么有什么辦法即使是cout<<d1這樣寫也能編譯通過呢?

首先我們的思路一定是想讓cout變成第一個參數,例如這樣:

void operator<< (ostream& out,Date *this);

這時候我們就能把函數寫到全局。

//自定義類型的流插入實現
void operator<< (ostream& out,const Date& d)
{out << d._year << "年" << d._month << "月" << d._day << "日"<<endl;
}

?但是這樣就有新問題,當我們是成員函數的時候我們能訪問私有,但是當我們是全局函數時我們就不能訪問私有了。

我們有兩個方法,一是寫一個公有成員函數來獲取年月日。例如:
?

	int getyear(){return _year;}int getmonth(){return _month;}int getday(){return _day;}
void operator<< (ostream& out, Date& d)
{out << d.getyear() << "年" << d.getmonth() << "月" << d.getday() << "日"<<endl;
}

?這時候就不能加const了

第二就是變成友元函數(后面會詳細講解)

friend void operator<< (ostream& out, const Date& d);
void operator<< (ostream& out, const Date& d)
{out << d._year << "年" << d._month << "月" << d._day << "日"<<endl;
}
void test2()
{Date d1(2025, 5, 24);//cout << d1;cout<<d1;
}

?

這里有人可能會想為什么?ostream& out不加const,因為流插入會把變量流入終端,因此肯定會改變。所以不能用const修飾。

那么連續的流插入是如何實現的呢?(這里與賦值運算符重載有異曲同工之妙)

void test3()
{Date d1(2025, 5, 24);Date d2(2023, 4, 5);Date d3(2022, 5, 5);cout << d1 << d2 << d3;
}

?注:賦值運算符重載是從右往左賦值,流插入是從左往右流入。為什么會有這個差異呢,因為賦值的返回值是賦值符號的左操作數,而流插入是cout。

這里就需要改一下返回值了

ostream& operator<< (ostream& out, const Date& d)
{out << d._year << "年" << d._month << "月" << d._day << "日"<<endl;return out;
}

注:友元函數那邊也需要更改。

?當然有流插入就有流提取:

friend istream& operator>> (istream& in,  Date& d);
istream& operator>> (istream& in,  Date& d)
{in >> d._year >> d._month >> d._day ;return in;
}
void test4()
{Date d1(2025, 5, 24);Date d2(2023, 4, 5);Date d3(2022, 5, 5);cout << d1 << d2 << d3;cin >> d1 >> d2 >> d3;cout << d1 << d2 << d3;
}

?

這里注意一下Date& d前面不能有const了,因為流提取會改變d。當然istream& in前面也不能加const因為流提取會改變cin的狀態值。

這里我們的日期類還存在些許問題,例如:
?

void test5()
{Date d1(2025, 13, 0);cout << d1;
}

這里我們就需要在構造函數中檢查一下,

Date(int year = 1900, int month = 1, int day = 1)
{if (_month > 0 && _month < 13 && _day>0 && _day <= GetMonthDay(_year, _month)){_year = year;_month = month;_day = day;}else{cout << "非法日期" << endl;assert(false);}}

再把cin也改一下

istream& operator>> (istream& in,  Date& d)
{int year, month, day;in >> year >> month >> day;if (month > 0 && month < 13 && day>0 && day <= d.GetMonthDay(year, month)){d._year = year;d._month = month;d._day = day;}else{cout << "非法日期" << endl;assert(false);}return in;
}


?

7.const成員?

將const修飾的“成員函數”稱之為const成員函數,const修飾類成員函數,實際修飾該成員函數 隱含的this指針,表明在該成員函數中不能對類的任何成員進行修改。

這時我們發現了一個問題d1調用Print的時候不出問題但是,d2又無法調用Printf?

void test7()
{Date d1(2023, 5, 5);d1.print();const Date d2(2023, 5, 5);d2.print();
}

?

?

這時候我們需要在Date* this前面加上const。

權限不能放大但是能縮小。這時候我們發現了this指針是不顯示的那么該怎么辦呢?C++可以在成員函數后面加上const來修飾this指針?。

我們來看看下面的代碼

class Date
{
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << "Print()" << endl;cout << "year:" << _year << endl;cout << "month:" << _month << endl;cout << "day:" << _day << endl << endl;}void Print() const{cout << "Print()const" << endl;cout << "year:" << _year << endl;cout << "month:" << _month << endl;cout << "day:" << _day << endl << endl;}
private:int _year; // 年int _month; // 月int _day; // 日
};
void Test()
{Date d1(2022, 1, 13);d1.Print();const Date d2(2022, 1, 13);d2.Print();
}

請思考下面的幾個問題:

1. const對象可以調用非const成員函數嗎?

2. 非const對象可以調用const成員函數嗎?

3. const成員函數內可以調用其它的非const成員函數嗎?

4. 非const成員函數內可以調用其它的const成員函數嗎?

8.取地址及const取地址操作符重載

這兩個默認成員函數一般不用重新定義 ,編譯器默認會生成。

class Date
{
public:Date* operator&(){return this;}const Date* operator&()const{return this;}
private:int _year; // 年int _month; // 月int _day; // 日
};

這兩個運算符一般不需要重載,使用編譯器生成的默認取地址的重載即可,只有特殊情況,才需 要重載,比如想讓別人獲取到指定的內容!

日期類最終代碼:

#include <iostream>
#include <assert.h>
using namespace std;
class Date
{friend ostream& operator<< (ostream& out, const Date& d);friend istream& operator>> (istream& in,  Date& d);
public:// 獲取某年某月的天數int GetMonthDay(int year, int month){static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30,31 };int day = days[month];if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))){day += 1;}return day;}// 全缺省的構造函數Date(int year = 1900, int month = 1, int day = 1){if (month > 0 && month < 13 && day>0 && day <= GetMonthDay(year, month)){_year = year;_month = month;_day = day;}else{cout << "非法日期" << endl;assert(false);}}// 拷貝構造函數// d2(d1)Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}// 賦值運算符重載// d2 = d3 -> d2.operator=(&d2, d3)Date& operator=(const Date& d) {if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;}// 析構函數~Date(){_year = 0;_month = 0;_day = 0;}void print() const{cout << _year <<"-" << _month <<"-" << _day << endl;}// 日期+=天數Date& operator+=(int day){if (day < 0){return *this -= -day;}_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);++_month;if (_month == 13){_month = 1;++_year;}}return *this;}// 日期+天數Date operator+(int day) const{Date tmp(*this);tmp += day;return tmp;}// 日期-=天數Date& operator-=(int day){if (day < 0){return *this += -day;}_day -= day;while (_day<1){--_month;if (_month == 0){--_year;_month = 12;}_day+= GetMonthDay(_year, _month);}return *this;}// 日期-天數Date operator-(int day) {Date tmp(*this);tmp -= day;return tmp;}// 前置++Date& operator++(){*this = *this + 1;return *this;}// 后置++Date operator++(int){Date tmp (*this);*this = *this + 1;return tmp;}// 后置--Date operator--(int){Date tmp(*this);*this = *this - 1;return tmp;}// 前置--Date& operator--(){*this = *this - 1;return *this;}// >運算符重載bool operator>(const Date& d) const{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;}return false;}// ==運算符重載bool operator==(const Date& d)const{return _year == d._year && _month == d._month && _day == d._day;}// >=運算符重載bool operator >= (const Date& d)const{return ((*this)>d)||((*this)==d);}// <運算符重載bool operator < (const Date& d)const{return !((*this)>=d);}// <=運算符重載bool operator <= (const Date& d)const{return !((*this)>d);}// !=運算符重載bool operator != (const Date& d)const{return !(*this==d);}// 日期-日期  返回天數int operator-(const Date& d){int flag = 1;Date more = *this;Date less = d;if (*this < d){more = d;less = *this;flag = -1;}int day = 0;while (less!=more){++less;++day;}return day*flag;}//int getyear()//{//	return _year;//}//int getmonth()//{//	return _month;//}//int getday()//{//	return _day;//}
private:int _year;int _month;int _day;
};
//自定義類型的流插入實現
//void operator<< (ostream& out, Date& d)
//{
//	out << d.getyear() << "年" << d.getmonth() << "月" << d.getday() << "日";
//}
ostream& operator<< (ostream& out, const Date& d)
{out << d._year << "年" << d._month << "月" << d._day << "日"<<endl;return out;
}
istream& operator>> (istream& in,  Date& d)
{int year, month, day;in >> year >> month >> day;if (month > 0 && month < 13 && day>0 && day <= d.GetMonthDay(year, month)){d._year = year;d._month = month;d._day = day;}else{cout << "非法日期" << endl;assert(false);}return in;
}
void test1()
{Date d1(2025, 5, 22);Date d2(2000, 4, 5);Date ret = d1 - 10;ret.print();int ret1 = d1 - d2;cout << ret1 << endl;
}
void test2()
{Date d1(2025, 5, 24);//cout << d1;cout<<d1;
}
void test3()
{Date d1(2025, 5, 24);Date d2(2023, 4, 5);Date d3(2022, 5, 5);cout << d1 << d2 << d3;
}
void test4()
{Date d1(2025, 5, 24);Date d2(2023, 4, 5);Date d3(2022, 5, 5);cout << d1 << d2 << d3;cin >> d1 >> d2 >> d3;cout << d1 << d2 << d3;
}
void test5()
{Date d1(2025, 13, 0);cout << d1;
}
void test6()
{Date d1(2025, 12, 1);cin>> d1;cout << d1;
}
void test7()
{Date d1(2023, 5, 5);d1.print();const Date d2(2023, 5, 5);d2.print();
}
int main()
{test7();return 0;
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/73311.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/73311.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/73311.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

開發語言漫談-groovy

groovy是一門腳本語言&#xff0c;在前期的腳本語言中簡單介紹了下。現在再深入介紹下&#xff0c;因為它是本平臺上選用的腳本語言。所謂腳本語言就是不用編譯&#xff0c;直接執行。這種特色非常適合做嵌入編程&#xff0c;即編即用。我們知道平臺后臺的業務開發語言是Java&a…

React+Ant Design的Layout布局實現暗黑模式切換

目錄 效果預覽完整代碼我遇到的BUG問題代碼BUG1&#xff1a;暗黑模式下內容區不變成深色BUG2&#xff1a;光亮模式下的左右區域是深色 補充知識ConfigProvider是什么&#xff1f;Ant Design中的theme如何使用&#xff1f;theme 配置的常見字段主題算法通過 useToken 獲取主題 效…

TCP 三次握手與四次揮手過程

TCP 作為一種面向連接的、可靠的傳輸層協議&#xff0c;其連接管理機制對于保障數據的可靠傳輸至關重要。 三次握手&#xff08;建立連接&#xff09; 三次握手是 TCP 建立連接時所采用的機制&#xff0c;其目的在于確保客戶端和服務器雙方都具備發送和接收數據的能力&#x…

【線程安全的單例模式和STL是否是線程安全/智能指針是否是線程安全】

文章目錄 一、單例模式的特點二、餓漢模式實現單例三、懶漢模式實現單例四、STL線程安全嗎&#xff1f;五、智能指針線程安全嗎&#xff1f; 一、單例模式的特點 一個類&#xff0c;只應該實例化了一個對象&#xff0c;就是單例。 二、餓漢模式實現單例 舉個餓漢模式的例子&…

力扣DAY24 | 熱100 | 回文鏈表

前言 簡單 √ 是反轉鏈表的衍生題&#xff0c;很快寫完了。不過沒考慮到恢復鏈表結構的問題。 題目 給你一個單鏈表的頭節點 head &#xff0c;請你判斷該鏈表是否為回文鏈表。如果是&#xff0c;返回 true &#xff1b;否則&#xff0c;返回 false 。 示例 1&#xff1a; 輸…

【GL010】C++

1.C中的const關鍵字有哪些用法&#xff1f; 1.修飾變量&#xff1a;表示變量的值不可修改。 const int a 10; 2.修飾指針&#xff1a; const int* p&#xff1a; // 指針指向的內容不可修改。 int* const p&#xff1a; // 指針本身不可修改。 const int* const…

金融行業 UE/UI 設計:解鎖高效體驗,重塑行業界面

在數字化浪潮中&#xff0c;金融行業的競爭日益激烈&#xff0c;用戶體驗&#xff08;UE&#xff09;和用戶界面&#xff08;UI&#xff09;設計成為企業脫穎而出的關鍵。蘭亭妙微憑借豐富的經驗和創新的方法&#xff0c;為金融行業打造了一套行之有效的 UE/UI 解決方案&#x…

C語言字符函數,字符串函數以及內存函數

那么博主寫這一片博客的目的就是為下一篇c的string類做鋪墊&#xff0c;那么下面就請期待博主的下一篇文章吧。 目錄 1.字符函數 2.字符串函數&#xff08;均在string.h頭文件中&#xff09; strlen的使用和模擬實現 strcpy 的使用和模擬實現 strcat 的使用和模擬實現 s…

_DISPATCHER_HEADER結構中的WaitListHead和_KWAIT_BLOCK的關系

第一部分&#xff1a; // // Wait block // // begin_ntddk begin_wdm begin_nthal begin_ntifs begin_ntosp typedef struct _KWAIT_BLOCK { LIST_ENTRY WaitListEntry; struct _KTHREAD *RESTRICTED_POINTER Thread; PVOID Object; struct _KWAIT_BLOCK *R…

flutter 自定義控件RenderObjectWidget使用

CustomWidget的自定義組件的注釋還是比較清晰的 參考文檔Flutter實戰 import package:flutter/cupertino.dart; import package:flutter/gestures.dart; import package:flutter/material.dart; /* * 如果組件不會包含子組件&#xff0c;則我們可以直接繼承自 LeafRenderObject…

機器視覺場景應用中,有沒有超景深的工業鏡頭

在機器視覺領域,確實存在具有超景深特性的工業鏡頭,這類鏡頭通過特殊的光學設計或技術手段,能夠顯著擴大清晰成像的縱向范圍,從而滿足復雜檢測場景中對多平面物體清晰成像的需求。以下是相關技術要點及典型鏡頭類型: 1. 遠心鏡頭 遠心鏡頭是超景深鏡頭的典型代表,其特點包…

【Linux】同步原理剖析及模擬BlockQueue生產消費模型

&#x1f4e2;博客主頁&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;博客倉庫&#xff1a;https://gitee.com/JohnKingW/linux_test/tree/master/lesson &#x1f4e2;歡迎點贊 &#x1f44d; 收藏 ?留言 &#x1f4dd; 如有錯誤敬請指正&#xff01; &…

光流 | 基于KLT算法的人臉檢測與跟蹤原理及公式,算法改進,matlab代碼

===================================================== github:https://github.com/MichaelBeechan CSDN:https://blog.csdn.net/u011344545 ===================================================== 人臉檢測與跟蹤 一、KLT算法原理與分析1. 核心思想2. 數學模型二、人臉…

<數據集>軌道異物識別數據集<目標檢測>

數據集下載鏈接&#xff1a;https://download.csdn.net/download/qq_53332949/90527370 數據集格式&#xff1a;VOCYOLO格式 圖片數量&#xff1a;1659張 標注數量(xml文件個數)&#xff1a;1659 標注數量(txt文件個數)&#xff1a;1659 標注類別數&#xff1a;6 標注類別…

LabVIEW液壓振動錘控制系統

在現代工程機械領域&#xff0c;液壓振動錘的高效與精準控制日益顯得重要。本文通過LabVIEW軟件&#xff0c;展開液壓振動錘啟停共振控制技術的研究與應用&#xff0c;探討如何通過改進控制系統來優化液壓振動錘的工作性能&#xff0c;確保其在復雜工況下的穩定性與效率。 ? …

【開源寶藏】30天學會CSS - DAY7 第七課 CSS 關鍵幀打造Preloader 追逐動畫

你的代碼實現了一個 方形軌跡預加載動畫&#xff08;Preloader Animation&#xff09;&#xff0c;其中三個 span 元素沿著一個 22 網格 軌跡循環移動。現在&#xff0c;我們將 拆解核心實現步驟&#xff0c;讓你能一步步理解并調整動畫效果。 第 0 步&#xff1a;項目概覽 你…

在shell腳本內部獲取該腳本所在目錄的絕對路徑

目錄 需求描述 方法一&#xff1a;使用 dirname 和 readlink 命令 方法二&#xff1a;使用 BASH_SOURCE 變量 方法三&#xff1a;僅使用純 Bash 實現 需求描述 工作中經常有這樣情況&#xff0c;需要在腳本內部獲取該腳本自己所在目錄的絕對路徑。 假如有一個腳本/a/b/c/…

常考計算機操作系統面試習題(一下)

目錄 操作系統基本類型 操作系統的功能 操作系統的主要任務 進程與線程 進程狀態轉變 內存管理 文件系統與文件管理 虛擬存儲器 設備管理 磁盤調度 死鎖 信號量機制 文件打開與管理 進程與線程的互斥與同步 進程同步 進程調度 文件分配磁盤塊的方法 程序執行…

GPT-SoVITS本地部署:低成本實現語音克隆遠程生成音頻全流程實戰

文章目錄 前言1.GPT-SoVITS V2下載2.本地運行GPT-SoVITS V23.簡單使用演示4.安裝內網穿透工具4.1 創建遠程連接公網地址 5. 固定遠程訪問公網地址 前言 今天要給大家安利一個絕對能讓你大呼過癮的聲音黑科技——GPT-SoVITS&#xff01;這款由花兒不哭大佬精心打造的語音克隆神…

JVM(基礎篇)

一.初識JVM 1.什么是JVM JVM全稱Java Virtyal Machine&#xff0c;中文譯名 Java虛擬機 。JVM本質上是一個運行在計算機上的程序&#xff0c;他的職責是運行Java字節碼文件(將字節碼解釋成機器碼)。 2.JVM的功能 解釋和運行&#xff1a;對字節碼文件中的指令號&#xff0c;實時…