大家都定義struct或class時,不能給成員直接賦值,那么對象中成員變量的初始值是多少?
對於局部對象變量而言,其成員是個隨機值,因為該變量是被分配在棧上,對於其它局部變量也是這樣.
對於全局對象變量而言,其成員都為0,因為該變量是被分配在靜態存儲區上,對於const修飾就是分配在只讀靜態存儲區上.
對於使用malloc分配的對象變量而言,其成員是個隨機值,因為用戶分配的地址是存在堆上
對於使用new分配的對象變量而言,其成員也為隨機,因為用戶分配的地址是存在堆上
所以:
- 在棧上創建對象時,成員變量為隨機值
- 在堆上創建對象時,成員變量初始為隨機值
- 在靜態存儲區創建對象時,成員變量初始為0
構造函數
一般而言,對象創建時都會需要一個確定的初始狀態
所以在C++中,引入了一個特殊函數-構造函數
- 構造函數的名字必須與類名相同
- 構造函數可以帶參數,但是沒有任何返回類型的聲明,
- 構造函數在創建對象時,會被自動調用
參考下面示例:
class Test { private: int i; int j; public: int getI() { return i; } int getJ() { return j; } Test() //構造函數 { i = 1; j = 2; } }; Test t; //創建全局對象t,並自動調用Test()來初始化 i=1 j=2
多個重載的構造函數
由於構造函數可以帶參數,所以一個類可以存在多個重載的構造函數
例如:
class Test { public: Test(){ } Test(int i){ } Test(int i,float t){ } };
多個重載構造函數的調用
在之前小節,分析到構造函數是用來初始化對象的.如果有多個重載的構造函數,又如何來調用呢?
參考下面示例:
#include <stdio.h> class Test { private: int m_val; public : Test() { m_val=0; printf("Test() \n"); } Test(int i) { m_val=i; printf("Test(int i) i=%d \n",i); } Test(float t,int i) { m_val=i; printf("Test(float t,int i) t=%f i=%d\n",t,i); } }; int main() { Test t1; //調用Test()初始化 Test t2(2); //調用Test(int i) 初始化 Test t4=1; //調用Test(int i) 初始化 Test t3(1.5f,2); //調用Test(float t,int i) 初始化 Test t4=Test(3,4); //調用Test(3,4)返回一個臨時對象,然后通過拷貝構造函數來初始化t4 t1=t4; //賦值操作,所以不會調用Test()初始化 return 0; }
為什么使用Test t4=1 能調用Test (4)?
當構造函數的參數只有一個時,並且參數是其它類型,該構造函數便稱為轉換構造函數
所以編譯Test t4=1時,編譯器會通過1來查找哪個構造函數的參數滿足它,若沒找到則編譯報錯.
同樣在C++中,也可以通過()來初始化變量,比如:
int i(100); //轉換為 int i=100;
對象數組之手工調用構造函數
還是以上個Test類為例:
Test Tarray[3]={ Test(),Test(1), Test(2)}; //初始化對象數組里的m_val值分別為0,1,2;
從上面可以看出,一個構造函數其實是有返回值的,返回的是一個臨時對象,然后賦值給Tarray[]數組里。
這個臨時對象僅僅在調用時有效,執行下個代碼時,就會被注銷。
臨時對象在后面第11章會講到:11.C++-臨時對象分析
特殊的構造函數
-無參數構造函數
當類中沒有定義構造函數時,編譯器會默認提供一個函數體為空的無參構造函數,
-拷貝構造函數 (參數為: const class_name&)
當類中沒有定義拷貝構造函數時,編譯器會默認提供一個拷貝構造函數,簡單的進行成員變量的復制
1.接下來證明無參構造函數的存在,參考下面出錯的示例
#include <stdio.h> class Test { private: int m_val; public : int getm(void) { return m_val; } Test(int val) { m_val=val; } }; int main() { Test t1; return 0; }
編譯時, 報錯:
test.cpp:21: error: no matching function for call to ‘Test::Test()’
提示說, 定義Test t1時,沒有沒匹配到Test()無參構造函數.
這是因為我們提供了構造函數,所以編譯器就不再提供無參構造函數了,從而編譯報錯。
也可以將上面的Test(int val)改為:
Test(int val=0) { m_val=val; }
這樣,就相當於提供了兩個函數: Test(int val), Test().
當我們調用Test(1)時,則val=1.
當我們調用Test()時,則val=0.
這是C++新加的特性,在C里是沒有該功能
2.接下來來證明拷貝構造函數的存在,參考下面示例
#include <stdio.h> class Test { private: int m_val; public : int getm(void) { return m_val; } // Test() // { // } // Test(const Test& t) //定義一個拷貝構造函數 // { // printf("set m_val=%d\n",t.m_val); // m_val= t.m_val; // } }; int main() { Test t1; //調用Test()初始化 Test t2=t1; printf("t1.m_val=%d t2.m_val=%d \n",t1.getm(),t2.getm()); return 0; }
運行打印:
t1.m_val=-1078151848 t2.m_val=-1078151848
可以發現打印的數據t1.m_val和t2.m_val的值是一摸一樣的,這是因為執行Test t2=t1;時,由於Test類里沒有提供拷貝構造函數,所以編譯器提供了一個拷貝構造函數。
我們取消上面示例的屏蔽,使用自定義的拷貝構造函數:
運行打印:
set m_val=-1076378568 t1.m_val=-1076378568 t2.m_val=-1076378568
從打印的數據上看到,執行Test t2=t1; 時,明顯調用了我們自定義的Test::Test(const Test& t)拷貝函數.
所以當類中沒有定義拷貝構造函數時,編譯器會默認提供一個拷貝構造函數,進行簡單的成員變量拷貝.
深入理解拷貝構造函數
拷貝構造函數分為兩種:
-淺拷貝(編譯器提供的)
拷貝后對象的物理狀態相同
-深拷貝(指自己定義的)
拷貝后對象的邏輯狀態相同
接下來看淺拷貝和深拷貝的區別,參考下面示例:
#include <stdio.h> class Test { private: int m_val; int *p; public : int getm() { return m_val; } int* getp() { return p; } void free() { delete p; } Test(int i) {
delete p; p= new int(i); m_val=0; } // Test(const Test& obj) // { // p=new int; // // m_val=t.m_val; // *p=*obj.p; // } }; int main() { Test t1(2); //調用Test(int i)初始化 Test t2=t1; //調用編譯器提供的拷貝構造函數,進行淺拷貝 printf("t1.m_val=%d t1.p=%p *t1.p=%d\n",t1.getm(),t1.getp(),*t1.getp()); printf("t2.m_val=%d t2.p=%p *t2.p=%d\n",t2.getm(),t2.getp(),*t2.getp());
t1.free(); t2.free(); return 0; }
運行打印:
t1.m_val=0 t1.p=0x9fd1008 *t1.p=2 t2.m_val=0 t2.p=0x9fd1008 *t2.p=2 *** glibc detected *** ./a.out: double free or corruption (fasttop): 0x09fd1008 ***
從打印結果看出,進行淺拷貝時,兩個對象的成員指針都指向同一個地址0x9fd1008,可以發現當我們釋放了t1對象的成員指針后,就不能繼續使用t2對象的成員指針了.
接下來,我們取消上面示例的屏蔽,使用深拷貝,便能解決這類問題了.
那么什么時候需要進行深拷貝?
-當對象成員有指針時
-當對象成員需要打開文件時
-需要鏈接數據庫時
總結:
既然,淺拷貝可以實現成員變量拷貝,所以,只要自定義拷貝構造函數,必然里面會實現深拷貝.
下章繼續學習:10.C++-構造函數初始化列表、對象構造順序、析構函數