1. 程式人生 > >C++基礎教程面向物件(學習筆記(29))

C++基礎教程面向物件(學習筆記(29))

拷貝建構函式

重新初始化型別

由於我們將在接下來的幾節課中談論很多初始化,讓我們首先回顧一下C ++支援的初始化型別:直接初始化,統一初始化或拷貝初始化。

以下是使用我們的Fraction類的所有示例:

#include <cassert>
#include <iostream>
 
class Fraction
{
private:
    int m_numerator;
    int m_denominator;
 
public:
    //預設建構函式 
    Fraction(int numerator=0, int denominator=1) :
        m_numerator(numerator), m_denominator(denominator)
    {
        assert(denominator != 0);
    }
 
    friend std::ostream& operator<<(std::ostream& out, const Fraction &f1);
};
 
std::ostream& operator<<(std::ostream& out, const Fraction &f1)
{
	out << f1.m_numerator << "/" << f1.m_denominator;
	return out;
}

我們可以直接初始化:

int x(5); // 用一個整數直接初始化
Fraction fiveThirds(5, 3); // 直接初始化一個Fraction, 呼叫Fraction(int, int) 建構函式

在C ++ 11中,我們可以進行統一初始化:

int x { 5 }; //用一個整數統一初始化
Fraction fiveThirds {5, 3}; // 統一初始化一個Fraction, 呼叫Fraction(int, int) 建構函式

最後,我們可以進行拷貝初始化:

int x = 6; // 用一個整數複製初始化
Fraction six = Fraction(6); // 複製初始化一個Fraction,將呼叫Fraction(6,1)
Fraction seven = 7; //複製初始化Fraction。編譯器將嘗試找到將7轉換為Fraction的方法,該方法將呼叫Fraction(7,1)建構函式。

通過直接和統一的初始化,直接初始化正在建立的物件。但是,複製初始化稍微複雜一些。我們將在下一課中更詳細地探討複製初始化。但為了有效地做到這一點,我們需要先放下這一點。

拷貝建構函式

現在考慮以下程式:

#include <cassert>
#include <iostream>
 
class Fraction
{
private:
    int m_numerator;
    int m_denominator;
 
public:
    // 預設建構函式
    Fraction(int numerator=0, int denominator=1) :
        m_numerator(numerator), m_denominator(denominator)
    {
        assert(denominator != 0);
    }
 
    friend std::ostream& operator<<(std::ostream& out, const Fraction &f1);
};
 
std::ostream& operator<<(std::ostream& out, const Fraction &f1)
{
	out << f1.m_numerator << "/" << f1.m_denominator;
	return out;
}
 
int main()
{
	Fraction fiveThirds(5, 3); // 直接初始化一個Fraction, 呼叫Fraction(int, int) 建構函式
	Fraction fCopy(fiveThirds); // 直接初始化 -- 呼叫什麼建構函式呢?
	std::cout << fCopy << '\n';
}

如果你編譯這個程式,你會看到它編譯得很好,併產生結果: 5/3 讓我們仔細看看這個程式是如何工作的。 變數fiveThirds的初始化只是一個標準的直接初始化,它呼叫Fraction(int,int)建構函式。非常正常。但下一行怎麼樣?變數fCopy的初始化顯然也是直接初始化,你知道建構函式用於初始化類。那麼這行呼叫的建構函式是什麼?

答案是這行是呼叫Fraction的拷貝建構函式。一個拷貝建構函式是用來建立一個新的物件,為現有物件的拷貝建構函式的一種特殊型別。與預設構造函 數非常相似,如果您沒有為類提供拷貝建構函式,C ++將為您建立一個公共複製建構函式。由於編譯器對您的類知之甚少,因此預設情況下,建立的拷貝建構函式使用稱為成員初始化的初始化方法。 成員初始化只是意味著副本的每個成員都直接從被拷貝的類的成員初始化。在上面的例子中,fCopy.m_numerator將從fiveThirds.m_numerator等初始化…

就像我們可以顯式定義預設建構函式一樣,我們也可以顯式定義拷貝建構函式。拷貝建構函式看起來就像你期望的那樣:

#include <cassert>
#include <iostream>
 
class Fraction
{
private:
    int m_numerator;
    int m_denominator;
 
public:
    // 預設建構函式
    Fraction(int numerator=0, int denominator=1) :
        m_numerator(numerator), m_denominator(denominator)
    {
        assert(denominator != 0);
    }
 
    // 拷貝建構函式
    Fraction(const Fraction &fraction) :
        m_numerator(fraction.m_numerator), m_denominator(fraction.m_denominator)
        // 注意:我們可以直接訪問引數fraction的成員,因為我們在Fraction類中
    {
        //這裡不需要檢查0的分母,因為分數必須已經是有效分數
        std::cout << "Copy constructor called\n"; // just to prove it works
    }
 
    friend std::ostream& operator<<(std::ostream& out, const Fraction &f1);
};
 
std::ostream& operator<<(std::ostream& out, const Fraction &f1)
{
	out << f1.m_numerator << "/" << f1.m_denominator;
	return out;
}
 
int main()
{
	Fraction fiveThirds(5, 3); // 直接初始化一個Fraction, 呼叫Fraction(int, int) 建構函式
	Fraction fCopy(fiveThirds); // 直接初始化 -- 用Fraction拷貝建構函式?
	std::cout << fCopy << '\n';
}

執行此程式時,您將獲得: Copy constructor called 5/3 我們在上面的例子中定義的拷貝建構函式使用成員初始化,並且在功能上等同於我們預設獲得的拷貝建構函式,除了我們添加了一個輸出語句來說明正在呼叫拷貝建構函式。 與預設建構函式(您應該始終提供自己的預設建構函式)不同,如果滿足您的需要,可以使用預設的拷貝建構函式。 一個有趣的注意事項:您已經看到了一些過載operator <<的例子,我們可以訪問引數f1的私有成員,因為該函式是Fraction類的友元函式。類似地,類的成員函式可以訪問相同類型別的引數的私有成員。由於我們的Fraction複製建構函式接受類型別的引數(以複製),我們能夠直接訪問引數fraction的成員,即使它不是隱式物件。

防止複製

我們可以通過將拷貝建構函式設為私有來防止產生的類的副本:

#include <cassert>
#include <iostream>
 
class Fraction
{
private:
    int m_numerator;
    int m_denominator;
 
    // 拷貝建構函式(私有)
    Fraction(const Fraction &fraction) :
        m_numerator(fraction.m_numerator), m_denominator(fraction.m_denominator)
    {
        // 這裡不需要檢查0的分母,因為副本必須已經是有效的分數
        std::cout << "Copy constructor called\n"; // 僅僅證明它在工作
    }
 
public:
    // 預設建構函式
    Fraction(int numerator=0, int denominator=1) :
        m_numerator(numerator), m_denominator(denominator)
    {
        assert(denominator != 0);
    }
 
    friend std::ostream& operator<<(std::ostream& out, const Fraction &f1);
};
 
std::ostream& operator<<(std::ostream& out, const Fraction &f1)
{
	out << f1.m_numerator << "/" << f1.m_denominator;
	return out;
}
 
int main()
{
	Fraction fiveThirds(5, 3); // 直接初始化一個Fraction,呼叫Fraction(int,int)建構函式
	Fraction fCopy(fiveThirds); // 拷貝建構函式是私有的,在這一行編譯錯誤
	std::cout << fCopy << '\n';
}

現在,當我們嘗試編譯程式時,我們將得到編譯錯誤,因為fCopy需要使用拷貝建構函式,但由於拷貝建構函式已宣告為私有,因此無法看到它。

可以省略拷貝建構函式

現在考慮以下示例:

#include <cassert>
#include <iostream>
 
class Fraction
{
private:
	int m_numerator;
	int m_denominator;
 
public:
    // 預設建構函式
    Fraction(int numerator=0, int denominator=1) :
        m_numerator(numerator), m_denominator(denominator)
    {
        assert(denominator != 0);
    }
 
        // 拷貝建構函式
	Fraction(const Fraction &fraction) :
		m_numerator(fraction.m_numerator), m_denominator(fraction.m_denominator)
	{
		// 這裡不需要檢查0的分母,因為副本必須已經是有效的分數
		std::cout << "Copy constructor called\n"; // just to prove it works
	}
 
	friend std::ostream& operator<<(std::ostream& out, const Fraction &f1);
};
 
std::ostream& operator<<(std::ostream& out, const Fraction &f1)
{
	out << f1.m_numerator << "/" << f1.m_denominator;
	return out;
}
 
int main()
{
	Fraction fiveThirds(Fraction(5, 3));
	std::cout << fiveThirds;
	return 0;
}

考慮一下該程式的工作原理 首先,我們使用Fraction(int,int)建構函式直接初始化一個匿名Fraction物件。然後我們使用該匿名Fraction物件作為Fraction fiveThirds的初始化器。由於匿名物件是一個Fraction,這應該呼叫拷貝建構函式,對嗎?

執行它並自己編譯。你可能希望得到這個結果(你可以):

Copy constructor called 5/3 但實際上,你更有可能(但不能保證)得到這個結果: 5/3 為什麼我們的複製建構函式沒有被呼叫?

請注意,初始化匿名物件然後使用該物件來指導初始化我們定義的物件需要兩個步驟(一個用於建立匿名物件,一個用於呼叫拷貝建構函式)。但是,最終結果基本上與僅進行直接初始化相同,只需要一步。

因此,在這種情況下,允許編譯器選擇不呼叫拷貝建構函式,而只是進行直接初始化。這個過程叫做elision。

所以儘管你寫道:

Fraction fiveThirds(Fraction(5, 3));

編譯器可能會將其更改為:

Fraction fiveThirds(5, 3);

它只需要一個建構函式呼叫(到Fraction(int,int))。請注意,在使用elision的情況下,拷貝建構函式體中的任何語句都不會執行,即使它們會產生其他作用作用(如列印到螢幕上)!

最後,請注意,如果您將拷貝建構函式設為private,那麼使用拷貝建構函式的任何初始化都將導致編譯錯誤,即使拷貝建構函式被省略了!