拷貝建構函式(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)。