文章目錄
- 一、類的默認成員函數
- 二、構造函數
- 三、析構函數
- 四、拷?構造函數
- 五、賦值運算符重載
- 1. 運算符重載
- 2. 賦值運算符重載
- 六、取地址運算符重載
- const成員函數
- 取地址運算符重載
- 七、應用:?期類實現
- Date.h
- Date.cpp
- test.cpp
一、類的默認成員函數
默認成員函數就是??沒有顯式實現,編譯器會?動?成的成員函數稱為默認成員函數。
?個類,我們不寫的情況下編譯器會默認?成以下6個默認成員函數,需要注意的是這6個中最重要的是前4個,最后兩個取地址重載不重要,只需要了解
①初始化功能的構造函數
②清理功能的析構函數
③使用同類對象初始化創建對象的拷貝構造
④把一個對象賦值給另一個對象的賦值重載
⑤對普通對象取地址重載
⑥對const對象取地址重載
默認成員函數很重要,也?較復雜,我們要從兩個??去學習:
? 第?:我們不寫時,編譯器默認?成的函數?為是什么,是否滿?我們的需求。
? 第?:編譯器默認?成的函數不滿?我們的需求,我們需要??實現,那么如何??實現?
二、構造函數
構造函數是特殊的成員函數,需要注意的是,構造函數雖然名稱叫構造, 但是構造函數的主要任務并不是開空間創建對象(我們常使?的局部對象是棧幀創建時,空間就開好了),?是對象實例化時初始化對象。
構造函數的特點:
- 函數名與類名相同。
- ?返回值。(返回值啥都不需要給,也不需要寫void,不要糾結,C++規定如此)
- 對象實例化時系統會?動調?對應的構造函數。
- 構造函數可以重載。
- 如果類中沒有顯式定義構造函數,則C++編譯器會?動?成?個?參的默認構造函數,?旦??顯式定義編譯器將不再?成。
- ?參構造函數、全缺省構造函數、我們不寫構造時編譯器默認?成的構造函數,都叫做默認構造函數。但是這三個函數有且只有?個存在,不能同時存在。 ?參構造函數和全缺省構造函數雖然構成函數重載,但是調?時會存在歧義。要注意默認構造函數并非是編譯器默認?成那個叫默認構造,實際上?參構造函數、全缺省構造函數也是默認構造,總結?下就是不傳實參就可以調?的構造就叫默認構造。
- 我們不寫,編譯器默認?成的構造,對內置類型成員變量的初始化沒有要求,也就是說是是否初始化是不確定的,看編譯器。對于?定義類型成員變量,要求調?這個成員變量的默認構造函數初始化。如果這個成員變量,沒有默認構造函數,那么就會報錯,我們要初始化這個成員變量,需要?初始化列表才能解決,后面再提到初始化列表。
說明:C++把類型分成內置類型(基本類型)和?定義類型。內置類型就是語?提供的原?數據類型。
如:int/char/double/指針等,?定義類型就是我們使?class/struct等關鍵字??定義的類型。
對于內置類型:
#include <iostream>
using namespace std;class Date
{
public://無參構造函數Date(){_year = 2000;_month = 6;_day = 1;}//帶參構造函數Date(int year, int month, int day){_year = year;_month = month;_day = day;}//全缺省構造函數//與無參構造函數矛盾,這兩個只能用一個/*Date(int year = 2024, int month = 7, int day = 1){_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.print();Date d2(2024, 7, 14);d2.print();return 0;
}
對于自定義類型:
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申請空間失敗");return;} _capacity = n;_top = 0;}
private:STDataType * _a;size_t _capacity;size_t _top;
};
// 兩個Stack實現隊列
class MyQueue
{
public://編譯器默認生成MyQueue的構造函數調用了Stack的構造,完成了兩個成員的初始化
private:Stack pushst;Stack popst;
};int main()
{MyQueue mq;return 0;
}
三、析構函數
析構函數與構造函數功能相反,析構函數不是完成對對象本?的銷毀,?如局部對象是存在棧幀的,函數結束棧幀銷毀,他就釋放了,不需要我們管,C++規定對象在銷毀時會?動調?析構函數,完成對象中資源的清理釋放?作。
析構函數的功能類?我們之前Stack實現的Destroy功能,?像Date沒有Destroy,其實就是沒有資源需要釋放,所以嚴格說Date是不需要析構函數的。
析構函數的特點:
- 析構函數名是在類名前加上字符~。
- ?參數?返回值。(這?跟構造類似,也不需要加void)
- ?個類只能有?個析構函數。若未顯式定義,系統會?動?成默認的析構函數。
- 對象?命周期結束時,系統會?動調?析構函數。
- 跟構造函數類似,我們不寫編譯器?動?成的析構函數對內置類型成員不做處理,?定類型成員會調?他的析構函數。
- 還需要注意的是我們顯?寫析構函數,對于?定義類型成員也會調?他的析構,也就是說?定義類型成員?論什么情況都會?動調?析構函數。
- 如果類中沒有申請資源時,析構函數可以不寫,直接使?編譯器?成的默認析構函數,如Date;如果默認?成的析構就可以?,也就不需要顯?寫析構,如MyQueue;但是有資源申請時,?定要??寫析構,否則會造成資源泄漏,如Stack。
- ?個局部域的多個對象,C++規定后定義的先析構。
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申請空間失敗");return;}_capacity = n;_top = 0;}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}
private:STDataType* _a;size_t _capacity;size_t _top;
};
// 兩個Stack實現隊列
class MyQueue
{
public://編譯器默認生成MyQueue的析構函數調用了Stack的析構,釋放的Stack內部的資源// 顯示寫析構,也會自動調用Stack的析構~MyQueue(){cout << "~MyQueue()" << endl;}
private:Stack pushst;Stack popst;
};int main()
{MyQueue mq;return 0;
}
四、拷?構造函數
如果?個構造函數的第?個參數是??類類型的引?,且任何額外的參數都有默認值,則此構造函數也叫做拷?構造函數,也就是說拷?構造是?個特殊的構造函數。
拷?構造的特點:
- 拷?構造函數是構造函數的?個重載。
- 拷?構造函數的第一個參數必須是類類型對象的引?,使?傳值?式編譯器直接報錯,因為語法邏輯上會引發?窮遞歸調?。
- C++規定?定義類型對象進?拷??為必須調?拷?構造,所以這??定義類型傳值傳參和傳值返回都會調?拷?構造完成。
- 若未顯式定義拷?構造,編譯器會?成?動?成拷?構造函數。?動?成的拷?構造對內置類型成員變量會完成值拷?/淺拷?(?個字節?個字節的拷?),對?定義類型成員變量會調?他的拷?構造。
- 像Date這樣的類成員變量全是內置類型且沒有指向什么資源,編譯器?動?成的拷?構造就可以完成需要的拷?,所以不需要我們顯?實現拷?構造。像Stack這樣的類,雖然也都是內置類型,但是_a指向了資源,編譯器?動?成的拷?構造完成的值拷?/淺拷?不符合我們的需求,所以需要
我們??實現深拷?(對指向的資源也進?拷?)。像MyQueue這樣的類型內部主要是?定義類型
Stack成員,編譯器?動?成的拷?構造會調?Stack的拷?構造,也不需要我們顯?實現 MyQueue的拷?構造。這?還有?個?技巧,如果?個類顯?實現了析構并釋放資源,那么他就 需要顯?寫拷?構造,否則就不需要。- 傳值返回會產??個臨時對象調?拷?構造,傳值引?返回,返回的是返回對象的別名(引?),沒 有產?拷?。但是如果返回對象是?個當前函數局部域的局部對象,函數結束就銷毀了,那么使? 引?返回是有問題的,這時的引?相當于?個野引?,類似?個野指針?樣。傳引?返回可以減少拷?,但是?定要確保返回對象,在當前函數結束后還在,才能?引?返回。
#include <iostream>
using namespace std;class Date
{
public://無參構造函數Date(){_year = 2000;_month = 6;_day = 1;}//帶參構造函數Date(int year, int month, int day){_year = year;_month = month;_day = day;}//拷貝構造函數 - 傳值//error C2652: “Date”: 非法的復制構造函數: 第一個參數不應是“Date”/*Date(Date d){}*///拷貝構造函數 - 傳引用Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}//這個不是拷貝構造函數,只是一個構造函數Date(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;
};Date& run()
{Date tmp(2024, 8, 1);return tmp;
}int main()
{Date d1(2024, 7, 14);//這里可以完成拷?,但是不是拷?構造,只是?個普通的構造Date dk(&d1);dk.print();//這樣寫才是拷?構造,通過同類型的對象初始化構造,而不是指針Date d2(d1);d2.print();//也可以這樣寫,這里也是拷?構造Date d3 = d1;d3.print();// run返回了一個局部對象tmp的引用作為返回值// run函數結束,tmp對象就銷毀了,相當于了一個野引用Date ret = run();ret.print();return 0;
}
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申請空間失敗");return;}_capacity = n;_top = 0;}void Push(STDataType x){if (_top == _capacity){int newcapacity = _capacity * 2;STDataType* tmp = (STDataType*)realloc(_a, newcapacity *sizeof(STDataType));if (tmp == NULL){perror("realloc fail");return;} _a = tmp;_capacity = newcapacity;} _a[_top++] = x;}~Stack(){free(_a);_a = nullptr;_capacity = 0;_top = 0;cout << "~Stack()" << endl;}Stack(const Stack& st){cout << "Stack(const Stack& st)" << endl;// 需要對_a指向資源創建同樣大的資源再拷貝值_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);if (nullptr == _a){perror("malloc申請空間失敗!!!");return;}memcpy(_a, st._a, sizeof(STDataType) * st._top);_top = st._top;_capacity = st._capacity;}
private:STDataType* _a;size_t _capacity;size_t _top;
};// 兩個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;
}
五、賦值運算符重載
1. 運算符重載
? 當運算符被?于類類型的對象時,C++語?允許我們通過運算符重載的形式指定新的含義。C++規定類類型對象使?運算符時,必須轉換成調?對應運算符重載,若沒有對應的運算符重載,則會編譯報錯。
? 運算符重載是具有特殊名字的函數,他的名字是由operator和后?要定義的運算符共同構成。和其他函數?樣,它也具有其返回類型和參數列表以及函數體。
? 重載運算符函數的參數個數和該運算符作?的運算對象數量?樣多。?元運算符有?個參數,?元運算符有兩個參數,?元運算符的左側運算對象傳給第?個參數,右側運算對象傳給第?個參數。
? 如果?個重載運算符函數是成員函數,則它的第?個運算對象默認傳給隱式的this指針,因此運算符重載作為成員函數時,參數?運算對象少?個。
? 運算符重載以后,其優先級和結合性與對應的內置類型運算符保持?致。
? 不能通過連接語法中沒有的符號來創建新的操作符:?如operator@。
? 注意以下5個運算符不能重載。
? 重載操作符?少有?個類類型參數,不能通過運算符重載改變內置類型對象的含義,如: int operator+(int x, int y)
? ?個類需要重載哪些運算符,是看哪些運算符重載后有意義,?如Date類重載operator-就有意義,但是重載operator+就沒有意義。
? 重載++運算符時,有前置++和后置++,運算符重載函數名都是operator++,?法很好的區分。
C++規定,后置++重載時,增加?個int形參,跟前置++構成函數重載,?便區分。
? 重載<<和>>時,需要重載為全局函數,因為重載為成員函數,this指針默認搶占了第?個形參位置,第?個形參位置是左側運算對象,調?時就變成了對象<<cout,不符合使?習慣和可讀性。
重載為全局函數把ostream/istream放到第?個形參位置就可以了,第?個形參位置當類類型對象。
#include <iostream>
using namespace std;class Date
{
public://無參構造函數Date(){_year = 2000;_month = 6;_day = 1;}//帶參構造函數Date(int year, int month, int day){_year = year;_month = month;_day = day;}bool operator==(Date& d){return _year == d._year && _month == d._month && _day == d._day;}//++dDate& operator++(){cout << "前置++" << endl;//...return *this;}//d++Date operator++(int){cout << "后置++" << endl;Date tmp = *this;//...return tmp;}void print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}
private:int _year;int _month;int _day;
};int main()
{Date d1;d1.print();Date d2(2024, 7, 14);d2.print();//運算符重載函數可以顯示調用d1.operator==(d2);//編譯器會轉換成 d1.operator==(d2);d1 == d2;cout << (d1 == d2) << endl;//編譯器會轉換成 d1.operator++();++d1;//編譯器會轉換成 d2.operator++(0);d2++;return 0;
}
2. 賦值運算符重載
賦值運算符重載是?個默認成員函數,?于完成兩個已經存在的對象直接的拷?賦值,這?要注意跟拷?構造區分,拷?構造?于?個對象拷?初始化給另?個要創建的對象。
賦值運算符重載的特點:
- 賦值運算符重載是?個運算符重載,規定必須重載為成員函數。賦值運算重載的參數建議寫成const當前類類型引?,否則會傳值傳參會有拷?。
- 有返回值,且建議寫成當前類類型引?,引?返回可以提?效率,有返回值?的是為了?持連續賦值場景。
- 沒有顯式實現時,編譯器會?動?成?個默認賦值運算符重載,默認賦值運算符重載?為跟默認拷貝構造函數類似,對內置類型成員變量會完成值拷?/淺拷?(?個字節?個字節的拷?),對?定義類型成員變量會調?他的賦值重載。
- 像Date這樣的類成員變量全是內置類型且沒有指向什么資源,編譯器?動?成的賦值運算符重載就可以完成需要的拷?,所以不需要我們顯?實現賦值運算符重載。像Stack這樣的類,雖然也都是內置類型,但是_a指向了資源,編譯器?動?成的賦值運算符重載完成的值拷?/淺拷?不符合我們的需求,所以需要我們??實現深拷?(對指向的資源也進?拷?)。像MyQueue這樣的類型內部主要是?定義類型Stack成員,編譯器?動?成的賦值運算符重載會調?Stack的賦值運算符重載,也不需要我們顯?實現MyQueue的賦值運算符重載。這?還有?個?技巧,如果?個類顯?實現了析構并釋放資源,那么他就需要顯?寫賦值運算符重載,否則就不需要。
#include <iostream>
using namespace std;class Date
{
public://無參構造函數Date(){_year = 2000;_month = 6;_day = 1;}//帶參構造函數Date(int year, int month, int day){_year = year;_month = month;_day = day;}//拷貝構造Date(Date& d){_year = d._year;_month = d._month;_day = d._day;}//賦值運算符重載Date& operator=(Date& d){// 不要檢查自己給自己賦值的情況if (this != &d){_year = d._year;_month = d._month;_day = d._day;} // d1 = d2表達式的返回對象應該為d1,也就是 * thisreturn *this;}void print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}
private:int _year;int _month;int _day;
};int main()
{// 賦值重載完成兩個已經存在的對象直接的拷?賦值// 拷?構造用于一個對象拷?初始化給另一個要創建的對象Date d1(2024, 7, 14);//拷貝構造Date d2(d1);//拷貝構造Date d3 = d2;Date d4(2025, 5, 5);//賦值重載d3 = d4;d3.print();return 0;
}
六、取地址運算符重載
const成員函數
將const修飾的成員函數稱之為const成員函數,const修飾成員函數放到成員函數參數列表的后?。
const實際修飾該成員函數隱含的this指針,表明在該成員函數中不能對類的任何成員進?修改。
const 修飾Date類的Print成員函數,Print隱含的this指針由 Date* const this 變為 const Date* const this
#include<iostream>
using namespace std;
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_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(2024, 7, 14);d1.Print();const Date d2(2024, 8, 1);d2.Print();return 0;
}
取地址運算符重載
取地址運算符重載分為普通取地址運算符重載和const取地址運算符重載,?般這兩個函數編譯器?動?成的就可以夠我們?了,不需要去顯?實現。
除??些很特殊的場景,?如我們不想讓別?取到當前類對象的地址,就可以??實現?份,胡亂返回?個地址。
#include <iostream>
using namespace std;class Date
{
public :Date(){_year = 2024;_month = 7;_day = 14;}Date(int year, int month, int day){_year = year;_month = month;_day = day;}Date* operator&(){//return this;//return nullptr;return (Date* )0x1256EF7;}const Date* operator&()const{//return this;//return nullptr;return (Date*)0x1256EA9;}
private:int _year;int _month;int _day;
};int main()
{Date d1;cout << &d1 << endl;const Date d2(2024, 8, 1);cout << &d2 << endl;return 0;
}
七、應用:?期類實現
Date.h
#pragma once#include <iostream>
using namespace std;//日期類
class Date
{
public:Date(int year = 2000, int month = 1, int day = 1);//拷貝構造Date(const Date& d);//賦值運算符重載構造Date& operator=(const Date& d);~Date(){_year = 0;_month = 0;_day = 0;}void print() const;bool CheckDate() const;static int GetMonthDay(int year,int month){int arr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };if (month == 2 && ((year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0))){return arr[month] + 1;}return arr[month];}Date operator+(int day) const;Date& operator+=(int day);Date operator-(int day) const;Date& operator-=(int day);bool operator<(const Date& d) const;bool operator<=(const Date& d) const;bool operator>(const Date& d) const;bool operator>=(const Date& d) const;bool operator==(const Date& d) const;bool operator!=(const Date& d) const;//++d 前置++Date& operator++();//d++ 后置++Date operator++(int);//日期相減 d1 - d2int operator-(const Date& d) const;friend ostream& operator<<(ostream& out, const Date& d);friend istream& operator>>(istream& in, Date& d);//取地址運算符重載Date* operator&(){//return this;//return nullptr;return (Date*)0x11451EF6;}const Date* operator&() const{//return this;//return nullptr;return (Date*)0x1145EE6;}private:int _year;int _month;int _day;
};ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
Date.cpp
#include "Date.h"Date::Date(int year, int month, int day)
{_year = year;_month = month;_day = day;if (!CheckDate()){cout << "日期錯誤:";print();}
}//拷貝構造
Date::Date(const Date& d)
{_year = d._year;_month = d._month;_day = d._day;
}//賦值運算符重載構造
Date& Date::operator=(const Date& d)
{_year = d._year;_month = d._month;_day = d._day;return *this;
}void Date::print() const
{cout << _year << " " << _month << " " << _day << endl;
}bool Date::CheckDate() const
{if (_month < 1 || _month > 12 || _day <= 0 || _day > GetMonthDay(_year, _month)){return false;}return true;
}Date Date::operator+(int day) const
{Date tmp = *this;tmp.operator+=(day);return tmp;
}Date& 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 Date::operator-(int day) const
{Date tmp = *this;tmp.operator-=(day);return tmp;
}
Date& Date::operator-=(int day)
{if (day < 0){return *this += (-day);}_day -= day;while (_day <= 0){_month--;if (_month == 0){_month = 12;_year--;}_day += GetMonthDay(_year, _month);}return *this;
}bool Date::operator<(const Date& d) const
{if (_year < d._year){return true;}else if (_year == d._year){if (_month < d._month){return true;}else if (_month == d._month){return _day < d._day;}}return false;
}bool Date::operator<=(const Date& d) const
{return *this < d || *this == d;
}bool Date::operator>(const Date& d) const
{return !(*this <= d);
}bool Date::operator>=(const Date& d) const
{return !(*this < d);
}bool Date::operator==(const Date& d) const
{return _year == d._year && _month == d._month && _day == d._day;
}bool Date::operator!=(const Date& d) const
{return !(*this == d);
}//++d 前置++
Date& Date::operator++()
{return *this += 1;
}//d++ 后置++
Date Date::operator++(int)
{Date tmp = *this;*this += 1;return tmp;
}int Date::operator-(const Date& d) const
{int flag = 1;int n = 0;Date max = *this;Date min = d;if (*this < d){max = d;min = *this;}while (min != max){n++;min++;flag = -1;}return n * flag;
}ostream& operator<<(ostream& out, const Date& d)
{out << d._year << "年" << d._month << "月" << d._day << "日" << endl;return out;
}istream& operator>>(istream& in, Date& d)
{while (1){cout << "輸入年 月 日" << endl;in >> d._year >> d._month >> d._day;if (!d.CheckDate()){cout << "日期錯誤:";d.print();}else{break;}}return in;
}
test.cpp
#include "Date.h"void test01()
{Date d1;Date d2(2024, 7, 12);d1.print();d2.print();d2 += 30000;d2.print();Date d3 = d1 + 10000;d3.print();
}void test02()
{Date d1;Date d2(2024, 7, 13);d1.print();d2.print();d2 -= 30000;d2.print();//d2 = d1 - 100;//d2.print();
}void test03()
{Date d1(2024, 7, 13);Date d2(2024, 7, 19);cout << (d1 < d2) << endl;cout << (d1 <= d2) << endl;cout << (d1 > d2) << endl;cout << (d1 >= d2) << endl;cout << (d1 == d2) << endl;cout << (d1 != d2) << endl;
}void test04()
{Date d1(2024, 7, 13);Date d2(2024, 7, 13);Date tmp1 = d1++;tmp1.print();d1.print();Date tmp2 = ++d2;tmp2.print();d2.print();
}void test05()
{Date d1(2024, 7, 13);Date d2(2000, 7, 20);cout << d1 - d2 << endl;Date d3(2024, 6, 31);
}void test06()
{Date d1(2024, 7, 13);Date d2(2000, 7, 20);cout << d1 << d2 << endl;cin >> d1 >> d2;cout << d1 << d2 << endl;}void test07()
{Date d1(2024, 7, 13);Date d2(d1);d2.print();Date d3 = d1;d3.print();Date d4(2024, 6, 5);Date d5(2012, 3, 14);d4 = d5;d4.print();
}void test08()
{Date d1(2024, 7, 13);const Date d2(2000, 8, 19);cout << &d1 << endl;cout << &d2 << endl;
}void test09()
{Date d1;Date d2(2024, 7, 14);cout << (d2 += (-500)) << endl;cout << (d1 -= (-500)) << endl;
}int main()
{//test02();//test01();//test03();//test04();//test05();//test06();//test07();//test08();test09();return 0;
}