C++ 拷貝控制 【學習筆記】

類的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'); //拷貝初始化
  • 什麼時候拷貝初始化發生?
    1. 使用=定義變量;
    2. 將一個對象作爲實參傳給一個非引用類型的形參;
    3. 從一個返回類型爲非引用類型的函數返回一個對象;
    4. 列表初始化
  • 爲什麼拷貝構造函數的第一個參數必須是引用類型?
    • 因爲如果參數不是引用類型,爲了調用構造函數,必須拷貝實參,爲了拷貝實參,又必須調用拷貝構造函數,因此調用永遠都不會成功。
  • 拷貝構造函數的第一個參數最好爲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();
	......
};
  • 什麼時候調用析構函數?無論何時一個對象被銷燬,就會調用其析構函數:
    1. 變量在離開其作用域時被銷燬;

    2. 當一個對象被銷燬,其成員也被銷燬;

    3. 容器(或數組)被銷燬,其元素也被銷燬;

    4. 對於動態分配的對象,當delete p時被銷燬;

    5. 對於臨時對象,當創建它的完整表達式結束時被銷燬;

      //臨時對象被銷燬的一個例子:
      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 &copy){
	i=copy.i;
	ps=new string(*copy.ps);
}	

也可以直接使用構造函數初始值列表:

HasPtr::HasPtr(const HasPtr &copy):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一樣對他進行初始化。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章