本篇文章旨在闡述C++類的構造,拷貝構造,析構機制,以及指針成員變量指針懸空問題的解決。需要讀者有較好的C++基礎,熟悉引用,const的相關知識。
引言:
類作為C++語言的一種數據類型,是對C語言結構體的一種擴展。由於C++是面向過程與面向對象的混合語言,因此在使用面向對象思想解決現實問題模型時,設計好類是很重要的(跑題了)。關於類,這篇blog中有很好的介紹(鏈接http://blog.csdn.net/zqixiao_09/article/details/51474556)。我要介紹的是,關於創建一個空類,類體內都包含哪些成員函數呢?看下面例子 。
class MyClass { //創建一個空類MyClass }; void main() { MyClass c; //創建該類的對象c,此處會自動調用默認構造函數 MyClass d(c); //創建一個對象d,並且用已經存在的同類對象c去初始化d,此處調用了默認拷貝構造函數 MyClass e; //創建一個對象e e = c; //此處是對象賦值,調用了默認賦值運算符成員函數 }
那么我們來運行一下
可以看到是成功的。
以上實例說明,對於用戶定義的空類,該類會自動包含六個成員函數,分別是:
l 默認構造函數 A(){//空函數體}
l 默認拷貝構造函數(本次講解重點)A(const A & ){//簡單的對象成員變量賦值操作}
l 默認析構函數 ~A(){//空函數體}
l 賦值運算符重載成員函數(本次講解重點) A & operator =(const A &){//也是簡單的對象成員變量賦值操作}
l 取地址操作符重載成員函數
l Const修飾的取地址操作符重載成員函數
前四個是本次講解的內容,重點放在拷貝構造,賦值運算符重載這兩個成員函數
拷貝構造函數:
拷貝構造函數是一種特殊的構造函數,具有單個形參,該形參(常用const修飾)是對該類類型的引用。當定義一個新對象並用一個同類型的對象對它進行初始化時,將顯示使用拷貝構造函數。歸結來說。有三個場合要用到拷貝構造函數:
l 對象作為函數的參數,以值傳遞的方式傳給函數
l 對象作為函數的返回值,以值傳遞的方式從函數返回調用處
l 使用一個對象去初始化一個新建的對象
即有拷貝構造函數的調用一定會有新對象生成。
還有一點需要注意的是,拷貝構造函數必須以引用的方式傳遞參數。這是因為,在值傳遞的方式傳遞給一個函數的時候,會調用拷貝構造函數生成函數的實參。如果拷貝構造函數的參數仍然是以值的方式,就會無限循環的調用下去,直到函數的棧溢出。
例子:
#include<iostream.h>
#include<string.h> class Person{ public : Person(); //無參構造函數 Person(int age,char na[]); //重載一般構造函數 Person(const Person & p);//拷貝構造函數 ~Person(); // 析構函數 void disp(); private : int age; char *name; }; Person::Person(){ age=0; name=new char[2]; strcpy(name,"\0"); cout<<"default constructor\n";} Person::Person(int age,char na[]) { this->age=age; name=new char[strlen(na)+1]; //為指針變量動態分配空間 strcpy(name,na); //賦值 cout<<"constructor\n"; } Person::Person(const Person & p) { this->age=p.age; this->name=new char[strlen(p.name)+1]; strcpy(name,p.name); cout<<"copy constructor\n"; } Person::~Person() { delete [] name; cout<<"destroy\n"; } void Person::disp() { cout<<"age "<<age<<" name "<<name<<endl; } void f(Person p) { cout<<"enter f \n"; p.disp(); return ; } Person f1() { cout<<"enter f \n"; Person p; cout<<"next is return object of Person\n"; return p; } void main() { Person p1(21,"xiaowang");//調用一般構造函數 p1.disp(); Person p2(p1);//調用拷貝構造函數 p2.disp(); Person p3=p1;//調用拷貝構造函數 p3.disp(); cout<<"true\n"; cout<<"拷貝構造函數調用在函數形參是對象且值傳遞\n"; f(p1); //① cout<<"拷貝構造函數調用在函數返回值是對象且值傳遞\n"; f1(); //② cout<<"主函數結束,調用三次析構函數銷毀對象\n"; }
運行結果
我們來分析一下源程序①②處以及運行結果的畫線處
① 處是函數形參是對象,且是值傳遞的情況下調用了拷貝構造函數,我們可以看到該形參對象的生存期是只在函數f里面,當函數調用結束后,就自動被析構函數清理了。但是不會引起指針懸空問題,因為如下圖所示。
其中p對象是f的形參,它由主函數調用f開始存在,由函數f調用結束而撤銷,但是析構p時不會將p1的name所指空間析構,因此最終主函數main救贖后析構p1時不會引起指針懸空問題
② 函數返回值是對象且值傳遞返回方式時會調用靠寶貝構造函數。
分析結果會看到有兩次對象創建,在子函數f1里面先創建默認對象p,然后返回對象p到調用處,會自動調用拷貝構造,創建一個匿名的對象(記為pi),調用結束后會先析構p,在析構pi
賦值運算符重載成員函數
拷貝構造函數和賦值運算符的行為比較相似,都是將一個對象的值復制給另一個對象;但是其結果卻有些不同,拷貝構造函數使用傳入對象的值生成一個新的對象的實例,而賦值運算符是將對象的值復制給一個已經存在的實例。這種區別從兩者的名字也可以很輕易的分辨出來,拷貝構造函數也是一種構造函數,那么它的功能就是創建一個新的對象實例;賦值運算符是執行某種運算,將一個對象的值復制給另一個對象(已經存在的)。調用的是拷貝構造函數還是賦值運算符,主要是看是否有新的對象實例產生。如果產生了新的對象實例,那調用的就是拷貝構造函數;如果沒有,那就是對已有的對象賦值,調用的是賦值運算符。
實例:
#include<iostream.h> const int MAX=6; class Array{ double * data; public: Array(); Array(const Array &a); ~Array(); double & operator [](int i); //下標重載運算符 Array & operator =(Array & a); //=重載賦值運算符 Array & operator +(Array& a); //+運算符重載成員函數 Array & operator -(Array & a); //-運算符重載成員函數 void disp(); //輸出一個數組 }; Array::Array() { int i; data=new double[MAX]; for(i=0;i<MAX;i++) data[i]=0; cout<<"construct"<<endl; } Array::Array(const Array &a) { data=a.data; cout<<"copy construct \n"; } Array::~Array() { delete [] data; cout<<"destroy"<<endl; } double& Array::operator [](int i) //返回引用類型,可以是左值 { return *(data+i); } Array& Array::operator =(Array &a) //=重載賦值運算符 { int i; for(i=0;i<MAX;i++) data[i]=a.data[i]; cout<<"對象賦值,調用賦值運算符重載函數\n"; return *this; } Array & Array::operator +(Array& a) { int i; static Array tmp; for(i=0;i<MAX;i++) tmp.data[i]=data[i]+a.data[i]; return tmp; } Array & Array::operator -(Array & a) { for(int i=0;i<MAX;i++) data[i]-=a.data[i]; return *this; } void Array::disp() { for(int i=0;i<MAX;i++) cout<<data[i]<<" "; cout<<endl; } void main() { Array a,b,c,d; cout<<"創建四個數組對象\n"; cout<<"給數組a賦部分值\n"; a[0]=1; a[1]=2; a[2]=3; a[3]=4; cout<<"a=";a.disp(); cout<<"執行b=a\n"; b=a; cout<<"b=";b.disp(); cout<<"執行c=a+b\n"; c=a+b; cout<<"c=";c.disp(); cout<<"執行c=a+b之后a,b結果:\n"; cout<<"a=";a.disp(); cout<<"b=";b.disp(); cout<<"執行d=a-b\n"; d=a-b; cout<<"d=";d.disp(); cout<<"執行d=a-b之后a,b結果:\n"; cout<<"a=";a.disp(); cout<<"b=";b.disp(); cout<<"主函數執行完畢,銷毀四個對象和靜態成員對象\n"; }
運行結果
分析:
從結果可以看出,如果函數的形參是對象,或者返回值是對象,但是是以引用傳遞的方式,那么靠誒構造函數就不會被調用,這也是引用的作用,即對同一個對象起別名。,但要注意在賦值運算符重載成員函數中,對象的定義為靜態變量,這是為了防止子函數調用已結束就將析構該對象導致指針懸空問題。
深拷貝與淺拷貝
深拷貝和淺拷貝主要是針對類中的指針和動態分配的空間來說的,因為對於指針只是簡單的值復制並不能分割開兩個對象的關聯,任何一個對象對該指針的操作都會影響到另一個對象。這時候就需要提供自定義的深拷貝的拷貝構造函數,消除這種影響。通常的原則是:
- 含有指針類型的成員或者有動態分配內存的成員都應該提供自定義的拷貝構造函數
- 在提供拷貝構造函數的同時,還應該考慮實現自定義的賦值運算符
對於拷貝構造函數的實現要確保以下幾點:
- 對於值類型的成員進行值復制
- 對於指針和動態分配的空間,在拷貝中應重新分配分配空間
- 對於基類,要調用基類合適的拷貝方法,完成基類的拷貝
- 拷貝構造函數和賦值運算符的行為比較相似,卻產生不同的結果;拷貝構造函數使用已有的對象創建一個新的對象,賦值運算符是將一個對象的值復制給另一個已存在的對象。區分是調用拷貝構造函數還是賦值運算符,主要是否有新的對象產生。
- 關於深拷貝和淺拷貝。當類有指針成員或有動態分配空間,都應實現自定義的拷貝構造函數。提供了拷貝構造函數,最后也實現賦值運算符。
總結:
- 拷貝構造函數和賦值運算符的行為比較相似,卻產生不同的結果;拷貝構造函數使用已有的對象創建一個新的對象,賦值運算符是將一個對象的值復制給另一個已存在的對象。區分是調用拷貝構造函數還是賦值運算符,主要是否有新的對象產生。
- 關於深拷貝和淺拷貝。當類有指針成員或有動態分配空間,都應實現自定義的拷貝構造函數。提供了拷貝構造函數,最后也實現賦值運算符。