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

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

過載賦值運算子

賦值運算(運算子=)用於從一個物件的值拷貝到另一個已存在的物件。

賦值與拷貝建構函式

拷貝建構函式和賦值運算子的目的幾乎相同 - 將一個物件複製到另一個物件。但是,拷貝建構函式初始化新物件,而賦值運算子則替換現有物件的內容。

拷貝建構函式和賦值運算子之間的區別給新程式設計師帶來了很多困惑,但實際上並不是那麼困難。

總結: #如果必須在複製之前建立新物件,則使用拷貝建構函式(注意:這包括按值傳遞或返回物件)。 #如果在複製發生之前不必建立新物件,則使用賦值運算子。 #過載賦值運算子

過載賦值運算子(operator =)非常簡單,我們將會遇到一個具體的警告。賦值運算子必須作為成員函式過載。

#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 &copy) :
		m_numerator(copy.m_numerator), m_denominator(copy.m_denominator)
	{
		//這裡不需要檢查0的分母,因為副本必須已經是有效的分數
		std::cout << "Copy constructor called\n"; // just to prove it works
	}
 
        // 過載賦值運算子
        Fraction& operator= (const Fraction &fraction);
 
	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;
}
 
// operator =的簡單實現(參見下面的更好實現)
Fraction& Fraction::operator= (const Fraction &fraction)
{
    // do the copy
    m_numerator = fraction.m_numerator;
    m_denominator = fraction.m_denominator;
 
    // 返回現有物件,以便我們可以連結此運算子
    return *this;
}
 
int main()
{
    Fraction fiveThirds(5, 3);
    Fraction f;
    f = fiveThirds; // 呼叫過載賦值運算子
    std::cout << f;
 
    return 0;
}

這列印: 5/3 到目前為止,這一切都應該非常簡單。我們過載的operator =返回* this,這樣我們就可以將多個賦值連結在一起:

int main()
{
    Fraction f1(5,3);
    Fraction f2(7,2);
    Fraction f3(9,5);
 
    f1 = f2 = f3; // 鏈式賦值
 
    return 0;
}

由於自我賦值引起的問題 事情開始變得有趣了。C ++允許自我賦值:

int main()
{
    Fraction f1(5,3);
    f1 = f1; // 自我賦值
 
    return 0;
}

這將呼叫f1.operator =(f1),並且在上面的簡單實現中,所有成員都將被分配給自己。在這個特定的例子中,自我賦值導致每個成員被分配給自己,除了浪費時間之外沒有總體影響。在大多數情況下,自我指派根本不需要做任何事情!

但是,在賦值運算子需要動態分配記憶體的情況下,自我賦值實際上可能很危險:

#include <iostream>
 
class MyString
{
private:
    char *m_data;
    int m_length;
 
public:
    MyString(const char *data="", int length=0) :
        m_length(length)
    {
        if (!length)
            m_data = nullptr;
        else 
            m_data = new char[length];
 
        for (int i=0; i < length; ++i)
            m_data[i] = data[i];
    }
 
    // 過載賦值
    MyString& operator= (const MyString &str);
 
    friend std::ostream& operator<<(std::ostream& out, const MyString &s);
};
 
std::ostream& operator<<(std::ostream& out, const MyString &s)
{
    out << s.m_data;
    return out;
}
 
// 實現一個簡單的operator= (do not use)
MyString& MyString::operator= (const MyString &str)
{
    // 如果當前字串中存在資料,請將其刪除
    if (m_data) delete[] m_data;
 
    m_length = str.m_length;
 
    // 將資料從str複製到隱式物件
    m_data = new char[str.m_length];
 
    for (int i=0; i < str.m_length; ++i)
        m_data[i] = str.m_data[i];
 
    // 返回現有物件,以便我們可以連結此運算子
    return *this;
}
 
int main()
{
    MyString alex("Alex", 5); // Meet Alex
    MyString employee;
    employee = alex; // Alex is our newest employee
    std::cout << employee; // Say your name, employee
 
    return 0;
}

首先,按原樣執行程式。你會看到該程式打印出“Alex”。

現在執行以下程式:

int main()
{
    MyString alex("Alex", 5); // Meet Alex
    alex = alex; 
    std::cout << alex; //  Alex
 
    return 0;
}

你可能會得到垃圾輸出。發生了什麼?

考慮過載operator =當隱式物件和傳入引數(str)都是變數alex時會發生什麼。在這種情況下,m_data與str._m_data相同。首先發生的是函式檢查隱式物件是否已經有一個字串。如果是這樣,它需要刪除它,所以我們最終沒有記憶體洩漏。在這種情況下,分配m_data,因此該函式刪除m_data。但str.m_data指向同一個地址!這意味著str.m_data現在是一個懸空指標。

稍後,我們將新記憶體分配給m_data(和str.m_data)。因此,當我們隨後將str.m_data中的資料複製到m_data時,我們正在複製垃圾,因為str.m_data從未被初始化。

檢測和處理自我分配

幸運的是,我們可以檢測何時發生自我分配。這是對Fraction類的過載operator =的更好實現:

//更好的實現operator=
Fraction& Fraction::operator= (const Fraction &fraction)
{
    // 自我賦值的看門狗
    if (this == &fraction)
        return *this;
 
    // 拷貝
    m_numerator = fraction.m_numerator;
    m_denominator = fraction.m_denominator;
 
    // 返回現有物件,以便我們可以連結此運算子
    return *this;
}

通過檢查我們的隱式物件是否與作為引數傳入的物件相同,我們可以讓我們的賦值運算子立即返回而不做任何其他工作。

請注意,不需要在拷貝建構函式中檢查自我賦值。這是因為複製建構函式僅在構造新物件時被呼叫,並且無法以呼叫拷貝建構函式的方式將新建立的物件分配給自身。

預設賦值運算子

與其他運算子不同,如果您沒有提供運算子,編譯器將為您的類提供預設的公共賦值運算子。此賦值運算子執行成員賦值(這與預設拷貝建構函式的成員初始化基本相同)。

與其他建構函式和運算子一樣,您可以通過將賦值運算子設為私有或使用delete關鍵字來阻止賦值:

#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 &copy) = delete;
 
	// 過載賦值
	Fraction& operator= (const Fraction &fraction) = delete; // no copies through assignment!
 
	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 f;
    f = fiveThirds; // 編譯錯誤,operator =已被刪除
    std::cout << f;
 
    return 0;
}