C++:賦值運算符重載

賦值運算符可能是最容易令人迷惑的一個,所以,重載它必須十分的小心。
1. 值運算符僅能重載爲成員函數。
C++不允許賦值運算符被重載爲全局形式,這是因爲如果可以寫出全局形式的賦值運算符函數的話,我們可以寫出這樣的函數:
 iint operator=(int a, integer b);
從而出現這樣無法無天的語句:
integer a(3);
2 = a;//God save me
2. 注意自賦值的情況
現在我們寫一個簡單的integer類並重載賦值運算符。
 class integer
{
int i;
public:
integer(int j):i(j){};
    integer& operator=(const integer& a)
{
    i = a.i;
    return *this;//就是這個了,每次都忘。
   };
 };
嗯,不錯。但,且慢,你沒有考慮自賦值的情況。啊,有必要嗎?的確,在這個例子中找不到檢測自賦值的理由,但請看下面這個例子:
class CA
{
public:
char* p;
   CA(){p = NULL;};
void Set(char* pStr)
{
    delete []p;
if(pStr == NULL)
{
         p = NULL;
}
else
{
p = new char[strlen(pStr)+1];
        strcpy(p, pStr);
}
 };
    CA& operator=(CA& a)
{
cout<<” operator = invoked/n”<<endl;
    //沒有檢測自賦值情況
    delete []p;
    p = a.p;
   a.p = NULL;
   return *this;
};
    ~CA(){delete []p;};
}; 
CA對象“擁有”它成員p指向的內存。所以,在賦值函數中,參數a將放棄 它的“擁有權”,並將它轉交給調用對象。(C++標誌庫中定義的智能指針auto_ptr就是一種“擁有”型智能指針,它也存在這種“擁有權轉移”的性質)
請見下面的例子代碼(例子代碼1):
CA a1, a2;
a1.Set(“Ok”);
a2 = a1;
我們的函數看起來工作的很好,但是,請看下面一條語句:
a2 = a2;// 悲劇發生了,a2“擁有”的內存被釋放了!
所以,賦值運算符函數應寫爲下面的形式:
CA& CA::operator=(CA& a)
{
   cout<<” operator = invoked/n”<<endl;
//檢測自賦值情況
if(this != &a)
{
delete []p;
        p = a.p;
    a.p = NULL;
}
return *this;
};
正因爲在自賦值的情況下可能給對象造成傷害,所以在重載賦值運算符時必須要注意自賦值的情況。所謂習慣成自然,如果我們養成良好的習慣,我們就會避免犯種種錯誤。
 
所以integer類中的賦值運算符函數應寫成這樣:
integer& integer::operator=(const integer& a)
{  
    if(this != &a)
    i = a.i;
return *this;
};
 
3.爲什麼賦值運算符沒有調用?
 
現在,我們的CA類擁有一個“完美”的賦值運算符函數,現在讓我們坐下來,寫下這樣一段代碼(例子代碼2),並等着它打印出operator = invoked:
CA a1;
a1.Set(” Ok”);
CA a2 = a1;
可是……
天哪,我們的程序崩潰了。
調試證明,這段代碼根本沒有調用賦值運算符函數,why?
如果你仔細地檢查例子代碼1和2,你會發現他們之間的差別僅僅在於: 代碼2中a2定義時就被初始化爲a1的值……
等等,你想到什麼了嗎,沒錯,就是它:拷貝構造函數。C++保證對象都會被初始化,所以CA a2 = a1;不會調用賦值運算符 而是 會調用拷貝構造函數。因爲類中沒有定義拷貝構造函數,所以編譯器就會生成一個缺省的拷貝構造函數。而這個函數僅僅是簡單的bitcopy,a1“擁有”的內存並沒有轉交給a2,這樣,那塊內存被兩個對象所“擁有”,當對象析構時,它被delete了兩次,於是悲劇發生了。
所以,我們需要定義自己的拷貝構造函數:
class CA
{
public:
    CA(CA& a)
   {
        cout<<"copy constructor"<<endl;
        p = a.p;
        a.p = NULL;
    }
……
};
因爲函數中將改變參數,所以參數不能定義爲const的。
現在無論執行代碼1還是代碼2都不會有什麼問題。
在這部分結束之前,我再問你一個問題:如果我們將賦值運算符函數的返回值類型由CA& 改爲 CA 會發生什麼呢?
好,讓我們執行例子代碼1看看結果:
operator= invoked
copy constructor //這一句話怎麼來的
嗯,沒錯,賦值運算符函數被調用了。但,爲什麼還會調用拷貝構造函數呢?
將代碼1中的a2 = a1;用函數形式代替,將幫助我們找到答案:
a2 = a1; 相當於a2.operator=(a1); 而函數operator=將返回一個CA對象,於是編譯器產生一個臨時變量,並調用拷貝構造函數對它進行初始化。而隨後,這個對象被摧毀,析構函數被調用。現在,讓我們給CA的構造和析構函數都加上打印語句,我們就會清楚的看到這個過程。
class CA
{
public:
int *p;
CA()
{
cout<<"constructor"<<endl;
    p = NULL;
};
 
~CA()
{
    cout<<"destructor"<<endl;
    delete []p;
};
……
};
執行例子代碼1,結果爲:
constructor           //a1的構造函數
constructor           //a2的構造函數
operator= invoked     //賦值語句
copy constructor      //臨時變量的拷貝構造函數
destructor            //臨時變量被析構
destructor            //a2被析構
destructor           //a1被析構
臨時變量產生、調用拷貝構造函數、然後被析構,不知你是否清楚的意識道這到底意味着什麼:當我們調用a2 = a1;時,a1“擁有”的內存被轉交給a2,然後又被轉交給了那個臨時變量,最後,當臨時變量析構時被釋放!這當然不是我們想要的,還是乖乖的把賦值運算符函數的返回值類型定義爲CA&吧。
  
4.     自動創建的賦值運算符
現在想一下,如果我們不定義CA中的賦值運算符會發生什麼事,難道例子代碼1中的 a2 = a1會引起一個編譯錯誤嗎?當然不會,編譯器將爲我們自動創建一個。這個運算符行爲模仿自動創建的拷貝構造函數:如果類包含對象(或是從其他類繼承下來的),對應這些對象,運算符‘=’被遞歸調用,這稱爲成員賦值。對於這一問題的詳細討論,請見《C++編程思想》第一版,第11章(p225)。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章