重載賦值運算子
來源:互聯網
上載者:User
重載賦值運算子 賦值運算子可能是最容易令人迷惑的一個,所以,重載它必須十分的小心。 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)。