C 拷貝控制

2021-12-29 19:57:13 字數 4011 閱讀 1862

c++作為高階語言,物件導向程式設計是其重要的語言特性。設計好的架構,其基礎也是類的設計。我們之前已經將類本身的知識梳理了一遍。這一章著重介紹類控制,包括拷貝控制、過載、物件導向設計以及模板和泛型程式設計。這些非常非常重要,是實現工程必須要掌握的基礎知識。要打起十分的精神來學習。

按照c++ primer的順序,我們從拷貝控制說起。

這章看的時間有點久,有些東西很陌生。可能自己接觸的實際工程太少,有些經驗的東西還體會不到。不過也不影響學習。至少有了前車之鑑,以後走過的坑會少點。

c++是一門很細緻的語言。物件的生命週期可能會經過初始化、拷貝、賦值、釋放一系列的過程。c++統統可以對其進行控制。在類的基礎知識那一章著重分析了類的初始化,也就是類的建構函式。這一章主要討論的是如何控制物件拷貝、賦值、移動和銷毀。

類通過特殊的成員函式來控制這些操作。

拷貝 —- 拷貝建構函式(初始化) 賦值 —- 拷貝賦值運算子 移動 —- 移動建構函式(初始化)、移動賦值運算子 銷毀 —- 析構函式注意:

拷貝建構函式和賦值是不一樣的,前面提到過初始化和賦值的區別。拷貝建構函式是在用另乙個物件建立本物件時呼叫,而賦值是在用右值替換左值過程呼叫。

這些統稱為拷貝控制操作。需要指出的是,如果類沒有定義這些操作,編譯器會自動生成預設的操作。但是一些特殊的場景下,使用預設的操作會出現問題。因此,問題的關鍵在於認識在何時需要定義這些操作

這三者操作是最基本的操作。移動操作是新的標準提供的性質,一會深入分析。

1.拷貝建構函式

拷貝建構函式使用場景有三個:

拷貝初始化 類型別按值傳遞 函式返回物件 使用花括號列表初始化乙個陣列的元素或聚合類成員

拷貝建構函式的第乙個引數是自身類型別的引用,且任何其他引數都有預設值。

class foo ;

foo a;

foo b = a; // 拷貝初始化

foo b(a); // 直接初始化拷貝建構函式不應該是explicit,因為拷貝建構函式在幾種情況下會被隱式的使用。

拷貝構造函式引數必須是引用,因為如果不是引用,按值傳遞就必須呼叫拷貝建構函式,如此無限迴圈。

2.拷貝賦值運算子

初始化和賦值是兩個不同的操作,我們反覆在強調這一點。這裡是過載賦值運算子,以控制同型別之間的物件賦值。

場景

sales_data trans, accum;

trans = accum; // 使用sales_data的拷貝賦值運算子過載運算子本質上是函式。

class foo ;

foo& foo::operator= (const foo &rhs) 3.析構函式

析構函式是在物件銷毀之前呼叫的函式。和建構函式執行相反的操作,這個相反在全方位。比如建構函式先執行初始化列表(按照資料成員定義順序),然後執行函式體。析構過程先執行函式體,然後銷毀資料成員(按照資料成員定義的逆序)。

析構函式不接受引數,不返回值。

class foo ;場景

變數離開作用域 物件被銷毀時 容器(陣列、vector等)被銷毀時 delete顯示銷毀時 臨時物件,建立的表示式結束時注意:

當指向乙個物件的引用或者指標離開作用域時,析構函式不會執行

4.三/五法則

何時定義拷貝控制操作,有幾條原則可尋。

需要析構函式的類也需要拷貝和賦值操作這是因為,需要析構函式,常常伴隨動態記憶體管理。而使用編譯器合成的操作,往往是淺拷貝。可能會析構多次,導致未定義的錯誤。因此,幾乎肯定需要拷貝和賦值操作

需要拷貝操作的類也需要賦值操作,反之亦然

5.使用合成版本

使用=default 顯示地要求編譯器生成合成的版本。

6.阻止拷貝

在某些場景下,需要禁止拷貝操作。比如iostream類,阻止拷貝,以避免多個物件讀寫同乙個io緩衝,導致資料不一致。

實現拷貝阻止有兩種方式:

定義刪除的函式(新標準)struct nocopy ;注意:

析構函式不能是刪除的成員,可以是刪除,但是一旦定義為刪除的,就不能定義這種型別的變數或成員,但是可以動態分配物件,卻又無法釋放物件。

private 拷貝控制通過將拷貝建構函式和拷貝賦值函式宣告成private 來阻止拷貝

class privatecopy ;注意:

雖然宣告成private,但是友元類和友元函式還是可以拷貝,因此,為了阻止友元類和友元函式拷貝,我們將這些拷貝成員宣告為private,但是不定義它們。這樣友元類和友元函式如果拷貝,會發生鏈結錯誤。

拷貝的過程,是先分配新的空間,然後向新的空間裡賦值。大部分情況下,物件拷貝之後會立即銷毀。如果使用移動而非拷貝物件,效能將會大幅度的提公升。新標準就提供了移動的新特性。

1.右值引用

為了支援移動操作,新標準引入了右值引用,非常triky。

我們之前提到的引用都是左值引用,即繫結到左值的引用。右值引用顧名思義,繫結到右值的引用。使用的方法是&&而非&。

右值引用乙個非常重要的特性是—–只能繫結到乙個將要銷毀的物件(臨時物件),因此,我們可以自由的將右值引用的資源」移動」到另乙個物件中。好好體會一下。

int i = 42;

int &r = i; // 左值引用

int &&rr = i; // 錯誤,不能將右值引用繫結到乙個左值

int &r2 = i * 42; // 錯誤,不能將乙個左值引用繫結到乙個右值

const int &r3 = i * 42; // 正確,const引用繫結右值

int &&rr2 = i * 42; // 正確,右值引用繫結右值左值持久,右值短暫(字面值、臨時物件)

但是我們說了,右值引用這個性質,可以讓我們自由的接管所引用的物件的資源。

int &&rr1 = 42; // 正確,繫結字面值

int &&rr2 = rr1; // 錯誤,rr1表示式是乙個左值雖然我們不可以將乙個右值直接繫結到左值上,但是我們可以顯示的將乙個左值轉換為右值引用

#include

int &&rr3 = std::move(rr1); // ok呼叫了move意味著承諾:除了對rr1賦值或者銷毀它以外,我們將不再使用它(不再使用它的值)。

2.移動建構函式、移動賦值運算子

移動操作,表示從給定物件「竊取」資源,而不是拷貝資源。

strvec::strvec(strvec &&s) noexcept // 移動操作不應該丟擲任何異常

// 成員初始化接管 s 中的資源

: element(s.element), first_free(s.first_free), cap(s.cap)

移動賦值運算子必須要正確處理自賦值(賦值運算子都需要考慮這一點)

strvec &strvec::operator=(strvec &&rhs) noexcept

}注意:

移源後物件必須可析構,另外,移動建構函式預設不能由編譯器合成,但是如果類中的每個非static資料成員都是可移動的,編譯器就可以為它合成移動建構函式或移動賦值運算子。其中,內建型別可以移動,string定義了自己的移動操作。

如果乙個類有乙個可用的拷貝建構函式而沒有移動建構函式,則其物件是通過拷貝建構函式來」移動」的。

class hasptr

// 賦值運算子既是移動賦值運算子,也是拷貝賦值運算子

hasptr &operator=(hasptr rhs)

};hp = hp2; // hp2 是乙個左值,使用拷貝建構函式來拷貝

hp = std::move(hp2); // 移動建構函式移動hp2最後建議:不要隨意使用移動操作,因為移後源物件具有不確定的狀態,對其呼叫std::move是危險的。當我們呼叫move函式時,必須絕對確認移後源物件沒有其他的使用者。

C 拷貝控制

本文主要討論c 類定義中的拷貝控制 copy control 拷貝建構函式 賦值操作符和析構函式。如果文中有錯誤或遺漏之處,敬請指出,謝謝!c 類中有四個不可或缺的部分,那就是建構函式 拷貝建構函式 賦值操作符和析構函式。如果類中沒有定義這些函式,那麼編譯器將為類自動生成這些函式。當然,你也可以通過...

C 拷貝控制

當定義乙個類時,顯示或隱式地指定了此型別的物件在拷貝 賦值和銷毀時所執行的操作,通過三個特殊的成員函式來控制這些操作,分別是拷貝建構函式,賦值運算子和析構函式。拷貝建構函式定義了當使用同型別的另乙個物件初始化新物件時的操作,賦值運算子定義了將乙個物件賦值給同型別的另乙個物件時的操作,析構函式定義了此...

C 拷貝控制

拷貝建構函式 如果有乙個建構函式第乙個引數是自身類型別的引用,且任何額外引數都有預設值,則此建構函式是拷貝建構函式。class foo 如果沒有為乙個類定義拷貝建構函式,編譯器會為我們定義乙個。拷貝建構函式不僅在我們用 定義變數時發生,也會在一下情況發生 拷貝賦值運算子 與拷貝建構函式一樣,如果類未...