9.C++-對象的構造函數(詳解)


大家都定義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_valt2.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++-構造函數初始化列表、對象構造順序、析構函數

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM