首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C++ >> 關於C++

C++ 空類別預設成員函數完整解析與深淺複製陷阱

     編輯:關於C++

一、編譯器自動生成的六大成員函數

對於未顯式宣告特定函數的類別,編譯器會隱式生成以下成員函數:

class Empty {
public:
    Empty();                    // 預設建構函數
    Empty(const Empty&);        // 複製建構函數
    ~Empty();                   // 解構函數
    Empty& operator=(const Empty&); // 複製賦值運算子
    Empty* operator&();         // 取址運算子 (non-const)
    const Empty* operator&() const; // 取址運算子 (const)
};

二、淺複製導致的資源管理災難

案例:動態記憶體成員的類別

class StringBad {
private:
    char* str;
    int len;
public:
    StringBad(const char* s);
    StringBad();
    ~StringBad();
};

StringBad::StringBad(const char* s) {
    len = std::strlen(s);
    str = new char[len + 1];
    std::strcpy(str, s);
}
StringBad::StringBad() { len = 4; str = new char[4]; }
StringBad::~StringBad() { delete[] str; }

複製建構陷阱

執行 StringBad sailor = sports; 時,編譯器生成的隱式複製建構函數執行「成員逐一複製」(淺複製):

sailor.str = sports.str;  // 僅複製指標值!

導致兩物件指向同一記憶體區塊。先被解構的物件釋放記憶體,另一物件的指標成為懸空指標,後續存取導致未定義行為。

賦值運算子同樣危險

knot = headline1; 呼叫隱式賦值運算子,同樣僅複製指標,造成雙重釋放或記憶體洩漏。

三、解決之道:深度複製

凡類別含 new 初始化的指標成員,必須顯式定義:

  1. 複製建構函數
  2. 複製賦值運算子

兩者皆需實作深度複製:配置新記憶體並複製資料內容,而非指標值。並遵循「三大法則」(C++98)或「五大法則」(C++11 含移動語意)。

四、複製建構函數辨識與多重載規則

判斷標準

類別 X 的建構函數若第一參數為下列之一,且無其他參數或其餘參數有預設值,即為複製建構函數:

  • X&
  • const X&
  • volatile X&
  • const volatile X&
X::X(const X&);        // 是
X::X(X&, int = 1);     // 是(第二參數有預設值)
X::X(X);                 // 否(傳值參數會無限遞迴)
X::X(X&, int, int=2);  // 否(第二參數無預設值)

多重載複製建構函數

類別可同時擁有 X(const X&) 與 X(X&)。但若僅提供 X(X&),則無法以 const X 或 volatile X 物件進行複製初始化。

五、編譯器生成的預設複製建構函數行為

  • 執行順序:先基底類別,再衍生類別成員。
  • 每個資料成員執行「成員複製」:
    • 類別型別成員 → 呼叫其複製建構函數
    • 陣列成員 → 逐元素位元複製
    • 標量型別 (int, double 等) → 內建賦值

六、成員函數樣板不會抑制編譯器生成複製函數

struct X {
    template<typename T> X(const T&);      // 非複製建構函數
    template<typename T> X& operator=(const T&); // 非複製賦值
};

原因:樣板不改變語言規則。當程式需要複製建構函數而使用者未宣告,編譯器依然會自動生成。此規則同樣適用於賦值運算子。參考《Effective C++ 3/e》條款 45。

七、測試題解析

#include <iostream>
#include <string>

struct X {
    template<typename T>
    X(T&) { std::cout << "This is ctor." << std::endl; }

    template<typename T>
    X& operator=(T&) { std::cout << "This is ctor." << std::endl; }
};

void main() {
    X a(5);        // 呼叫樣板建構函數
    X b(10.5);     // 呼叫樣板建構函數
    X c = a;       // 呼叫編譯器生成的複製建構函數(非樣板!)
    c = b;         // 呼叫編譯器生成的複製賦值運算子(非樣板!)
}

輸出結果:

This is ctor.   // a(5)
This is ctor.   // b(10.5)
This is ctor.   // c = a (編譯器生成的複製建構函數預設輸出相同訊息?實際上不會輸出此訊息)
This is ctor.   // c = b (編譯器生成的複製賦值運算子同理)

修正說明:編譯器生成的複製建構函數與賦值運算子不會輸出 "This is ctor."。實際輸出僅前兩行。樣板函數不會被選為複製操作,編譯器會另行合成正確簽名的函數。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved