C++類和對象——類的6個預設成員函數(構造、析構、拷貝構造、指派運算符重載)
- 一、類的6個預設成員函數
- 二、構造函數
-
- 2.1 構造函數概念
- 2.2 構造函數特性
-
- <1> 函數名類名與相同。
- <2> 無傳回值。
- <3> 對象執行個體化時編譯器自動調用對應的構造函數。
- <4> 構造函數可以重載。多個構造函數就會有多種初始化的方式。
- <5> 如果類中沒有顯式定義構造函數,則C++編譯器會自動生成一個無參的預設構造函數,一旦使用者顯式定義編譯器将不再生成。
- <6> 無參的構造函數和全預設的構造函數都稱為預設構造函數,并且預設構造函數隻能有一個。
- <7> 沒寫構造函數時,編譯器會預設生成一個構造函數。這個構造函數什麼事情都不做嘛?
- 三、析構函數
-
- 3.1 概念
- 3.2 特性
-
- <1> 析構函數名是在類名前加上字元 ~
- <2> 無參數無傳回值
- <3> 一個類有且隻有一個析構函數。若未顯式定義,系統會自動生成預設的析構函數。
- <4> 對象生命周期結束時,C++編譯系統系統自動調用析構函數。
- <5> 關于編譯器自動生成的析構函數,是否會完成一些事情呢?
- 四、拷貝構造函數
-
- 4.1 概念
- 4.2 特征
-
- <1> 拷貝構造函數是構造函數的一個重載形式
- <2> 拷貝構造函數的參數隻有一個且必須使用引用傳參,使用傳值方式會引發無窮遞歸調用
- <3> 若未顯示定義,系統生成預設的拷貝構造函數。 預設的拷貝構造函數對象按記憶體存儲按位元組序完成拷貝,這種拷貝我們叫做淺拷貝,或者值拷貝
- <4>編譯器生成的預設拷貝構造函數已經可以完成位元組序的值拷貝了,我們還需要自己實作嗎?
- 五、指派運算符重載
-
- 5.1 運算符重載
- 5.2 指派運算符重載
- 六、const成員
-
- 6.1 const修飾類的成員函數
-
- Q1:四種const修飾的調用關系?
- 七、取位址重載
一、類的6個預設成員函數
如果一個類中什麼成員都沒有,簡稱為空類。空類中什麼都沒有嗎?并不是的,任何一個類在我們不寫的情況下,都會自動生成下面6個預設成員函數
簡單來說上面6個預設成員函數就是,如果我們寫了編譯器就會使用我們寫的,如果我們不寫,編譯器就會自動生成一份。 針對不同的編譯器,生成的預設成員函數也會有差異,是以我們需要掌握每個函數的含義。
二、構造函數
2.1 構造函數概念
構造函數是一個特殊的成員函數,名字與類名相同,建立類類型對象時由編譯器自動調用,保證每個資料成員都有 一個合适的初始值,并且在對象的生命周期内隻調用一次。
2.2 構造函數特性
構造函數是特殊的成員函數,需要注意的是,構造函數雖然名稱叫構造,但是需要注意的是構造函數的主要任務并不是開空間建立對象,而是初始化對象。
要牢記以下特性:
<1> 函數名類名與相同。
例如:日期類的構造函數就是Date()
<2> 無傳回值。
<3> 對象執行個體化時編譯器自動調用對應的構造函數。
<4> 構造函數可以重載。多個構造函數就會有多種初始化的方式。
實作日期類的構造函數,首先類名與函數名要相同,其次沒有傳回值,兩種構造方式,無參和帶參
class Date
{
public:
//1.無參構造函數
Date()
{}
//2.帶參構造函數
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;
};
void Test1()
{
Date d1; //調用無參構造函數
d1.Print();
Date d2(2021, 3, 2); //調用帶參構造函數
d2.Print();
// 注意:如果通過無參構造函數建立對象時,對象後面不用跟括号,否則就成了函數聲明
// 以下代碼的函數:聲明了d3函數,該函數無參,傳回一個日期類型的對象
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;
};
void Test2()
{
// 沒有定義構造函數,對象也可以建立成功,是以此處調用的是編譯器生成的預設構造函數
Date d;
d.Print();
}
<6> 無參的構造函數和全預設的構造函數都稱為預設構造函數,并且預設構造函數隻能有一個。
注意:
無參構造函數、全預設構造函數、我們沒寫編譯器預設生成的構造函數,都可以認為是預設成員函數
// 預設構造函數
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();
}
可以看到編譯失敗,原因是無參構造函數和全預設構造函數都是預設成員函數,且隻能存在一個,是以編譯失敗!
<7> 沒寫構造函數時,編譯器會預設生成一個構造函數。這個構造函數什麼事情都不做嘛?
//2.2.7
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;
};
void Test3()
{
Date d;
}
解答:
C++把類型分成内置類型(基本類型)和自定義類型。
内置類型就是文法已經定義好的類型:如int/char…,
自定義類型就是我們使用class/struct/union自己定義的類型
上述程式可以看出編譯器生成預設的構造函數會對自定類型成員_t調用的它的預設成員函數
三、析構函數
3.1 概念
前面通過構造函數的學習,我們知道一個對象時怎麼來的,那一個對象又是怎麼沒呢的?
析構函數:與構造函數功能相反,析構函數不是完成對象的銷毀,局部對象銷毀工作是由編譯器完成的。而對象在銷毀時會自動調用析構函數,完成類的一些資源清理工作。
3.2 特性
析構函數是特殊的成員函數。
其特征如下:
<1> 析構函數名是在類名前加上字元 ~
<2> 無參數無傳回值
<3> 一個類有且隻有一個析構函數。若未顯式定義,系統會自動生成預設的析構函數。
<4> 對象生命周期結束時,C++編譯系統系統自動調用析構函數。
對于前面實作的日期類,我們不要釋放任何資源,是以不需要編寫,使用C++預設生成的析構函數即可
而對于開辟空間的類則需要自己實作析構函數,釋放資源,例如:棧Stack
//3.2.1234
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int *)malloc(sizeof(int)*capacity);
_size = 0;
_capacity = capacity;
}
//棧的析構函數
~Stack()
{
free(_a); //釋放堆上的空間
_a = nullptr; //指針置空
_size = _capacity = 0;
cout << "~Stack()析構函數" << endl;
}
private:
int* _a;
int _size;
int _capacity;
};
void Test4()
{
Stack s;
}
<5> 關于編譯器自動生成的析構函數,是否會完成一些事情呢?
下面的程式我們會看到,編譯器生成的預設析構函數,對會自定類型成員調用它的析構函數
//3.2.5
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int *)malloc(sizeof(int)*capacity);
_size = 0;
_capacity = capacity;
}
//棧的析構函數
~Stack()
{
free(_a); //釋放堆上的空間
_a = nullptr; //指針置空
_size = _capacity = 0;
cout << "~Stack()析構函數" << endl;
}
private:
int* _a;
int _size;
int _capacity;
};
class Person
{
private:
int _age;
Stack _s;
};
void Test5()
{
Person p;
}
四、拷貝構造函數
4.1 概念
構造函數:隻有單個形參,該形參是對本類類型對象的引用(一般常用const修飾),在用已存在的類類型對象建立新對象時由編譯器自動調用。
4.2 特征
拷貝構造函數也是特殊的成員函數,其特征如下:
<1> 拷貝構造函數是構造函數的一個重載形式
<2> 拷貝構造函數的參數隻有一個且必須使用引用傳參,使用傳值方式會引發無窮遞歸調用
//4.2.2
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 Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void Test6()
{
Date d1(2021,3,7);
Date d2(d1);
Date d3 = d1;
d1.Print();
d2.Print();
d3.Print();
}
<3> 若未顯示定義,系統生成預設的拷貝構造函數。 預設的拷貝構造函數對象按記憶體存儲按位元組序完成拷貝,這種拷貝我們叫做淺拷貝,或者值拷貝
<4>編譯器生成的預設拷貝構造函數已經可以完成位元組序的值拷貝了,我們還需要自己實作嗎?
當然像日期類這樣的類是沒必要的。但是像Stack這樣關注着記憶體塊資源的類淺拷貝是不能解決的,需要自己實作拷貝構造,即深拷貝!
五、指派運算符重載
5.1 運算符重載
C++為了增強代碼的可讀性引入了運算符重載,運算符重載是具有特殊函數名的函數,也具有其傳回值類型,函數名字以及參數清單,其傳回值類型與參數清單與普通的函數類似。
函數名字為:關鍵字operator後面接需要重載的運算符符号。
函數原型:傳回值類型 operator操作符(參數清單)
注意:
- 不能通過連接配接其他符号來建立新的操作符:比如[email protected]
- 重載操作符必須有一個類類型或者枚舉類型的操作數
- 用于内置類型的操作符,其含義不能改變,例如:内置的整型+,不能改變其含義
作為類成員的重載函數時,其形參看起來比操作數數目少1成員函數的
操作符有一個預設的形參this,限定為第一個形參
- .* 、:: 、sizeof 、?: 、. 注意以上5個運算符不能重載。這個經常在筆試選擇題中出現。
//5.1
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;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void Test7()
{
Date d1(2021, 3, 7);
Date d2(2021, 3, 6);
Date d3(2021, 3, 7);
cout << (d1 == d2) <<endl;
cout << (d1 == d3) << endl;
}
5.2 指派運算符重載
指派運算符主要有四點:
- 參數類型
- 傳回值
- 檢測是否自己給自己指派
- 傳回*this
- 一個類如果沒有顯式定義指派運算符重載,編譯器也會生成一個,完成對象按位元組序的值拷貝。
//5.2
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//如果使用者沒有顯式定義指派運算符重載,編譯器器也會自動生成一個
bool operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day == d._day;
}
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void Test8()
{
Date d1(2021, 3, 7);
Date d2 = d1;
d1.Print();
d2.Print();
}
同樣對于日期類來說我們隻需要使用編譯器預設生成的複制重載函數就可以了,但是對于像Stack這樣的類來說則需要使用深拷貝去解決了!
六、const成員
6.1 const修飾類的成員函數
将const修飾的類成員函數稱之為const成員函數,const修飾類成員函數,實際修飾該成員函數隐含的this指針,表明在該成員函數中不能對類的任何成員進行修改。
首先來看以下為什麼要使用const修飾類的成員函數,如下問題
這個時候我們就使用const修飾類的成員函數,實際上就是修飾this指針
//6.1
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void Test8()
{
const Date d1(2021, 3, 7);
d1.Print();
}
結果
Q1:四種const修飾的調用關系?
-
const對象可以調用非const成員函數嗎?
不可以 , 原因:this指針被傳遞給非const成員函數,權限被擴大
-
非const對象可以調用const成員函數嗎?
可以,原因:this指針被傳遞給const成員函數,權限被縮小
-
const成員函數内可以調用其它的非const成員函數嗎?
不可以,原因:const修飾的成員函數其實就是修飾this指針,在const修飾的成員函數内調用非const成員函數,就是将const 修飾的this指針傳遞傳遞給了非const成員函數,權限被擴大
-
非const成員函數内可以調用其它的const成員函數嗎?
可以,原因同3相反,非const成員函數将this指針傳遞給了const成員函數,就是對非const修飾的this指針加了const修飾,權限被縮小
總結:
能否調用看的就是this指針是否被傳過去!
能否傳過去就是要看權限被放大還是縮小,權限隻能縮小不能放大!
七、取位址重載
這兩個預設成員函數一般不用重新定義 ,編譯器預設會生成。
//7
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};