C 基礎教程物件導向(學習筆記(33))

2021-08-29 18:44:23 字數 3957 閱讀 5962

淺層拷貝

因為c ++對您的類知之甚少,所以它提供的預設拷貝建構函式和預設賦值運算子使用稱為成員拷貝的複製方法(也稱為淺層複製)。這意味著c ++單獨複製類的每個成員(使用賦值運算子過載operator =,並直接初始化複製建構函式)。當類很簡單時(例如,不包含任何動態分配的記憶體),這非常有效。

例如,讓我們來看看我們的fraction類:

#include #include class fraction

friend std::ostream& operator<<(std::ostream& out, const fraction &f1);};

std::ostream& operator<<(std::ostream& out, const fraction &f1)

編譯器為此類提供的預設複製建構函式和賦值運算子如下所示:

#include #include class fraction

// 拷貝建構函式

fraction(const fraction &f) :

m_numerator(f.m_numerator), m_denominator(f.m_denominator)

fraction& operator= (const fraction &fraction);

friend std::ostream& operator<<(std::ostream& out, const fraction &f1);};

std::ostream& operator<<(std::ostream& out, const fraction &f1)

// 更好的實現operator=

fraction& fraction::operator= (const fraction &fraction)

請注意,因為這些預設運算子適用於拷貝此類,所以在這種情況下,沒有理由編寫這些函式的自己運算子。

但是,在設計處理動態分配記憶體的類時,成員(淺)複製可能會給我們帶來很多麻煩!這是因為指標的淺拷貝只是複製指標的位址 - 它不會分配任何記憶體或複製指向的內容!

我們來看乙個這樣的例子:

#include // for strlen()

#include // for assert()

class mystring

~mystring() // 銷毀

char* getstring()

int getlength()

};

上面是乙個簡單的字串類,它分配記憶體來儲存我們傳入的字串。請注意,我們還沒有定義拷貝建構函式或過載賦值運算子。因此,c ++將提供乙個預設的拷貝建構函式和預設賦值運算子,它們執行淺拷貝。拷貝建構函式看起來像這樣:

mystring::mystring(const mystring &source) :

m_length(source.m_length), m_data(source.m_data)

請注意,m_data只是source.m_data的淺指標副本,這意味著它們現在都指向同乙個東西。

現在,請考慮以下**段:

int main()

//copy是乙個區域性變數,所以它在這裡被銷毀。析構函式刪除了copy的字串,它用乙個懸空指標留下hello

std::cout << hello.getstring() << '\n'; // 這將有不確定的行為

return 0;

}

雖然這段**看起來無害,但它包含乙個潛在的問題,會導致程式崩潰!你能發現它嗎?不要擔心,如果你不能,它是相當微妙的。

讓我們逐行分解這個例子:

mystring hello("hello, world!");
這行**是無害的。這將呼叫mystring建構函式,該建構函式分配一些記憶體,將hello.m_data設定為指向它,然後將字串「hello,world!」複製到其中。

mystring copy = hello; // 使用預設拷貝建構函式
這行**似乎也無害,但它實際上是我們問題的根源!在評估此行時,c ++將使用預設的拷貝建構函式(因為我們沒有自己提供)。此複製建構函式將執行淺複製,將copy.m_data初始化為hello.m_data的相同位址。結果,copy.m_data和hello.m_data現在都指向同一塊記憶體!

} // 副本在這裡被銷毀
當副本超出範圍時,將在複製時呼叫mystring析構函式。析構函式刪除copy.m_data和hello.m_data指向的動態分配的記憶體!因此,通過刪除副本,我們也(無意中)影響了hello。變數副本然後被銷毀,但hello.m_data指向已刪除(無效)的記憶體!

std::cout << hello.getstring() << '\n'; // 這將有不確定的行為
現在你可以看到為什麼這個程式有未定義的行為。我們刪除了hello指向的字串,現在我們正在嘗試列印不再分配的記憶體值。

這個問題的根源是拷貝建構函式完成的淺複製 - 在複製建構函式中執行指標值的淺複製或過載賦值運算子幾乎總是要求麻煩。

深度複製

這個問題的乙個答案是對正在複製的任何非空指標進行深層複製。乙個深拷貝分配的副本儲存,然後複製的實際內容,因此複製住在從源不同的儲存器。這樣,副本和源是不同的,不會以任何方式相互影響。執行深層複製需要我們編寫自己的拷貝建構函式和過載賦值運算子。

讓我們繼續說明如何為mystring類完成此操作:

// 拷貝建構函式

mystring::mystring(const mystring& source)

else

m_data = 0;

}

正如您所看到的,這比簡單的淺拷貝更複雜!首先,我們必須檢查以確保源甚至有乙個字串(第8行)。如果是,那麼我們分配足夠的記憶體來儲存該字串的副本(第11行)。最後,我們必須手動複製字串(第14和15行)。

現在讓我們來做過載賦值運算子。過載的賦值運算子稍微複雜一些:

// 賦值操作符

mystring& mystring::operator=(const mystring & source)

else

m_data = 0;

return *this;

}

請注意,我們的賦值運算子與我們的拷貝建構函式非常相似,

但有三個主要區別:

#我們新增了自我分配檢查。

#我們返回*這樣我們可以鏈結賦值運算子。

#我們需要顯式釋放字串已經存在的任何值(因此我們在以後重新分配m_data時沒有記憶體洩漏)。

當呼叫過載賦值運算子時,分配給的項可能已包含先前的值,我們需要確保在為新值分配記憶體之前清理它們。對於非動態分配的變數(固定大小),我們不必費心,因為新值只會覆蓋舊值。但是,對於動態分配的變數,我們需要在分配任何新記憶體之前顯式釋放任何舊記憶體。如果我們不這樣做,**就會崩潰,但是每次我們完成一項任務時,我們都會有記憶體洩漏,這會洩漏我們的空閒記憶體!

更好的解決方案

處理動態記憶體的標準庫中的類(如std :: string和std :: vector)處理所有記憶體管理,並且過載了拷貝建構函式和賦值操作符,這些操作符可以進行適當的深度複製。因此,您可以像普通的基本變數一樣初始化或分配它們,而不是自己進行記憶體管理!這使得這些類更易於使用,更不容易出錯,而且您不必花時間編寫自己的過載函式!

summary:

預設的拷貝建構函式和預設賦值運算子執行淺拷貝,這對於不包含動態分配的變數的類很好。

具有動態分配變數的類需要具有執行深層複製的拷貝建構函式和賦值運算子。

支援使用標準庫中的類而不是自己進行記憶體管理。

C 基礎教程物件導向(學習筆記5(2))

在編寫具有多個建構函式的類 大多數建構函式 時,必須為每個建構函式中的所有成員指定預設值會導致冗餘 如果更新成員的預設值,則需要觸控每個建構函式。從c 11開始,可以直接為普通類成員變數 不使用static關鍵字的變數 提供預設初始化值 class rectangle void print int ...

C 基礎教程物件導向(學習筆記(23))

過載一元運算子 與您目前看到的運算子不同,正 負 和邏輯非 運算子都是一元運算子,這意味著它們只能在乙個運算元上執行。因為它們僅對它們所應用的物件進行操作,所以通常將一元運算子過載實現為成員函式。所有三個運算元都以相同的方式實現。讓我們看一下我們如何在前面的例子中使用的cents類上實現operat...

C 基礎教程物件導向(學習筆記(24))

過載比較運算子相對簡單,因為它們遵循我們在過載其他運算子時看到的相同模式。因為比較運算子都是不修改左運算元的二元運算子,所以我們將使過載的比較運算子宣告為友元函式。這是乙個帶有過載運算子 和operator!的car類的示例。include include class car friend bool...