B b;(1)
A a=b;(2)
A a=B();(3)
A a(b);(4)
如果(2)能執行成功的話,那么需要滿足什么?(2)到底與(4)有什么區別?(4)能執行成功的條件與(2)有什么區別?
回答完上述問題, 我順便還能回答一個問題: A(A& a)與A(const A& a)有什么區別?
下面就來回答這個問題:
首先來分析(2)是如何執行的,分為以下步驟:
1. b能轉化為一個A的臨時對象;(轉化的方法可以是B自身包含一個到A的強制類型轉換操作符或者A有一個未指定explicit的參數為B或者B&或者const B&的構造函數
2. 調用A的拷貝構造函數, 而且必須是A(const A& a), 如果沒有const是不行的, 至于原因我一會兒再說
再說說(3)執行的步驟與條件:
1. B的這個臨時對象能轉化為一個A的臨時對象; (轉化的方法可以是B自身包含一個到A的強制類型轉換操作符或者A有一個未指定explicit的參數必須為B或者const B&的構造函數
2. 調用A的拷貝構造函數, 而且必須是A(const A& a), 如果沒有const是不行的, 至于原因我一會兒再說
最后說說(4)執行的條件:
A有參數為B或者B&或者const B&的構造函數;
或者是: B自身支持強制類型轉化為某種類型, 該類型能恰好為A的某一個構造函數的參數.
按照我上面的分析, 對于(2)和(3)如果B自身不提供轉化機制的話, 那么豈不是要調用A的構造函數兩次了, 其實最后只調用一次就可以了, 也就是步驟2里調用A的拷貝構造函數不會被調用, 但這只是編譯器作了優化, 省掉了一次, 并不代表只執行步驟1, 不信的話你把A拷貝構造函數里的const去掉試試看, 要是能編譯過去才怪.
那說到根兒上, 加不加const修飾到底有啥區別呢?
其實這才是關鍵所在, 理解了這個也就知道本質原因了, 下面我就來說說這個const:
首先要讓大家知道A(const A&)和A(A&)是兩個函數吧?也就是構造函數的重載, 對于重載的函數參數類型或個數至少得有點不同, 這里我告訴你參數類型加不加const是完全不同的兩種參數類型.
知道了上面的概念, 再引用一段話:(出自http://www.xmsc.com.cn/InfoView/Article_122425.html)
說說左值和右值以及臨時對象的問題。也許你會說左值應該就是能夠改變的變量,右值當然就是不能改變的變量嘍!對嗎?對了一點點,實際上左值是能夠被引用的變量,說的通俗點就是有名字的變量,你一定想到了些什么,對了,臨時變量就沒有名字,即使有你也不會知道,因為它不是由你創建的,編譯器會在內部辨別它,但你并不知道,因此臨時變量不是左值,而是右值。你也許還會問,那const變量是不是左值呢?根據定義,它有名字,當然就是左值了。因此左值并非一定“ 可被修改”。但是左值和右值與參數有什么關系嗎?我要告訴你的是:有,而且相當密切,因為標準c++規定:若傳遞給類型為引用的形參的實參是右值的話必須保證形參為const引用。
其實上面這段話最關鍵的地方在于指出了左值能被引用右值不能被引用, 臨時變量以及沒有顯示名字的變量都是右值, 因此在(2)或(3)中生成的A的臨時變量必定是右值, 而A的構造函數必須為引用類型, 因此按照"若傳遞給類型為引用的形參的實參是右值的話必須保證形參為const引用"這個斷言, A的拷貝構造函數里包含const也就是必須的了.
所以大家要消除一個固有的概念: 拷貝構造函數就得A(const A&)這么寫, 完全錯誤, 其實A(A&)有些情況下是必須有的, 沒有都不行, 比如auto_ptr, 因為它在調用拷貝構造函數時, 需要把自己當前維護的那根指針銷毀掉(即設為NULL)同時把這根指針交給另一個auto_ptr保管.
下面的代碼是我自己實驗的一個小例子:
using namespace std;
template <typename T>
class A
{
public:
A(){}
A(A& a)
{
cout<<"A(A& a)"<<endl;
}
template <typename T2>
A(const A<T2>& a)
{
cout<<typeid(T).name()<<endl;
cout<<typeid(T2).name()<<endl;
cout<<"A(const A<T2>&a)"<<endl;
}
};
int main(int argc, char* argv[])
{
A<int> a_int;
A<string> a=(a_int);
return 0;
}
輸出結果是:
Ss
i
A(A<T2>&a)
Ss
Ss
A(A<T2>&a)
如果把A的兩個拷貝構造函數都加上const,或者把模板拷貝構造函數的那個const移到非模板拷貝構造函數上, 結果都會為:
Ss
i
A(A<T2>&a)
上述代碼是在linux的gcc3.4下通過的, 我相信大家應該可以分析出原因來, 不過對于模板類我還是得羅唆幾句, 首先大家應該清楚A<int>和A<string>是完全不同的兩個類型, 就像int和string一樣風馬牛不相及, 只不過它們公用了一些模板代碼而已, 因此你把A<int>和A<string>看作兩個不同的類型來分析就應該能分析透徹了.
在http://www.xmsc.com.cn/InfoView/Article_122425.html一文中還講解了auto_ptr中auto_ptr_ref的作用, 說到這里,讓我真的很恨auto_ptr源碼中的注釋啊,因為它的注釋有錯誤,一直誤導了我, 現在才體會到“錯誤的注釋還不如沒有注釋”的真諦了。錯誤出現在:
* auto_ptr<Derived> func_returning_auto_ptr(.....);
* ...
* auto_ptr<Base> ptr = func_returning_auto_ptr(.....);
其實應該是:
* auto_ptr<Derived> func_returning_auto_ptr(.....);
* ...
* auto_ptr<Base> ptr(func_returning_auto_ptr(.....)); or auto_ptr<Derived> ptr = func_returning_auto_ptr ;
不要小看這點改動,上面的那種方案是編譯不過去的。因為上面的方案里需要轉換兩次再調用一次拷貝構造函數,這違背了C++最多轉換一次的限制。
最后, 或許你在VC下實驗的結果會和我說的不一樣, 這是因為VC在臨時對象這一點上對標準C++的支持不夠好,用臨時對象作參數的時候不加const也可以編譯通過.