这篇文章,主要是受Jinhao (辣子鸡丁·GAME就这样OVER了 )在CSDN上一篇题为《有关拷贝构造函数的说法不正确的是》的帖子启发,鸡丁就这四个问题回答如下。
拷贝构造函数的名字和类名是一样的 [错]
类中只有一个拷贝构造函数 [错]
拷贝构造函数可以有多个参数 [对]
拷贝构造函数无任何函数类型 [错]
在这里我不想讨论以上问题的正确与错误,只是讨论一下构造函数,拷贝构造函数,可能还会涉及到赋值函数,析构函数,这些都属于类中的特殊函数。至于以上问题的正误,由读者您来决定。
讨论这些问题前,我们来基本了解一个默认构造函数,拷贝构造函数:
1:关于默认构造函数,拷贝构造函数,析构函数,赋值函数,C++标准中提到
The default constructor , copy constructor and copy assignment operator , and destructor are special member functions. The implementation will implicitly declare these member functions for a class type when the program does not explicitly declare them, except as noted in 12.1. The implementation will implicitly define them if they are used。这段话的意思是说,以上四个函数如果在使用的过程中,发现程序没有显示声明,将隐式执行生成这些声明。所以这里有可能给我们带来一个麻烦就是如果我们声明的默认构造函数,拷贝构造函数,析构函数,赋值函数不正确(非语法层次),那么我们的class一样可以执行以上提到的操作。
2:这些特殊函数遵循一般函数访问规则,比如在类中声明一个保护类型构造函数,只有它的继承类和友元类才可以使用它创建对象。Special member functions obey the usual access rules (clause 11). [Example: declaring a constructor protected ensures that only derived classes and friends can create objects using it.],但是也有其特殊性,不然也不会叫特殊函数,稍后我们逐渐讨论他们的特殊性。
3:关于构造函数的名称,构造函数的名字是否一定要与类名相同?按C++标准说,构造函数是没有名字的。也许你看到这里后会惊讶,但是确实是这样。Constructors do not have names. A special declarator syntax using an optional sequence of functions pecifiers(7.1.2) followed by the constructor’s class name followed by a parameter list is used to declare ordefine the constructor. In such a declaration, optional parentheses around the constructor class name are ignored.。构造函数属于特殊处理函数,它没有函数名称,通过函数名也无法找到它。确认调用构造函数的是通过参数链中参数顺序。
因为构造函数的特殊性,它不同于普通函数的一点是,const,static,vitual, volatile对构造函数的修饰无效
class CAboutConstructer
{
public:
static CAboutConstructer()
{
_iTemp = 1;
cout << "Constuctor" << endl;
}
private:
int _iTemp;
};
如上,如果把构造函数声明为static类型编译器将返回错误:
error C2574: “CAboutConstructer::CAboutConstructer” 构造函数和析构函数不能声明为静态的
同样如果声明为const,编译器将返回错误:C2583 “CAboutConstructer::CAboutConstructer” : “const”“this”指针对于构造函数/析构函数是非法的。
但是构造函数可以是inline,隐式声明的构造函数也是inline类型的。这条规则也适用析构函数,唯一不同就是析构函数可以是virtual,而构造函数不能是virtual的。这里可能涉及到的一个问题是,把析构函数声明为inline virtual或者virtual inline会怎么样?其实这时的析构函数又显示出其一般函数的特性,这时的析构函数和普通函数在对待virtual inline问题是一样的,inline属于编译时刻展开,而virtual是运行时刻绑定。我们的编译器不能做到使我们的程序既有inline带来的速度,又有virtual带来的运行时刻区别。
4:什么情况下需要显示声明构造函数,并不是任何情况都需要显示声明构造函数的,比方说,声明的类不存在虚函数,不存在继承关系或者所有的非静态数据都没有显示声明构造函数,或者所有的非静态数据不需要初始化为特定值,那么这种情况下也没有必要显示声明构造函数。这条规则同样适合拷贝构造函数。
5:拷贝构造函数也是一个构造函数,其第一个参数值必须为type X&,或者type const X&类型,并且没有其他参数或者其他参数都有默认值,我们看C++标准中的一个例子
[Example: X::X(const X&) and X::X(X&, int=1)
are copy constructors.
class X {
// ...
public:
X(int);
X(const X&, int = 1);
};
X a(1); // calls X(int);
X b(a, 0); // calls X(const X&, int);
X c = b; // calls X(const X&, int);
—end example] [Note: all forms of copy constructor may be declared for a class. [Example:
class X {
// ...
public:
X(const X&);
X(X&); //OK
};
现在总结一下:构造函数是一种特殊函数,而拷贝构造函数是一种特殊的构造函数,拷贝构造函数的第一个参数必须为type X&,或者Type const X&,要么不存在其他参数,如果存在其他参数,其他参数必须有默认值,不妨加一句,根据这些定义可以确定一个类中可以有多个拷贝构造函数,但是我们根据拷贝构造函数的应用,即在赋值对象操作,对象作为参数时传递,以及对象作为返回值返回和在异常中抛出对象时,都需要调用类的拷贝构造函数生成对象这一点来定义拷贝构造函数,那么类中是否还可以定义多个拷贝构造函数,即理论上可以,实际中是否也可以定义多个拷贝构造函数?这个问题我们先保留,稍后讨论,
构造函数,拷贝构造函数都没有返回值,如果程序没有显示声明或者显示声明错误(ill -formed),都会生成相应的默认构造函数,拷贝构造函数,析构函数等。这些工作都有我们的编译器在编译的时候帮我们做好。
看过这些标准后,我的感觉就是C++难学,而C++编译器更难做,因为编译器要帮我们做太多的事情。这些都是题外话,我们将继续我们的构造函数之旅。
也就是因为构造函数的特殊性(没有函数名,不具有返回值,编译器可以默认创建,一般函数可享受不了这种待遇,这还不特殊吗),作为特殊函数,那么必须尤其特殊性,才能彰显出与众不同。
1:explicit关键字就是为构造函数准备的。这个关键字的含义就是显示调用构造函数,禁止编译器转换。确实编译器帮我们做太多的转换了,有时编译器的这种好意会给我们带来麻烦,所以我们要控制编译器。
class CAboutConstructer
{
public:
explicit CAboutConstructer(int ivalue)
{
_iTemp = ivalue;
cout << "Constuctor" << endl;
}
inline void p_Show() const
{
cout << _iTemp << endl;
}
private:
int _iTemp;
};
CAboutConstructer a(1);
a.p_Show();
a = 6; // 如果CAboutConstructer声明为explicit,那么此处无法编译,我们需要,显示调用CAboutConstructer,声明如下a = CAboutConstructer(6);
2:对象初始化列表,参考下面构造函数中对_iTemp,不同的初始化方式。
class CAboutConstructer
{
public:
CAboutConstructer(int ivalue):_iTemp(ivalue)
{
// _iTemp = ivalue;
cout << "Constuctor" << endl;
}
private:
int _iTemp;
};
对象的构造过程是,类的构造函数首先调用类内变量的构造函数(在C++中我们应该也把int等内置类型看作一个对象,是一个类,比方说我们可以这样定义一个int类型变量int i(5);这里的功能相对于int i; i = 5;,不过这两种方式是等效的,可以查看反汇编代码,这么不做过多解释)。那么调用构造函数有两种方式1:调用默认构造函数2:按值构造对象,在这里就是应用2特性,即构造函数在初始化_iTemp时直接把ivalue传递给_iTemp,这样减少了后面赋值操作_iTemp = ivalue;,所以初始化列表的效率相对于普通的赋值操作要高。
构造函数中拷贝构造函数,拷贝构造函数的定义非常简单:拷贝构造函数是一个构造函数,其第一个参数必须为type X&或者type const X&类型,并且没有其他参数,或者其他参数都有默认值。那么如下声明方式都应该是正确
CAboutConstructer(CAboutConstructer &rValue);
CAboutConstructer(const CAboutConstructer &rValue);
CAboutConstructer(CAboutConstructer& rValue,int ipara = 0);
CAboutConstructer(const CAboutConstructer& rValue,int ipara = 0);
CAboutConstructer(CAboutConstructer& rValue,int ipara1 = 0,int ipara2 = 0);
CAboutConstructer(const CAboutConstructer& rValue,int ipara = 0,int ipara2 = 0);
测试一下,除了在编译时刻有一个warnging外,编译成功。
warning C4521: “CAboutConstructer” : 指定了多个复制构造函数
这个warning提示我们说声明了多个复制(拷贝)构造函数,这个warning的含义写的有点不明白
“class”: 指定了多个复制构造函数
类有单个类型的多个复制构造函数。使用第一个构造函数。
我们不管它,现在至少说明一点拷贝构造函数可以在形式上定义多个,但是形式上的定义,能否经的住考验。看下面这个例子,我们先从函数重载说起
void p_Show(int i) const;
void p_Show(int i,int j = 0) const;
void p_Show(CAboutConstructer &rValue) const;
void p_Show(const CAboutConstructer &rValue) const;
void p_Show(CAboutConstructer& rValue,int ipara = 0) const;
void p_Show(const CAboutConstructer& rValue,int ipara = 0) const;
上面这几种声明方式,在我们编译时,居然没有报二义性错误,编译通过了,很奇怪。但是当我们使用上面的函数时
int _tmain(int argc, _TCHAR* argv[])
{
CAboutConstructer a(1);
a.p_Show(1);
system("pause");
return 0;
}
再次编译,发生一个错误
error C2668: “CAboutConstructer::p_Show” : 对重载函数的调用不明确
可能是“void CAboutConstructer::p_Show(int,int) const”
或是“void CAboutConstructer::p_Show(int) const”
“function”: 对重载函数的调用不明确
未能解析指定的重载函数调用。可能需要显式转换一个或多个实际参数。
那么从上面我们可以得出一点,编译器只有在使用函数时,才会对函数进行二义性检查,或者说实现时。
看到这里不得不想,拷贝构造函数会不会也这样那?声明时没有问题,而在实际应用过程中出错。
那么我们做如下测试
// 第一种形式
CAboutConstructer a(1);
CAboutConstructer a1(a);
// 第二种形式
const CAboutConstructer b(1);
CAboutConstructer b1(b);
// 第三种形式
b1 = a1;
// 第四种形式
b1.p_Show(a1);
// 第五种形式
a1.p_Show(b);
为了测试函数传递对象,我们定义如下两个函数,其目的就是在一个对象内显示另一个对象的_iTemp值,把上面的先注释掉,编译如下两个函数,
void p_Show(CAboutConstructer rValue) const
{
rValue.p_Show();
}
void p_Show(const CAboutConstructer rValue) const
{
rValue.p_Show();
}
编译出错
error C2535: “void CAboutConstructer::p_Show(CAboutConstructer) const” : 已经定义或声明成员函数。
这说明在按值传递的函数重载时,不能通过对一个参数添加const来实现函数重载,但是修改把上面参数传递修改为引用方式,就可以通过添加const来实现函数重载,这些都是题外话,毕竟我们在这里要测试的是,参数按值传递时,调用拷贝构造函数的问题,不知道这条规则是否也适合拷贝构造函数?拷贝构造函数是按引用方式传递。这一点是和普通函数调用方式一样。
void p_Show(CAboutConstructer rValue) const
{
rValue.p_Show();
}
编译正确。
为了验证拷贝构造函数,现在我们也把拷贝构造函数实现,如下
CAboutConstructer(CAboutConstructer &rValue)
{
_iTemp = rValue._iTemp;
}
CAboutConstructer(const CAboutConstructer &rValue)
{
_iTemp = rValue._iTemp;
}
CAboutConstructer(CAboutConstructer& rValue,int ipara = 0)
{
_iTemp = rValue._iTemp;
}
CAboutConstructer(const CAboutConstructer& rValue,int ipara = 0)
{
_iTemp = rValue._iTemp;
}
CAboutConstructer(CAboutConstructer& rValue,int ipara1 = 0,int ipara2 = 0)
{
_iTemp = rValue._iTemp;
}
CAboutConstructer(const CAboutConstructer& rValue,int ipara = 0,int ipara2 = 0)
{
_iTemp = rValue._iTemp;
}
编译通过,期待中的二义性还是没有出现。
好,现在开始测试第一种情况
CAboutConstructer a(1);
CAboutConstructer a1(a);
编译,期待中的二义性终于出现了
error C2668: “CAboutConstructer::CAboutConstructer” : 对重载函数的调用不明确可能是“CAboutConstructer::CAboutConstructer(CAboutConstructer &,int,int)” 或“CAboutConstructer::CAboutConstructer(CAboutConstructer &,int)”或“CAboutConstructer::CAboutConstructer(CAboutConstructer &)”
好那么先注释掉一些,仅保留
CAboutConstructer(CAboutConstructer &rValue)
测试没有问题,继续扩大拷贝构造函数范围,加上
CAboutConstructer(const CAboutConstructer &rValue)
编译通过,居然没有问题,也没有二义性。但是此时
CAboutConstructer a1(a);
究竟调用的那个拷贝构造函数那?我们跟踪一下,发现调用的是CAboutConstructer(CAboutConstructer &rValue),至此,我们可以确定一点,const修饰符在拷贝构造函数中对参数确实产生了影响,这是和普通函数不同的。
继续扩大拷贝构造函数范围,发现只要是有const修饰的都没有问题,更进一步表明const确实对拷贝构造函数的参数产生了影响。
用第二种方式
const CAboutConstructer b(1);
CAboutConstructer b1(b);
进行测试,发现这种方式调用的是CAboutConstructer(const CAboutConstructer &rValue)拷贝构造函数。
哈哈,至此我们可以在实际应用中得到拷贝构造函数的应用例子了。
那么再接下来的测试中,我们发现CAboutConstructer不使用const修改的拷贝构造函数都也没有问题,但是问题还没有完。
我们使用第一,第二中形式测试,发现只要存在任意一对拷贝构造函数,都可以测试通过,为了便于说明,我们分别给他们编号,我们任意一对奇偶编号组合的拷贝构造函数都可以同时存在,并且可以执行相应的拷贝构造函数。
1)CAboutConstructer(CAboutConstructer &rValue);
2)CAboutConstructer(const CAboutConstructer &rValue);
3)CAboutConstructer(CAboutConstructer& rValue,int ipara = 0);
4)CAboutConstructer(const CAboutConstructer& rValue,int ipara = 0);
5)CAboutConstructer(CAboutConstructer& rValue,int ipara1 = 0,int ipara2 = 0);
6)CAboutConstructer(const CAboutConstructer& rValue,int ipara = 0,int ipara2 = 0);
那么我们继续用第三种形式测试
// 第三种形式
b1 = a1;
很遗憾,在赋值运算中,不会调用拷贝构造函数。
用第四种方式进行测试,
// 第四种形式
b1.p_Show(a1);
发现调用的是CAboutConstructer(CAboutConstructer &rValue)这个拷贝构造函数,很正常,因为a1是非const类型的,所以当然会调用非const的构造函数,那么我们预测第五种形式,应该是调用
CAboutConstructer(const CAboutConstructer &rValue);
第五种形式测试
a1.p_Show(b);
不出所料果然是CAboutConstructer(const CAboutConstructer &rValue);
至此,我们还没有对函数返回对象,异常抛出对象时的拷贝构造进行测试,不过做这么多测试,我们可以预测,那两种情况下拷贝构造函数的调用,应该是和普遍函数调用是相同。如果您不相信可以测试一下,如果是预测错误,欢迎您批评指正。
总结:构造函数,拷贝构造函数,析构函数由于其本身是特殊函数,虽然他们也遵守一般函数的一般规则,比方说存在函数重载,函数参数默认值,引用const的问题,但是并不是完全相同,比如他们没有返回值。而其自身又有很多特殊型,比方说explicit修饰符,对象初始化列表。
以上测试结果基于编译环境。
编译环境:Windows2003 + VS2003
备注:以上测试,我们没有考虑代码优化,编译器设置等方面,只是着重考察C++的语言特性,如果您有什么不满的地方,欢迎指正。同时如果您在其他编译器上做测试,测试结果与VC2003下不同,也希望您发送给我一份,注明您的编译环境和编译器版本,我将在修订版中,署上您的大名以及测试结果。
最新修订请到http://www.exuetang.net和http://blog.csdn.net/ugg查阅
联系方式
邮箱:exuetang@163.com
这里特别感谢CSDN上的sinall网友,他首先指正了我对拷贝构造函数下const结论的问题
参考资料:
CSDN:有关拷贝构造函数的说法不正确的是
http://community.csdn.net/Expert/TopicView3.asp?id=4720584
C++标准ISO/IEC 14882:2003(E)
深度探索C++对象模型(Inside The C++ Object Model)