1. 程式人生 > >C++回顧之深淺拷貝、禁止拷貝、空類的預設成員

C++回顧之深淺拷貝、禁止拷貝、空類的預設成員

        個人見解,先談談淺拷貝與深拷貝之間的區別,區分它們的最大區別就是在呼叫完拷貝建構函式後,兩個物件之間是否還存在一定的聯絡,如果兩個物件能夠完全獨立,則說明是深拷貝,否則是淺拷貝。以各種教材的String類的深拷貝實現為例,下面進行說明。

        為了實現深拷貝操作,我們需要自己來構建拷貝建構函式,因為我們一旦構建了自己的拷貝建構函式,系統就不再提供預設的拷貝構造函數了。

        下面是String.h

#ifndef  _STRING_H_
#define _STRING_H_
class String
{
public:
    String(char *str=""); //帶一個引數的建構函式,可傳入引數如“ABC”
    ~String();

    void Display();

    String( const String &other);    //拷貝建構函式
    String & operator=(const String &other);  //等號賦值運算子過載

private:
    char *str_;          //String需要維護str_資料成員
    char *AllocAndCpy(char *str);
};

#endif
        

        下面是String類的具體實現,String.cpp

#include "String.h"
#include <cstring>
#include <iostream>
using namespace std;

char *String::AllocAndCpy(char *str) //實施深拷貝的函式
{
    int len = strlen(str) + 1;
    char *tmp = new char[len];
    memset( tmp, 0, len);
    strncpy(tmp, str, len );
    return tmp;
}

String::String( const String & other) /* : str_(other.str_) 這種方式還是淺拷貝*/
{
        str_ = AllocAndCpy(other.str_); //函式內的實現才是真正的深拷貝,它的處理使得str_與原物件脫離了聯絡
}

String & String::operator=(const String &other)
{
    if( this == &other)
    {
        return *this;
    }

    //銷燬原有空間
    delete [] str_;
    str_ = AllocAndCpy(other.str_);

    return *this;
}

String::String( char *str)
{
    str_ = AllocAndCpy(str);
}

String::~String()
{
    delete [] str_;
}

void String::Display()
{
     cout << str_ << endl;
}
        上面程式碼中的AllocAndCpy函式是非常重要的函式,它重新分配了一塊記憶體空間,然後將原記憶體的資料複製至新記憶體空間中,這樣兩個類物件之間就有各自獨立的記憶體空間,物件之間就沒有聯絡,這就是深拷貝。如果不這麼做,直接通過str_ = other.str_實施拷貝,只是簡單的複製指標的地址,這樣兩個類物件實際指向的是同一塊記憶體,當兩上物件的生命週期結束後,均需要釋放記憶體空間,這樣就造成了同一塊記憶體單元被釋放了兩次。因為它們僅僅拷貝了“形”,而沒有拷貝“本質”的資料,所以拷貝建構函式需要我們自己來重新編寫。由於等號賦值過載函式需要賦值操作,實際上也是複製,所以也需要自己編寫operator=函式。

         下面是測試程式碼,程式碼中也有詳細的註釋:       

int main(void)
{
	String s1("AAA");
	s1.Display();
	
	String s2 = s1;//呼叫預設的拷貝建構函式,系統提供的預設拷貝建構函式實施的是淺拷貝s2.str_ = s1.str_;相同於s1,s2兩個物件的str_指標指向相同的記憶體,當兩個物件生存期結束時,都要呼叫解構函式,導致同一塊記憶體被,釋放了兩次,故而產生錯誤.解決方法實施深拷貝,自己提供拷貝建構函式


    //等號運算子的過載
    String s3;
    s3.Display();
    s3 = s2; // = 號運算子,它呼叫的是系統預設的等號運算子,實施的也是淺拷貝,s3.str_ = s2.str_;仍然會出現同一塊記憶體被銷燬兩次的情況,所以要自己提供等號運算子實施深拷貝。等價於s3.operator=(s2);

    return 0;
}
        

        下面說明關於禁止拷貝的情形。在某些場合,比如設計模式中有個Singleton單例模式,即一個類只能有一個例項,就是說這個類的物件是要獨一無二的,它不允許被拷貝,也不允許賦值,也就是說我們要提供禁止拷貝的功能,以下就是將一個類實施為禁止拷貝的步驟,其實很簡單:

        (1)將拷貝建構函式與operator=(等號運算子過載函式)均宣告為private私有訪問許可權

        (2)在CPP檔案中不提供他們的實現

        這樣,當在主程式進行拷貝複製或賦值操作時,編譯將出錯,因為它們沒有拷貝建構函式或賦值操作的實現。只有宣告,而且還是私有的。

        下面總結一下,空類預設的成員函式有6個:

class Empty{}; //這是一個空類。

Empty(); //預設建構函式
Empty(const Empty &);//預設拷貝構造
~Empty(); //預設解構函式
Empty & operator=(const Empty &)//預設賦值運算子
Empty *operator&();//取地址運算子
const Empty *operator &() const; //取地址運算子const
        下面舉例說明operator&()與operator &() const的用法:
#include <iostream>
using namespace std;

class Empty
{
public:
    Empty * operator&()
    {
        cout << "AAA"<< endl;
        return *this;
    }

    const Empty *operator&() const
    {
        cout << "BBB"<<endl;
        return this;
    }

};

int main()
{
    Empty e;
    Empty *p = &e; //將呼叫取地址運算子,等價於e.operator&()
    
    const Empty e2;
    const Empty *p2 = &e2; //呼叫的是含const的取地址運算子

    //空類的大小是1個位元組,編譯器將為它生成一個字生的空間
    cout << sizoef(Empty) << endl;

    return 0;
}