歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

賦值操作符的異常實現方式

在類的定義中,我們通常會重載賦值操作符,來替代編譯器合成的版本,實現中會對每個類的成員變量進行具體的操作,比如下面的代碼:

class Sales_Item
{
public:
    Sales_Item& operator=(const Sales_Item & rhs);
//other mebers and functions
private:
    char *pIsbn;
    int units_sold;
    double revenue;
};

Sales_Items& Sales_item::operator=(const Sales_Item & rhs)
{
    if(this != &rhs)
    {
        if(pIsbn)
            delete[] pIsbn;
        pIsbn = new char[strlen(rhs.pIsbn)+1];
        strcpy(pIsbn, rhs.pIsbn);
       
        units_sold = rhs.units_sold;
        revenue = rhs.revenue
    }
    return *this;
}

需要先判斷是否為同一個對象,再用形參對象中的成員變量對當前對象成員變量進行賦值。類的成員變量涉及到內存、資源的分配時,需要重載賦值操作符,避免內存、資源的洩露和重復釋放等問題。在某處看到一個重載賦值操作符定義如下:

T& T::operator = (const T& other)
{
    if(this != &other)
    {
        this->~T();
        new (this) T(other);
    }
    return *this;
}

可以看出這個operator=的定義上很簡單,首先調用T類的析構函數,然後使用placement new在原有的地址上,以other為形參,調用T類的拷貝構造函數。在這種慣用法中,拷貝賦值運算符是通過拷貝構造函數實現的,它努力保證T的拷貝賦值運算符和拷貝構造函數完成相同的功能,使程序員無需再兩個不同的地方編寫重復代碼。對於Sales_Item類,如果用這個operator=來代替其原有的實現,盡管不會出錯,但這種定義是一種非常不好的編程風格,它會帶來很多問題:

•它切割了對象。如果T是一個基類,並定義了虛析構函數,那麼"this->~T();new (this) T(other);" 將會出現問題,如果在一個派生類對象上調用這個函數,那麼這些代碼將銷毀派生類對象,並用一個T對象來代替,這幾乎會破壞後面所有試圖使用這個對象的代碼,考慮如下代碼:

//在派生類的賦值運算函數中通常會調用基類的賦值運算函數
Derived& Derived::operator=(const Derived& other)
{
    if(this != &rhs)
    {
        Base::operator=(other);
        //...現在對派生類的成員進行賦值...
    }

    return *this;
}

//本實例中,我們的代碼是
class U : public T{/*...*/};
U& U::operator=(const U& other)
{
    if(this != &rhs)
    {
        T::operator=(other);
        //...對U的成員進行賦值...
        //...但這已經不再是U的對象了,銷毀派生類對象,並在派生類內存建立基類對象
    }

    return *this;    //同樣的問題
}

在U的operator=中,首先調用父類T的operator=,那麼會調用"this->T::~T();",並且隨後再加上對T基類部分進行的placement new操作,對於派生類來說,這只能保證T基類部分被替換。而更重要的是,在T類型的operator=中,虛函數指針會被指定為T類的版本,無法實現動態調用。如果要實現正確的調用,派生類U的operator=需要定義與父類T的operator=中同樣的實現:

U& operator=(const U& rhs)
{
    if(this != &rhs)
    {
        this->~U();
        new(this)U(rhs);
    }

    return *this;
}

它不是異常安全的。在new語句中將調用T的拷貝構造函數。如果在這個構造函數拋出異常,那麼這個函數就不是異常安全的,因為它在最後只銷毀了舊的對象,而沒有用其他對象來代替。

它改變了正常對象的生存期。根本問題在於,這種慣用法改變了構造函數和析構函數的含義。構造過程和析構過程應該與對象生存期的開始/結束對應,而在通常含義下,此時正是獲取/釋放資源的時刻。構造過程和析構過程並不是用來改變對象的值得。

它將破壞派生類。調用"this->T::~T();",這種方法只是對派生類對象中"T"部分(T基類子對象)進行了替換。這種方法違背了C++的基本保證:基類子對象的生存期應該完全包含派生類對象的生存期——也就是說,通常基類子對象的構造要早於派生類對象,而析構要晚於派生類對象。特別是,如果派生類並不知道基類部分被修改了,那麼所有負責管理基類狀態的派生類都將失敗。

測試代碼:

class T
{
public:
    T(const char *pname, int nage)
    {
        name = new char[strlen(pname)+1];
        strcpy_s(name, strlen(pname)+1, pname);
        age = nage;
    }
    T(const T &rhs)
    {
        name = new char[strlen(rhs.name)+1];
        strcpy_s(name, strlen(rhs.name)+1, rhs.name);
        age = rhs.age;
    }
    T& operator=(const T& rhs)
    {
        if(this != &rhs)
        {
            cout<<"T&operator="<<endl;
            this->~T();
            new(this)T(rhs);
        }

        return *this;
    }
    virtual ~T()
    {
        if(name!=NULL)
            delete[] name;
        cout<<"~T()"<<endl;
    }
    virtual void print(ostream& out)const
    {
        out<<"name is "<<name<<", age is "<<age;
    }
private:
    char *name;
    int age;
};

ostream& operator<<(ostream& out, const T&t)
{
    t.print(out);
    return out;
}

class U:public T
{
public:
    U(const char *pname, int nage, const char *prace, int nchampion):T(pname, nage)
    {
        race = new char[strlen(prace)+1];
        strcpy_s(race, strlen(prace)+1, prace);
        champion = nchampion;
    }
    U(const U &rhs):T(rhs)
    {
        race = new char[strlen(rhs.race)+1];
        strcpy_s(race, strlen(rhs.race)+1, rhs.race);
        champion = rhs.champion;
    }
    U& operator=(const U& rhs)
    {
        if(this != &rhs)
        {
        /*    T::operator=(rhs);
            race = new char[strlen(rhs.race)+1];
            strcpy_s(race, strlen(rhs.race)+1, rhs.race);
            champion = rhs.champion;
            */
            this->~U();
            new(this)U(rhs);
        }

        return *this;
    }
    virtual ~U()
    {
        if(race!=NULL)
            delete[] race;
        cout<<"~U()"<<endl;
    }
    virtual void print(ostream& out)const
    {
        T::print(out);
        out<<", race is "<<race<<", champion number is "<<champion<<".";
    }
private:
    char *race;
    int champion;
};
int _tmain(int argc, _TCHAR* argv[])
{
    cout<<sizeof(T)<<"  "<<sizeof(U)<<endl;

    U u("Moon", 21, "Night Elf", 0);
    U t("Grubby", 21, "Orc", 2);

    u = t;
    cout<<u<<endl;

    return 0;
}

在重載operator=運算符時,另一個值得關注的是,用const來修飾返回值:

class T
{
public:
    T(int x=12):value(x){}
    const T& operator=(const T & rhs)
    {
        if(this != &rhs)
        {
            //implement
        }

        return *this;
    }
    int getValue()
    {
        return value;
    }
    void setValue(int x)
    {
        value = x;
    }
public:
    int value;
};

int main()
{
    T t1;
    T t2;
    t2 = t1;
    t2.setValue(21);

    return 0;
}

注意setValue函數改變了t2對象的value值,而line26賦值後,t2仍然可以調用setValue函數,這說明“返回const並不意味著類T本身為const,而只意味著你不能使用返回的引用來直接修改它指向的結構”。看看下面這段代碼:

int main()
{
    T t1;
    T t2;
    (t2=t1).setValue(21);

    return 0;
}

這裡直接對t2=t1的返回結果調用setValue,因為返回的是const&類型,所以不能調用此setValue函數。

Copyright © Linux教程網 All Rights Reserved