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也可以编译通过.