在學習這一章內容前我們已經學習過了類的建構函式和解構函式的相關知識,對於普通類型的對象來說,他們之間的複製是很簡單的,例如:
int a = 10;
int b =a;
自己定義的類的對象同樣是對象,誰也不能阻止我們用以下的方式進行複製,例如:
#include <iostream>
using namespace std;
class Test
{
public:
Test(int temp)
{
p1=temp;
}
protected:
int p1;
};
void main()
{
Test a(99);
Test b=a;
}
普通對象和類對象同為對象,他們之間的特性有相似之處也有不同之處,類對象內部存在成員變數,而普通對象是沒有的,當同樣的複製方法發生在不同的對象上的時候,那麼系統對他們進行的操作也是不一樣的,就類對象而言,相同類型的類對象是通過拷貝建構函式來完成整個複製過程的,在上面的代碼中,我們並沒有看到拷貝建構函式,同樣完成了複製工作,這又是為什麼呢?因為當一個類沒有自訂的拷貝建構函式的時候系統會自動提供一個預設的拷貝建構函式,來完成複製工作。
下面,我們為了說明情況,就普通情況而言(以上面的代碼為例),我們來自己定義一個與系統預設拷貝建構函式一樣的拷貝建構函式,看看它的內部是如何工作的!
代碼如下:
#include <iostream>
using namespace std;
class Test
{
public:
Test(int temp)
{
p1=temp;
}
Test(Test &c_t)//這裡就是自訂的拷貝建構函式
{
cout<<"進入copy建構函式"<<endl;
p1=c_t.p1;//這句如果去掉就不能完成複製工作了,此句複製過程的核心語句
}
public:
int p1;
};
void main()
{
Test a(99);
Test b=a;
cout<<b.p1;
cin.get();
}
上面代碼中的Test(Test &c_t)就是我們自訂的拷貝建構函式,拷貝建構函式的名稱必須與類名稱一致,函數的形式參數是本類型的一個引用變數,且必須是引用。
當用一個已經初始化過了的自訂類類型對象去初始化另一個新構造的對象的時候,拷貝建構函式就會被自動調用,如果你沒有自訂拷貝建構函式的時候系統將會提供給一個預設的拷貝建構函式來完成這個過程,上面代碼的複製核心語句就是通過Test(Test &c_t)拷貝建構函式內的p1=c_t.p1;陳述式完成的。如果取掉這句代碼,那麼b對象的p1屬性將得到一個未知的隨機值;
下面我們來討論一下關於淺拷貝和深拷貝的問題。
就上面的代碼情況而言,很多人會問到,既然系統會自動提供一個預設的拷貝建構函式來處理複製,那麼我們沒有意義要去自訂拷貝建構函式呀,對,就普通情況而言這的確是沒有必要的,但在某寫狀況下,類體內的成員是需要開闢動態開闢堆記憶體的,如果我們不自訂拷貝建構函式而讓系統自己處理,那麼就會導致堆記憶體的所屬權產生混亂,試想一下,已經開闢的一端堆地址原來是屬於對象a的,由於複製過程發生,b對象取得是a已經開闢的堆地址,一旦程式產生析構,釋放堆的時候,電腦是不可能清楚這段地址是真正屬於誰的,當連續發生兩次析構的時候就出現了運行錯誤。
為了更詳細的說明問題,請看如下的代碼。
#include <iostream>
using namespace std;
class Internet
{
public:
Internet(char *name,char *address)
{
cout<<"載入建構函式"<<endl;
strcpy(Internet::name,name);
strcpy(Internet::address,address);
cname=new char[strlen(name)+1];
if(cname!=NULL)
{
strcpy(Internet::cname,name);
}
}
Internet(Internet &temp)
{
cout<<"載入COPY建構函式"<<endl;
strcpy(Internet::name,temp.name);
strcpy(Internet::address,temp.address);
cname=new char[strlen(name)+1];//這裡注意,深拷貝的體現!
if(cname!=NULL)
{
strcpy(Internet::cname,name);
}
}
~Internet()
{
cout<<"載入解構函式!";
delete[] cname;
cin.get();
}
void show();
protected:
char name[20];
char address[30];
char *cname;
};
void Internet::show()
{
cout<<name<<":"<<address<<cname<<endl;
}
void test(Internet ts)
{
cout<<"載入test函數"<<endl;
}
void main()
{
Internet a("中國軟體開發實驗室","www.cndev-lab.com");
Internet b = a;
b.show();
test(b);
}
上面代碼就示範了深拷貝的問題,對對象b的cname屬性採取了新開闢記憶體的方式避免了記憶體歸屬不清所導致析構釋放空間時候的錯誤,最後我必須提一下,對於上面的程式我的解釋並不多,就是希望讀者本身運行程式觀察變化,進而深刻理解。
拷貝和淺拷貝的定義可以簡單理解成:如果一個類擁有資源(堆,或者是其它系統資源),當這個類的對象發生複製過程的時候,這個過程就可以叫做深拷貝,反之對象存在資源但複製過程並未複製資源的情況視為淺拷貝。
淺拷貝資源後在釋放資源的時候會產生資源歸屬不清的情況導致程式運行出錯,這點尤其需要注意!
以前我們的教程中討論過函數返回對象產生臨時變數的問題,接下來我們來看一下在函數中返回自訂類型對象是否也遵循此規則產生臨時對象!
先運行下列代碼:
#include <iostream>
using namespace std;
class Internet
{
public:
Internet()
{
};
Internet(char *name,char *address)
{
cout<<"載入建構函式"<<endl;
strcpy(Internet::name,name);
}
Internet(Internet &temp)
{
cout<<"載入COPY建構函式"<<endl;
strcpy(Internet::name,temp.name);
cin.get();
}
~Internet()
{
cout<<"載入解構函式!";
cin.get();
}
protected:
char name[20];
char address[20];
};
Internet tp()
{
Internet b("中國軟體開發實驗室","www.cndev-lab.com");
return b;
}
void main()
{
Internet a;
a=tp();
}
從上面的代碼運行結果可以看出,程式一共載入過解構函式三次,證明了由函數返回自訂類型對象同樣會產生臨時變數,事實上對象a得到的就是這個臨時Internet類類型對象temp的值。
這一下節的內容我們來說一下無名對象。
利用無名對象初始化對象系統不會不調用拷貝建構函式。
那麼什麼又是無名對象呢?
很簡單,如果在上面程式的main函數中有:
Internet ("中國軟體開發實驗室","www.cndev-lab.com");
這樣的一句語句就會產生一個無名對象,無名對象會調用建構函式但利用無名對象初始化對象系統不會不調用拷貝建構函式!
下面三段代碼是很見到的三種利用無名對象初始化對象的例子。
#include <iostream>
using namespace std;
class Internet
{
public:
Internet(char *name,char *address)
{
cout<<"載入建構函式"<<endl;
strcpy(Internet::name,name);
}
Internet(Internet &temp)
{
cout<<"載入COPY建構函式"<<endl;
strcpy(Internet::name,temp.name);
cin.get();
}
~Internet()
{
cout<<"載入解構函式!";
}
public:
char name[20];
char address[20];
};
void main()
{
Internet a=Internet("中國軟體開發實驗室","www.cndev-lab.com");
cout<<a.name;
cin.get();
}
上面代碼的運行結果有點“出人意料”,從思維邏輯上說,當無名對象建立了後,是應該調用自訂拷貝建構函式,或者是預設拷貝建構函式來完成複製過程的,但事實上系統並沒有這麼做,因為無名對象使用過後在整個程式中就失去了作用,對於這種情況c++會把代碼看成是:
Internet a("中國軟體開發實驗室",www.cndev-lab.com);
省略了建立無名對象這一過程,所以說不會調用拷貝建構函式。
最後讓我們來看看引用無名對象的情況。
#include <iostream>
using namespace std;
class Internet
{
public:
Internet(char *name,char *address)
{
cout<<"載入建構函式"<<endl;
strcpy(Internet::name,name);
}
Internet(Internet &temp)
{
cout<<"載入COPY建構函式"<<endl;
strcpy(Internet::name,temp.name);
cin.get();
}
~Internet()
{
cout<<"載入解構函式!";
}
public:
char name[20];
char address[20];
};
void main()
{
Internet &a=Internet("中國軟體開發實驗室","www.cndev-lab.com");
cout<<a.name;
cin.get();
}
引用本身是對象的別名,和複製並沒有關係,所以不會調用拷貝建構函式,但要注意的是,在c++看來:
Internet &a=Internet("中國軟體開發實驗室","www.cndev-lab.com");
是等價與:
Internet a("中國軟體開發實驗室","www.cndev-lab.com");
的,注意觀察調用解構函式的位置(這種情況是在main()外調用,而無名對象本身是在main()內析構的)。