構造函數詳解


1. 構造函數基本概念

   1)C++中的類可以定義與類名相同的特殊成員函數,這種與類名相同的成員函數叫做構造函數;

   2)構造函數在定義時可以有參數;

   3)沒有任何返回類型的聲明;

   二個特殊的默認構造函數:

      1)默認無參構造函數:當類中沒有定義構造函數時,編譯器提供一個默認的無參構造函數,並且其函數體為空。

      2)默認拷貝構造函數:當類中沒有定義拷貝構造函數時,編譯器提供一個默認的拷貝構造函數,簡單的進行成員變量的值復制。

   構造函數調用規則:

      1)當類中沒定義任何構造函數時,C++編譯器會提供默認無參構造函數和默認拷貝構造函數。

      2)當類中定義了拷貝構造函數時,C++編譯器不會提供無參數構造函數。

      3)當類中定義了任意的非拷貝構造函數(即:當類中提供了有參構造函數或無參構造函數),C++編譯器不會提供默認無參構造函數。

      4)默認拷貝構造函數進行的是淺拷貝。

      5)當類中定義了拷貝構造函數時,C++編譯器不會提供移動構造函數了。

 

2. 構造函數的分類及調用

   我們來看如下代碼:

class Test
{
private:
    int a, b;

public:
    Test() {}                  // 無參數構造函數
    Test(int a, int b) {}      // 帶參數的構造函數
    Test(const Test &obj) {}   // 賦值構造函數

public:
    void init(int _a, int _b)
    {
        a = _a;
        b = _b;
    }
};

   1)無參數構造函數:調用方法如下

Test t1, t2;
Test t1 = Test();        // 這樣才是調用默認構造函數,這時必須帶有括號

   2)帶參數構造函數

Test t1(20, 10);         // 括號法: C++編譯器默認調用有參構造函數 
Test t2 = (20, 10);      // 等號法: C++編譯器默認調用有參構造函數
Test t3 = Test(20, 10);  // 直接調用構造構造函數法: 程序員手工調用構造函數產生了一個對象

   3)賦值(拷貝)構造函數:顧名思義,即由其它對象來初始化自己。下面介紹賦值構造函數的三種調用場景(調用時機)。

      a. 定義變量時,用對象1初始化對象2

class Test
{
public:
    Test() { cout << "我是構造函數,自動被調用了" << endl; }
    Test(int _a) : a(_a) {}
    Test(const Test &obj2) { cout << "我也是構造函數,我是通過另外一個對象obj2,來初始化我自己" << endl; }
    ~Test() { cout<<"我是析構函數,自動被調用了"<<endl; }

private:
    int a;
};

int main()
{
    Test a1;
    Test a2 = a1; // 用 a1 初始化 a2
    Test a3(a1);  // 這樣寫也是用 a1 初始化 a3
    return 0;
}

      b. 實參變量初始化形參變量

class Location 
{ 
public:
    Location(int x = 0, int y = 0) : X(x), Y(y) { cout << "Constructor Object.\n"; }
    Location(const Location &p) : X(p.X), Y(p.Y) { cout << "Copy_constructor called." << endl; }
    ~Location() { cout << X << "," << Y << " Object destroyed." << endl; }
    int GetX()  { return X; }     
    int GetY()  { return Y; }

private:
    int X, Y;
};

void f(Location  p)   
{ 
    cout << "Funtion:" << p.GetX() << "," << p.GetY() << endl; 
}

int main()
{  
    Location A(1, 2);
    f(A);  // 調用f會構造一個臨時對象p,此時會調用拷貝構造函數
    return 0;
}

      c. 函數返回匿名對象,會在棧上面通過拷貝構造函數產生一個臨時對象(一般會被編譯器優化),然后原來的棧變量被析構。

         之后就取決於程序員怎么來接收這個匿名對象,不同的接法差別在於會不會多一次賦值運算符的調用。

         注:可以在編譯時設置編譯選項-fno-elide-constructors用來關閉返回值優化效果。

class Location 
{ 
public:
    Location(int x = 0, int y = 0) : X(x), Y(y) { cout << "Constructor Object.\n"; }
    Location(const Location &p) : X(p.X), Y(p.Y) { cout << "Copy_constructor called." << endl; }
    ~Location() { cout << X << "," << Y << " Object destroyed." << endl; }
    int GetX()  { return X; }     
    int GetY()  { return Y; }

private:
    int X, Y;
};

void f(Location p)   
{ 
    cout << "Funtion:" << p.GetX() << "," << p.GetY() << endl; 
}

/*
 * 當函數需要返回一個對象,他會在棧中創建一個臨時對象,存儲函數的返回值。
 * 這個臨時對象也是匿名對象,構造它時會調用拷貝構造函數,用A來初始化這個匿名對象。
 * 然后函數調用結束,A被銷毀.
 * 但是這個臨時對象的構造一般會被編譯器優化掉,所以自己測試的時候一般不會調用拷貝構造函數了。 
 */
Location g()
{
    Location A(1, 2);
    return A;
}

int main()
{  
    Location B;
    B = g();           // 若返回的匿名對象,賦值給另外一個同類型的對象,那么匿名對象會被析構。(會調用賦值運算符)
    Location C = g();  // 若返回的匿名對象,來初始化另外一個同類型的對象,那么匿名對象會直接轉成新的對象。(啥也不調用)
    return 0;
}

   4)移動構造函數:C++11引入移動語義----臨時對象資源的控制權(堆內存)全部交給目標對象。注意一下,臨時對象和目標對象是兩個獨立的不同對象,

      移動構造函數也不是說將臨時對象直接變成目標對象,只是將臨時對象所控制的資源進行淺拷貝(拷貝指針),而沒有了深拷貝然后臨時對象就無法

      訪問這個資源了,但臨時對象本身還是要被析構的。因為淺拷貝是難以避免的,所以類如果沒有堆上的資源,也就沒必要實現移動構造函數。

      下面舉個例子:

static unsigned int cCount;  //統計拷貝構造函數調用次數
static unsigned int mCount;  //統計移動構造函數調用次數

class MyString
{
public:
    // 構造函數
    MyString(const char* cstr = 0)
    {
        if (cstr) 
        {
            m_data = new char[strlen(cstr) + 1];
            strcpy(m_data, cstr);
        }
        else 
        {
            m_data = new char[1];
            *m_data = '\0';
        }
    }

    // 拷貝構造函數
    MyString(const MyString& str) 
    {
        cCount++;
        m_data = new char[strlen(str.m_data) + 1];
        strcpy(m_data, str.m_data);
    }

    // 移動構造函數
    MyString(MyString&& str)
    {
        mCount++;
        m_data = str.m_data;  // 目標對象接管堆上資源
        str.m_data = nullptr; // 臨時對象不再指向那個資源了
    }

    ~MyString() { delete[] m_data; }

private:
    char* m_data;
};

int main()
{
    vector<MyString> vecStr;
    vecStr.reserve(1000);    // 先分配好1000個空間
    for(int i = 0; i < 1000; i++)
    {
        vecStr.push_back(MyString("hello"));
    }
    cout << "cCount: " << cCount << endl;
    cout << "mCount: " << mCount << endl;
    return 0;
}

      運行可知道程序調用了1000次的移動構造函數,這樣就不會去重新分配一塊新的空間,將要拷貝的對象復制過來,而是"偷"了過來,將自己的指針

      指向別人的資源,然后將別人的指針修改為nullptr,這一步很重要,如果不將別人的指針修改為空,那么臨時對象析構的時候就會釋放掉這個資源,"偷"也白偷了。

      拋出一個問題:我們知道const引用也是能夠被右值初始化的,那編譯器怎么知道調用哪個構造函數呢?是拷貝還是移動?

          編譯器判斷傳入的參數是一個右值,會認為移動構造函數是一個更好的匹配。

      對於一個左值,肯定是調用拷貝構造函數了,但是有些左值是局部變量,生命周期也很短,能不能也移動而不是拷貝呢?C++11為了解決這個問題,提供

      了std::move()方法來將左值轉換為右值,從而方便應用移動語義。我覺得它其實就是告訴編譯器,雖然我是一個左值,但是不要對我用拷貝構造函數,而是

      用移動構造函數吧。。。

      注意一下:將一個臨時對象賦值給 T &&x 是延長臨時對象的生命周期的做法(不會移動或者拷貝),是右值引用。若賦值給 T x 則會觸發移動或者賦值構造函數。

      還是上面的類,現在改寫一下main函數。

int main()
{
    vector<MyString> vecStr;
    vecStr.reserve(1000);      //先分配好1000個空間
    for(int i = 0; i < 1000; i++)
    {
        MyString tmp("hello");
        vecStr.push_back(tmp); //調用的是拷貝構造函數
    }
    cout << "cCount: " << cCount << endl;
    cout << "mCount: " << mCount << endl;

    cCount = 0;
    mCount = 0;

    vector<MyString> vecStr2;
    vecStr2.reserve(1000);     //先分配好1000個空間
    for(int i = 0; i < 1000; i++)
    {
        MyString tmp("hello");
        /*
         * 調用的是移動構造函數
         * 此時tmp指向的資源已經為null了,但對象在表達式結束時尚未析構,作用域結束后才析構
         */
        vecStr2.push_back(std::move(tmp)); //調用的是移動構造函數
    }
    cout << "cCount: " << cCount << endl;
    cout << "mCount: " << mCount << endl;
    return 0;
}

// 輸出如下
cCount:1000
mCount:0
cCount:0
mCount:1000

    需要注意一下:如果我們沒有提供移動構造函數,只提供了拷貝構造函數,std::move()會失效但是不會發生錯誤,因為編譯器找不到移動構造函數就

      去尋找拷貝構造函數,也這是拷貝構造函數的參數是const T&常量左值引用的原因!

 

3. 構造函數隱式轉換

   用單個實參(也可以有多個實參,但是除了第一個參數,其它參數必須有默認值)來調用的構造函數定義了從形參類型類類型的一個隱式轉換。   

   隱式轉換沒有特別的語法,只要類型滿足構造函數的參數即可以觸發。簡單舉個例子

class Test  
{  
public:  
    bool same(const Test &rbs) const { return isbn == rbs.isbn; }  
    Test(const std::string &book = "7115145547") : isbn(book) {}  
private:  
    std::string isbn;  
};  

int main()
{
    Test trans;  
    string null_book = "9-999-99999-9";  
    trans.same(null_book);    // 這里會發生隱式類型轉換,從string轉換為test(因為有構造函數可以用一個string做參數),建立一個臨時的類的對象
    return 0; 
}

    為了避免這個情況的發生,可以將類的構造函數聲明為explicit,然后顯示調用:

explicit Test(const std::string &book = "7115145547") : isbn(book) {}
trans.same(Test(null_book));

 


免責聲明!

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



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