📙 作者簡介 :RO-BERRY
📗 學習方向:致力于C、C++、數據結構、TCP/IP、數據庫等等一系列知識
📒 日后方向 : 偏向于CPP開發以及大數據方向,歡迎各位關注,謝謝各位的支持
目錄
- 前言
- 1. 構造函數
- 1.1 概念
- 1.2 特性
- 2.析構函數
- 2.1 概念
- 2.2 特性
- 3. 拷貝構造函數
- 3.1 概念
- 3.2 特征
- 4.賦值運算符重載
- 4.1 運算符重載
- 4.2 賦值運算符重載
- 4.3 前置++和后置++重載
- 5.const成員
- 6.取地址及const取地址操作符重載
前言
如果一個類中什么成員都沒有,簡稱為空類。
空類中真的什么都沒有嗎?
并不是,任何類在什么都不寫時,編譯器會自動生成以下6個默認成員函數。
這六個函數包括:構造函數、析構函數、拷貝構造函數、賦值運算符重載、普通對象取地址重載、const修飾對象的取地址重載。
所有的默認成員函數都是可以由編譯器自己生成的,編譯器會自己生成一個默認的函數。同時我們也可以自己定義這些函數,那么編譯器就會直接使用我們定義好的函數而不會自己再生成了。
1. 構造函數
1.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 公有方法給對象設置日期,但如果每次創建對象時都調用該方法設置信息,未免有點麻煩,那能否在對象創建時,就將信息設置進去呢?
這個時候就要引入我們的構造函數
構造函數是一個特殊的成員函數,名字與類名相同,創建類類型對象時由編譯器自動調用,以保證每個數據成員都有 一個合適的初始值,并且在對象整個生命周期內只調用一次。
1.2 特性
構造函數是特殊的成員函數,需要注意的是,構造函數雖然名稱叫構造,但是構造函數的主要任務并不是開空間創建對象,而是初始化對象。
其特征如下:
- 函數名與類名相同。
- 無返回值。
- 對象實例化時編譯器自動調用對應的構造函數。
- 構造函數可以重載。
class Date
{
public:// 1.無參構造函數//Date()//{// _year = 1;// _month = 1;// _day = 1;//} 2.帶參構造函數//Date(int year, int month, int day)//{// _year = year;// _month = month;// _day = day;//}
//這兩個函數構成重載,但是無參調用存在歧義,不能同時存在//Date(int year, int month, int day)//{// _year = year;// _month = month;// _day = day;//}//Date(int year=1, int month=1, int day=1) 全缺省//{// _year = year;// _month = month;// _day = day;//}Date(int year=1, int month=1, int day=1) //全缺省{_year = year;_month = month;_day = day;}
//使用全缺省這里可以傳一個兩個三個或者不傳,就比較方便
private:int _year;int _month;int _day;
};
void TestDate()
{Date d1; // 調用無參構造函數//這里可不可以寫Date d1()//不可以 這里如果這樣寫就和函數聲明分不開了//比如Date f() -->函數名為f。返回值為DateDate d2(2015, 1, 1); // 調用帶參的構造函數// 注意:如果通過無參構造函數創建對象時,對象后面不用跟括號,否則就成了函數聲明// 以下代碼的函數:聲明了d3函數,該函數無參,返回一個日期類型的對象// warning C4930: “Date d3(void)”: 未調用原型函數(是否是有意用變量定義的?)Date d3();
}
- 如果類中沒有顯式定義構造函數,則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;
}
- 關于編譯器生成的默認成員函數,很多童鞋會有疑惑:不實現構造函數的情況下,編譯器會生成默認的構造函數。但是看起來默認構造函數又沒什么用?對象調用了編譯器生成的默認構造函數,但是對象_year/_month/_day依舊是隨機值。也就說在這里編譯器生成的默認構造函數并沒有什么用??
解答**:C++把類型分成內置類型(基本類型)和自定義類型。**內置類型就是語言提供的數據類
型,如:int/char…,自定義類型就是我們使用class/struct/union等自己定義的類型,看看
下面的程序,就會發現編譯器生成默認的構造函數會對自定類型成員_t調用的它的默認成員
函數
class Time
{
public:Time(){cout << "Time()" << endl; //運行構造函數會輸出Time()_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
{
private:// 基本類型(內置類型)int _year = 1970;int _month = 1;int _day = 1;// 自定義類型Time _t;
};
int main()
{Date d;return 0;
}
- 無參的構造函數和全缺省的構造函數都稱為默認構造函數,并且默認構造函數只能有一個。
注意:無參構造函數、全缺省構造函數、我們沒寫編譯器默認生成的構造函數,都可以認為
是默認構造函數。
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類里有多個默認構造函數
2.析構函數
2.1 概念
通過前面構造函數的學習,我們知道一個對象是怎么來的,那一個對象又是怎么沒呢的?
析構函數:與構造函數功能相反,析構函數不是完成對對象本身的銷毀,局部對象銷毀工作是由
編譯器完成的。而對象在銷毀時會自動調用析構函數,完成對象中資源的清理工作。
2.2 特性
析構函數是特殊的成員函數,其特征如下:
- 析構函數名是在類名前加上字符 ~。
- 無參數無返回值類型。
- 一個類只能有一個析構函數。若未顯式定義,系統會自動生成默認的析構函數。注意:析構
函數不能重載 - 對象生命周期結束時,C++編譯系統系統自動調用析構函數
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;
}
- 初始化Stack
- 插入數據
- 程序結束
- 關于編譯器自動生成的析構函數,是否會完成一些事情呢?下面的程序我們會看到,編譯器
生成的默認析構函數,對自定類型成員調用它的析構函數。
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類。
3. 拷貝構造函數
3.1 概念
在現實生活中,可能存在一個與你一樣的自己,我們稱其為雙胞胎。
那在創建對象時,可否創建一個與已存在對象一某一樣的新對象呢?
拷貝構造函數:只有單個形參,該形參是對本類類型對象的引用(一般常用const修飾),在用已存在的類類型對象創建新對象時由編譯器自動調用。
3.2 特征
拷貝構造函數也是特殊的成員函數,其特征如下:
- 拷貝構造函數是構造函數的一個重載形式。
- 拷貝構造函數的參數只有一個且必須是類類型對象的引用,使用傳值方式編譯器直接報錯,
因為會引發無窮遞歸調用。
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) // 錯誤寫法:編譯報錯,會引發無窮遞歸{_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;
}
- 若未顯式定義,編譯器會生成默認的拷貝構造函數。 默認的拷貝構造函數對象按內存存儲按
字節序完成拷貝,這種拷貝叫做淺拷貝,或者值拷貝。
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;
}
注意:在編譯器生成的默認拷貝構造函數中,內置類型是按照字節方式直接拷貝的,而自定
義類型是調用其拷貝構造函數完成拷貝的。
- 編譯器生成的默認拷貝構造函數已經可以完成字節序的值拷貝了,還需要自己顯式實現嗎?
當然像日期類這樣的類是沒必要的。那么下面的類呢?驗證一下試試?
這里會發現下面的程序會崩潰掉,這里就需要我們以后講的深拷貝去解決。
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(s1);return 0;
}
注意:類中如果沒有涉及資源申請時,拷貝構造函數是否寫都可以;一旦涉及到資源申請
時,則拷貝構造函數是一定要寫的,否則就是淺拷貝。
- 拷貝構造函數典型調用場景:
- 使用已存在對象創建新對象
- 函數參數類型為類類型對象
- 函數返回值類型為類類型對象
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;
}
為了提高程序效率,一般對象傳參時,盡量使用引用類型,返回時根據實際場景,能用引用
盡量使用引用。
4.賦值運算符重載
4.1 運算符重載
C++為了增強代碼的可讀性引入了運算符重載,運算符重載是具有特殊函數名的函數,也具有其返回值類型,函數名字以及參數列表,其返回值類型與參數列表與普通的函數類似。
函數名字:關鍵字operator后面接需要重載的運算符符號。
函數原型:返回值類型 operator操作符(參數列表)
注意:
- 不能通過連接其他符號來創建新的操作符:比如operator@
- 重載操作符必須有一個類類型參數
- 用于內置類型的運算符,其含義不能改變,例如:內置的整型+,不 能改變其含義
- 作為類成員函數重載時,其形參看起來比操作數數目少1,因為成員函數的第一個參數為隱藏的this
.*
::
sizeof
?:
.
注意這5個運算符不能重載。這個經常在筆試選擇題中出現。
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;
}
void Test()
{Date d1(2018, 9, 26);Date d2(2018, 9, 27);cout << (d1 == d2) << endl;
}class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// bool operator==(Date* this, const Date& d2)// 這里需要注意的是,左操作數是this,指向調用函數的對象bool operator==(const Date & d2){return _year == d2._year;&& _month == d2._month&& _day == d2._day;}
private:int _year;int _month;int _day;
};
4.2 賦值運算符重載
- 賦值運算符重載格式
- 參數類型: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;}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;
};
- 賦值運算符只能重載成類的成員函數不能重載成全局函數
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 =”必須是非靜態成員
原因:賦值運算符如果不顯式實現,編譯器會生成一個默認的。此時用戶再在類外自己實現一個全局的賦值運算符重載,就和編譯器在類中生成的默認賦值運算符重載沖突了,故賦值運算符重載只能是類的成員函數。
- 用戶沒有顯式實現時,編譯器會生成一個默認賦值運算符重載,以值的方式逐字節拷貝。注意:內置類型成員變量是直接賦值的,而自定義類型成員變量需要調用對應類的賦值運算符重載完成賦值。
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;
}
既然編譯器生成的默認賦值運算符重載函數已經可以完成字節序的值拷貝了,還需要自己實現嗎?當然像日期類這樣的類是沒必要的。那么下面的類呢?驗證一下試試?
// 這里會發現下面的程序會崩潰掉?這里就需要我們以后講的深拷貝去解決。
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;
}
注意:如果類中未涉及到資源管理,賦值運算符是否實現都可以;一旦涉及到資源管理則必
須要實現。
4.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;
}
5.const成員
將const修飾的“成員函數”稱之為const成員函數,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();
}
我們定義一個const對象Date d1
我們再實現成員函數print卻無法實現這是為什么?
在這里就是權限放大是不可以訪問到加了const的類對象
所以我們開發者就有了一個解決方案,那就是在函數后面加上const
這樣操作const對象就能調用了,那么我們的非const對象又能不能調用呢?
我們調試了發現是可以的,所以我們在函數后面加const之后,既可以訪問一般類對象也可以訪問加了
const類對象
在這里就是權限的縮小
也就意味著權限可以調用同權限的函數也可以實現權限縮小,就是不可以進行權限放大
那么是不是所有的成員函數都需要加const呢?
并不是這樣的
我們一般進行運算法重載的成員函數后面需要加const
在這里我們說一下成員函數定義的原則:
1.能定義成const的成員函數都應該定義成const,這樣const對象和非const對象都可以調用
2.要修改成員變量的函數,不能定義成const
有人會問那么那些不加const的函數那么加const的類對象不是無法訪問?
對,確實是無法訪問,但是const對象本來就不支持修改成員變量,那么不能訪問也當然是很合理的啊
6.取地址及const取地址操作符重載
這兩個默認成員函數一般不用重新定義 ,編譯器默認會生成。
class Date
{
public:Date* operator&(){return this;}const Date* operator&()const{return this;}
private:int _year; // 年int _month; // 月int _day; // 日
};
這兩個運算符一般不需要重載,使用編譯器生成的默認取地址的重載即可,只有特殊情況,才需
要重載,比如想讓別人獲取到指定的內容!