類的5種特殊成員函數
- 拷貝構造函數
- 拷貝賦值運算符
- 移動構造函數
- 移動賦值運算符
- 析構函數
1. 拷貝、賦值和銷燬
拷貝構造函數
Sales_data::Sales_data(const Sales_data &orig):
bookNo(orig.bookNo),
units_sold(orig.units_sold),
revenue(orig.revenue)
{}
- 定義:類構造函數的第一個參數是自身類類型的引用,且額外的參數都有默認值;
- 拷貝初始化調用了拷貝構造函數:
string dots(10,'.'); // 直接初始化
string s(dots); // 直接初始化
string s2 = dots; //拷貝初始化
string null_book = "99999999"; //拷貝初始化
string nines = string(100,'9'); //拷貝初始化
- 什麼時候拷貝初始化發生?
- 使用
=
定義變量; - 將一個對象作爲實參傳給一個非引用類型的形參;
- 從一個返回類型爲非引用類型的函數返回一個對象;
- 列表初始化
- 使用
- 爲什麼拷貝構造函數的第一個參數必須是引用類型?
- 因爲如果參數不是引用類型,爲了調用構造函數,必須拷貝實參,爲了拷貝實參,又必須調用拷貝構造函數,因此調用永遠都不會成功。
- 拷貝構造函數的第一個參數最好爲const類型,因爲拷貝構造函數並不會更改被拷貝對象。
拷貝賦值運算符
Sales_data& Sales_data::operator=(const Sales_data &rhs){
bookNo=rhs.bookNo; //調用string::operator=
units_sold=rhs.units_sold;//內置的int賦值
revenue=rhs.revenue; //內置的double賦值
return *this; //返回此對象的引用
}
- 重載運算符本質上是函數,函數名由關鍵字operator和運算符符號組成,賦值運算符就是一個名爲**operator=**的函數;
- 爲了與內置類型的賦值保持一致,賦值運算符通常返回一個指向左側運算對象的引用;
析構函數
- 析構函數釋放對象使用的資源,銷燬對象的非static數據成員;
- 析構函數不接受參數,也不能被重載,一個類有唯一的析構函數;
class Sales_data{
public:
~Sales_data();
......
};
- 什麼時候調用析構函數?無論何時一個對象被銷燬,就會調用其析構函數:
-
變量在離開其作用域時被銷燬;
-
當一個對象被銷燬,其成員也被銷燬;
-
容器(或數組)被銷燬,其元素也被銷燬;
-
對於動態分配的對象,當delete p時被銷燬;
-
對於臨時對象,當創建它的完整表達式結束時被銷燬;
//臨時對象被銷燬的一個例子: int *x=new int(1024); void process(shared_ptr<int> ptr){} process(shared_ptr<int>(x));//在這一步操作中,process函數接受了一個臨時對象,該臨時對象在 //表達式結束之後就銷燬了,因此x對應的內存被智能指針釋放,傳入的是一個被釋放的空指針
-
- 當指向一個對象的指針或引用離開作用域時,析構函數不會執行;
例題 1:(裏面有很多需要注意的點)
- 編寫String類的構造函數、拷貝構造函數、賦值函數和析構函數,已知String類的原型如下:
class String
{
public:
String(const char *str = NULL); // 普通構造函數
String(const String &other); // 拷貝構造函數
~ String(void); // 析構函數
String & operator =(const String &other); // 賦值函數
private:
char *m_data; // 用於保存字符串
};
- 解答:
- 注意:String中的數據成員是一個指針,未被初始化,不指向任何內存,因此所有構造函數都需要重新分配內存
//普通構造函數
String::String(const char *str){
//這裏如果傳入的str指針沒有被正確的初始化,對其進行strlen操作也會產生不確定的值,但是我們在函數內部只能判斷其
//是否爲空,不能判斷是否正確的初始化,這應該由函數的使用者來保證
if(str==NULL){ //這裏先判斷str是否爲空,空指針不能對其進行strlen操作,會產生段錯誤
//錯誤示範:*m_data='\0';//不能對一個未初始化的指針進行解引用操作,必須先申請內存
m_data = new char[1];//申請大小爲1的char數組來存放空字符
*m_data='\0';//現在指針指向了一塊內存,因此可以進行解引用操作
}
else{
int len=strlen(str);
m_data = new char[len+1]; //申請空間的時候要注意申請空字符的存儲空間
strcpy(m_data,str);
}
}
//拷貝構造函數
String::String(const String &other){
//先判斷other的m_data是否爲空
if(other.m_data==NULL){
m_data=new char[1];
*m_data = '\0';
}
else{
int len=strlen(other.m_data);//這裏不要有疑問,被拷貝對象other可以訪問其private成員,因爲這是在類內定義的構造函數
m_data=new char[len+1];
strcpy(m_data, other.m_data);//strcpy會將結尾的空字符一併拷貝
}
}
//拷貝賦值運算符
String & String::operator=(const String &other){
//檢查自賦值,如果自賦值,直接返回*this,不能進行後面的內存釋放操作了,如果釋放了內存,則參數中的對象也被釋放了,就無從拷貝了
if(this==&other){//判斷是否是自賦值,要用指針來比較,不能用對象來比較,對象就算相同,也不一定是自賦值
return *this;
}
//如果不是自賦值,就先釋放內存
delete [] m_data;
int len=strlen(other.m_data);
m_data=new char[len+1];
strcpy(m_data, other.m_data);
return *this;
}
//析構函數
String::~ String(void){
delete [] m_data;
}
拷貝、賦值、銷燬定義的基本原則:
- 基本原則一:需要析構函數的也需要拷貝構造和拷貝賦值運算符
- 基本原則二:需要拷貝操作的類也需要賦值操作,反之亦然
阻止拷貝
- 對於某些類,比如iostream,拷貝和賦值是沒有意義的,因此必須阻止拷貝,方法是定義刪除的函數:
struct NoCopy{
NoCopy()=default; //使用合成的默認構造函數
NoCopy(const NoCopy&)=delete;
NoCopy& operator=(NoCopy&)=delete;
~NoCopy()=default; //析構函數不能使刪除的成員
};
2. 拷貝控制和資源管理
- 對於一個類對象,我們對其進行拷貝,有兩種語義:第一種,這個類像一個值,原始對象和拷貝的副本擁有獨立的內存,改變原對象不會對副本產生任何影響,反之亦然;第二種,這個類像一個指針,拷貝操作只是拷貝了這個指針,但原始對象和拷貝的副本使用的是相同的底層數據
行爲像值的類:
- 例題1就是一個行爲像值的類,這裏再做一道例題,編寫一個類值版本的HasPtr構造函數:
class HasPtr{
public:
HasPtr(const string &s=string()):ps(new string(s)),i(0){} //普通構造函數
HasPtr(const HasPtr&); //拷貝構造函數
HasPtr& operator=(const HasPtr&);//賦值運算符
~HasPtr();//析構函數
private:
string *ps;
int i;
}
- 拷貝構造函數:
HasPtr::HasPtr(const HasPtr ©){
i=copy.i;
ps=new string(*copy.ps);
}
也可以直接使用構造函數初始值列表:
HasPtr::HasPtr(const HasPtr ©):i(copy.i),ps(new string(*copy.ps));
- 賦值運算符:(不需要檢測自賦值的寫法)
HasPtr& HasPtr::operator=(const HasPtr &orig){
auto temp=new string(*orig.ps);//使用中間變量的好處是,不需要檢測自賦值
delete ps;
ps=temp;
i=orig.i;
return *this;
}
- 析構函數:
HasPtr::~HasPtr(){delete ps;}
- 思考:這裏拷貝構造函數爲什麼不需要判斷ps爲空?
因爲,這裏默認構造函數中已經爲ps申請了一個空間,該空間至少存放一個空string,因此ps一定不是空指針,而且對空string解引用也是正確的。
而在例題一中,默認構造函數中接受的參數是一個內置指針,我們無法像string一樣對他進行初始化。