1. 程式人生 > >拷貝建構函式(C++學習筆記 25)

拷貝建構函式(C++學習筆記 25)

一、拷貝建構函式的基本概念

  • 拷貝建構函式是一種特殊的建構函式。其形參是本類物件的引用。
  • 拷貝建構函式的作用:在建立一個新物件時,使用一個已經存在的物件去初始化這個新物件。例如:
Point p2(p1);

其作用是,在建立新物件p2時,用已經存在的物件p1去初始化新物件p2,在這個過程中就要呼叫拷貝建構函式。

  • 拷貝建構函式的特點
    1、因為該函式也是一種建構函式,所以其函式名與類名相同,並且該函式也沒有返回值型別。
    2、該函式只有一個引數,並且是同類物件的引用。
    3、每個類都必須有一個拷貝建構函式。
    可以自定義拷貝建構函式,用於按照需要初始化新物件;如果程式設計師沒有定義類的拷貝建構函式,系統就會自動產生一個預設拷貝建構函式,用於複製出資料成員值完全相同的新物件。

二、自定義拷貝建構函式

自定義拷貝建構函式(見例 1)

類名::類名(const 類名 &物件名)
{
   //拷貝建構函式的函式體
}

三、呼叫拷貝建構函式的3種情況(見例 2):

普通的建構函式是在物件建立時被呼叫,而拷貝建構函式在以下3中情況下都會被呼叫:
1、當用類的一個物件去初始化該類的另一個物件時。

代入法賦值法 兩種方法呼叫拷貝建構函式。

代入法:
  類名 物件2(物件1)
  例如:Point p1(p2);

賦值法:
  類名 物件2=物件1;
  例如:Point p2=p1;

  在這裡要注意 “ 賦值法呼叫拷貝建構函式 ” 和 “ 物件賦值語句 ” 的區別,物件賦值語句不會呼叫拷貝建構函式,它是通過預設運算子函式實現的。
  這裡的物件賦值是指對其中的資料成員賦值,而不對成員函式賦值。

2、當函式的形參是類的物件,在呼叫函式進行形參和實參結合時。
例如:

void fun1(Point p)   //形參是類Point的物件
{
	p.print();
}
int main(){
	Point p1(10,20);
	fun1(p1);   //呼叫函式fun1時,實參p1是類Point的物件。將呼叫拷貝建構函式,初始化形參物件p
	return 0;   
}

如果類Point中有自定義的拷貝建構函式,就呼叫這個自定義的拷貝建構函式,否則就呼叫系統自動生成的預設拷貝建構函式。

3、當函式的返回值是類的物件,在函式呼叫完畢將返回值(物件)帶回函式呼叫處時,此時就會呼叫拷貝建構函式,將此物件複製給一個臨時物件並傳到該函式的呼叫處。


例如:

Point fun2()    //函式fun2的返回值型別是Point類型別
{
	Point p1(10,30);
	return p1;   
}
int main(){
	Point p2;
	p2=fun2();    //函式執行完成,返回呼叫者時,呼叫拷貝建構函式
	return 0;
}

在上例中,物件p1是在函式fun2中定義的,在呼叫函式fun2結束時,p1的生命週期結束了,因此在函式fun2結束前,執行語句“return p1;”時,將會呼叫拷貝建構函式將p1的值複製到一個臨時物件中,這個臨時物件時編譯系統在主程式中臨時建立的。函式執行結束時,物件p1消失,但臨時物件將會通過語句“p2=fun2( )”將它的值賦給物件p2。執行完成這個語句後,臨時物件的使命也就完成了,該臨時物件便自動消失了。

四、例子

例 1: 自定義拷貝建構函式的使用。

#include<iostream>
using namespace std;
class Point{
	public:
		Point(int a,int b){  //普通建構函式 
			x=a;
			y=b;
		}
		Point(const Point &p){
			x=2*p.x;
			y=2*p.y;
		}
		void print(){
			cout<<x<<" "<<y<<endl;
		}
	private:
		int x,y;
};
int main(){
	Point p1(30,40);  //定義物件p1,呼叫了普通的建構函式 
	Point p2(p1);  //用代入法呼叫拷貝建構函式,用物件p1初始化物件p2
	Point p3=p1;   //用賦值法呼叫拷貝建構函式,用物件p1初始化物件p3 
	Point p4(0,0);
	p4=p1;    //物件賦值 
	p1.print();
	p2.print();
	p3.print();
	p4.print();
	return 0; 
}

執行結果:
在這裡插入圖片描述

例 2: 呼叫拷貝建構函式的3種情況。

#include<iostream>
using namespace std;
class Point{
	public:
		Point(int a=0,int b=0);  //宣告建構函式
		Point(const Point &p);  //宣告拷貝建構函式
		void print(); 
	private:
		int x,y;	
};
Point::Point(int a,int b){  //定義建構函式 
	x=a;
	y=b;
	cout<<"Using normal constructor\n";
}
Point::Point(const Point &p){	//定義拷貝建構函式
	x=2*p.x;
	y=2*p.y;
	cout<<"using copy constructor\n";
}
void Point::print(){
	cout<<x<<" "<<y<<endl;
}

void fun1(Point p){  //函式fun1的形參是類物件 
	p.print();
}
Point fun2(){  //函式fun2的返回值是類物件
	Point p4(10,30);   //定義物件p4時,要呼叫普通的建構函式
	return p4; 
}
int main(){
	//第一種 
	Point p1(30,20);  //定義物件p1,第 1 次呼叫普通的建構函式
	p1.print();
	Point p2(p1);  //建立新物件p2時,第 1 次呼叫拷貝建構函式 
	p2.print();
	Point p3=p1;  //建立新物件p3時,第 2 次呼叫拷貝建構函式
	p3.print();
	//第二種 
	fun1(p1);   //第 3 次呼叫拷貝建構函式,呼叫fun1時實參與形參結合 
	//第三種 
	p2=fun2();   //fun2函式中第 2 次呼叫普通的建構函式 ,第 4 次呼叫拷貝建構函式 
	p2.print();  
	return 0;	
}

執行結果:
在這裡插入圖片描述
按說,第三種情況的應該也會呼叫拷貝建構函式,但是顯示的結果卻意味著沒有呼叫。這是由於RVO優化。

RVO優化:
return value optimistic,指當一個函式返回一個值型別而非引用型別時,可以繞過拷貝/移動建構函式,直接在呼叫函式的地方構造返回值。
那怎麼去除RVO優化呢?
參考文章:https://blog.csdn.net/XiyouLinux_Kangyijie/article/details/78939291
https://blog.csdn.net/aaqian1/article/details/84205668

在這裡插入圖片描述
可以看到正確的執行結果了!!!

例 3: 拷貝建構函式練習-1

#include<iostream>
using namespace std;
class Point3d {
	private: 
		int m_x; 
		int m_y; 
		int m_z; 
	public:
		Point3d(int x,int y,int z):m_x(x),m_y(y),m_z(z) { 
			cout << "constructor"<<endl; 
		}
	    ~Point3d() {
	     	cout << "deconstructor"<<endl;
	    }
	    Point3d(const Point3d &other) {
	   	    this->m_x = other.m_x;
		    this->m_y = other.m_y;
		    this->m_z = other.m_z; 
			cout << "copy constructor"<<endl; 
		}
	    Point3d &operator=(const Point3d &other) {
			 if(this != &other) {
			 	 this->m_x = other.m_x;
				 this->m_y = other.m_y;
				 this->m_z = other.m_z;
	            } 
			cout << "operator="<<endl;
		    return *this; 
		} 
};

Point3d factory() { 
	Point3d po(1,2,3);  //第 1 次呼叫建構函式 
	return po;   //第 1 次呼叫拷貝建構函式 ;
	             //第 1 次呼叫解構函式,析構掉區域性物件po 
}
int main() {
	 Point3d p = factory(); //第 2 次呼叫拷貝建構函式;
	                        //第 2 次呼叫解構函式,析構掉臨時物件_temp
				//第 3 次呼叫解構函式,main函式結束,析構物件p 
	 return 1;
 }

執行結果:
在這裡插入圖片描述

例 4 :拷貝建構函式練習-2

程式碼來自:https://blog.csdn.net/XiyouLinux_Kangyijie/article/details/78939291

#include<iostream>
using namespace std;
class A{
	public:
		A() = default;
		A(const A &a):str(a.str){
			cout<<"copy"<<endl;
		}
		A(const string &d):str(d){  //建構函式,形參為常量引用
			cout<<"str"<<endl;
		}		
		string str;
};
void func(A a){
	cout<<a.str<<endl;
}
int main(){
	func(string("hello world"));//呼叫std::string 的建構函式,通過一個字串常量,構造了一個std::string物件
				   //這個物件是個臨時值,也就是說是個右值,我們叫它為string_temp。
	return 0;
}

執行結果:
在這裡插入圖片描述

  首先呼叫std::string 的建構函式,通過一個字串常量,構造了一個std::string物件,這個物件是個臨時值,也就是說是個右值,我們叫它為string_temp。
  在函式呼叫過程中,具有非引用型別的引數要進行拷貝初始化。這裡只能通過const string &的建構函式先構造出一個臨時A型別物件,再用該物件執行拷貝初始化。
   String_temp和A型別並不相等,這裡進行了隱式轉換,單引數的隱式轉換static_cast(String_temp)等同於A(String_temp),根據String_temp的型別決議使用const std::string &型別的建構函式。

  然後由於func的引數是一個A型別的物件,並且A有一個引數為const string &的建構函式,並且它沒有explicit修飾,也就支援通過std::string隱式轉換為A。那麼在這裡,便通過string_temp構造出了一個A型別物件,同樣是臨時值(右值),我們叫它A_temp。

  但是因為func的引數是一個A型別的物件,並不是一個引用,所以應該呼叫拷貝建構函式將A_temp拷貝到a。

  這裡發生了1次string的構造(string_temp),1次A的構造(A_temp),1次A的拷貝構造(a)。