拷贝构造函数
拷贝构造函数,是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构建及初始化。其唯一的形参必须是引用,但并不限制为const,一般普遍的会加上const限制。此函数经常用在函数调用时用户定义类型的值传递及返回。拷贝构造函数要调用基类的拷贝构造函数和成员函数。如果可以的话,它将用常量方式调用,另外,也可以用非常量方式调用
调用拷贝构造函数的情形
在C++中,下面三种对象需要调用拷贝构造函数(有时也称“复制构造函数”):
1) 一个对象作为函数参数,以值传递的方式传入函数体;
2) 一个对象作为函数返回值,以值传递的方式从函数返回;
3) 一个对象用于给另外一个对象进行初始化(常称为赋值初始化);
如果在前两种情况不使用拷贝构造函数的时候,就会导致一个指针指向已经被删除的内存空间。对于第三种情况来说,初始化和赋值的不同含义是拷贝构造函数调用的原因。事实上,拷贝构造函数是由普通构造函数和赋值操作符共同实现的。描述拷贝构造函数和赋值运算符的异同的参考资料有很多。
通常的原则是:①对于凡是包含动态分配成员或包含指针成员的类都应该提供拷贝构造函数;②在提供拷贝构造函数的同时,还应该考虑重载"="赋值操作符号。原因详见后文。
拷贝构造函数必须以引用的形式传递(参数为引用值)。其原因如下:当一个对象以传递值的方式传一个函数的时候,拷贝构造函数自动的被调用来生成函数中的对象。如果一个对象是被传入自己的拷贝构造函数,它的拷贝构造函数将会被调用来拷贝这个对象这样复制才可以传入它自己的拷贝构造函数,这会导致无限循环直至栈溢出(Stack Overflow)。除了当对象传入函数的时候被隐式调用以外,拷贝构造函数在对象被函数返回的时候也同样的被调用。
隐式地拷贝构造函数
如果在类中没有显式的声明一个拷贝构造函数,那么,编译器会自动生成一个来进行对象之间非static成员的位拷贝(Bitwise Copy)。这个隐含的拷贝构造函数简单的关联了所有的类成员。注意到这个隐式的拷贝构造函数和显式声明的拷贝构造函数的不同在于对成员的关联方式。显式声明的拷贝构造函数关联的只是被实例化的类成员的缺省构造函数,除非另外一个构造函数在类初始化或构造列表的时候被调用。
拷贝构造函数使程序更有效率,因为它不用再构造一个对象的时候改变构造函数的参数列表。设计拷贝构造函数是一个良好的风格,即使是编译系统会自动为你生成默认拷贝构造函数。事实上,默认拷贝构造函数可以应付许多情况。
引申:在这里,与C#是不同的。C#里面用已知的对象去初始化另一个对象,传递的是该已知对象的指针,而并不是隐式地拷贝构造函数。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
usingSystem; usingSystem.Collections.Generic; usingSystem.Linq; usingSystem.Text; namespaceC_Sharp_structVSclass { classMyClass { publicintval; } structmyStruct { publicintval; } classProgram { staticvoidMain( string []args) { MyClassobjectA=newMyClass(); MyClassobjectB=objectA; objectA.val=10; objectB.val=20; Console.WriteLine( "{0}" ,objectA.val); Console.WriteLine( "{0}" ,objectB.val); Console.ReadKey(); } } } |
这里的输出会是“20,20”而不是C++里面的10,20。所以一定要跟C++区分开看。
2例述编辑
复制初始化
以下讨论中将用到的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
classCExample { public : CExample(){pBuffer=NULL;nSize=0;} ~CExample(){ delete []pBuffer;} voidInit(intn){pBuffer=newchar[n];nSize=n;} private : char *pBuffer; //类的对象中包含指针,指向动态分配的内存资源 intnSize; }; 这个类的主要特点是包含指向其他资源的指针,pBuffer指向堆中动态分配的一段内存空间。 intmain(intargc, char *argv[]) { CExampletheObjone; theObjone.Init(40); //现在需要另一个对象,并将它初始化为theObjone CExampletheObjtwo=theObjone; ... } |
语句"CExample theObjtwo=theObjone;"用theObjone初始化theObjtwo。
回顾一下此语句的具体过程:首先建立对象theObjtwo,并调用其构造函数,然后成员被复制初始化。
其完成方式是内存拷贝,复制所有成员的值。完成后,theObjtwo.pBuffer==theObjone.pBuffer。
即它们将指向同样的地方,指针虽然复制了,但所指向的空间并没有复制,而是由两个对象共用了。这样不符合要求,对象之间不独立了,并为空间的删除带来隐患。所以需要采用必要的手段来避免此类情况:可以在构造函数中添加操作来解决指针成员的这种问题。
所以C++语法中除了提供缺省形式的构造函数外,还规范了另一种特殊的构造函数:拷贝构造函数,一种特殊的构造函数重载。上面的语句中,如果类中定义了拷贝构造函数,在对象复制初始化时,调用的将是拷贝构造函数,而不是缺省构造函数。在拷贝构造函数中,可以根据传入的变量,复制指针所指向的资源。
拷贝构造函数的格式为:类名(const 类名& 对象名);//拷贝构造函数的原型,参数是常量对象的引用。由于拷贝构造函数的目的是成员复制,不应修改原对象,所以建议使用const关键字。
提供了拷贝构造函数后的CExample类定义为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
classCExample { public : CExample(){pBuffer=NULL;nSize=0;} ~CExample(){ delete []pBuffer;} CExample(constCExample&); //拷贝构造函数 voidInit(intn){pBuffer=newchar[n];nSize=n;} private : char *pBuffer; //类的对象中包含指针,指向动态分配的内存资源 intnSize; }; CExample::CExample(constCExample&RightSides) //拷贝构造函数的定义 { nSize=RightSides.nSize; //复制常规成员 pBuffer=newchar[nSize]; //分配内存 memcpy (pBuffer,RightSides.pBuffer,nSize* sizeof ( char )); } |
这样,定义新对象,并用已有对象初始化新对象时,CExample(const CExample& RightSides)将被调用,而已有对象用别名RightSides传给构造函数,以用来作复制。
对象按值传递
下面介绍拷贝构造函数的另一种调用:当对象直接作为参数传给函数时,函数将建立对象的临时拷贝,这个拷贝过程也将调用拷贝构造函数。例如:
BOOL testfunc(CExample obj);
testfunc(theObjone); //对象直接作为参数。
BOOL testfunc(CExample obj)
{
//针对obj的操作实际上是针对复制后的临时拷贝进行的
}
还有一种情况,也是与临时对象有关:当函数中的局部对象作为返回值被返回给函数调者时,也将建立此局部对象的一个临时拷贝,拷贝构造函数也将被调用。
CTest func()
{
CTest theTest;
return theTest;
}
总结:当某对象是按值传递时(无论是作为函数参数,还是作为函数返回值),编译器都会先建立一个此对象的临时拷贝,而在建立该临时拷贝时就会调用类的拷贝构造函数。
3赋值重载编辑
重载的必要性
下面的代码与上例相似
1
2
3
4
5
6
7
8
9
10
|
intmain(intargc, char *argv[]) { CExampletheObjone; theObjone.Init(40); CExampletheObjthree; theObjthree.init(60); //现在需要一个对象赋值操作,被赋值对象的原内容被清除,并用右边对象的内容填充。 theObjthree=theObjone; return0; } |
这里也用到了"="号,但与"复制初始化"中的例子并不同。"复制初始化"的例子中,"="在对象声明语句中,表示初始化。更多时候,这种初始化也可用圆括号表示。例如:CExample theObjthree(theObjone);。
而本例子中,"="表示赋值操作。将对象theObjone的内容复制到对象theObjthree,这其中涉及到对象theObjthree原有内容的丢弃,新内容的复制。
因此,包含动态分配成员的类除提供拷贝构造函数外,还应该考虑重载"="赋值操作符号。
重载的示例
类定义变为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
classCExample { public : CExample(){pBuffer=NULL;nSize=0;} ~CExample(){deletepBuffer;} CExample(constCExample&); //拷贝构造函数 CExample&operator=(constCExample&); //赋值符重载 voidInit(intn){pBuffer=newchar[n];nSize=n;} private : char *pBuffer; //类的对象中包含指针,指向动态分配的内存资源 intnSize; }; //赋值操作符重载 CExample&CExample::operator=(constCExample&RightSides) { if ( this ==&RightSides) //如果自己给自己赋值则直接返回 { return * this ;} nSize=RightSides.nSize; //复制常规成员 char *temp=newchar[nSize]; //复制指针指向的内容 memcpy (temp,RightSides.pBuffer,nSize* sizeof ( char )); delete []pBuffer; //删除原指针指向内容(将删除操作放在后面,避免X=X特殊情况下,内容的丢失) pBuffer=temp; //建立新指向 return * this ; } |
重载的注意事宜
拷贝构造函数和赋值函数的功能是相同的,为了不造成重复代码,拷贝构造函数实现如下:
CExample::CExample(const CExample& RightSides)
{
*this=RightSides; //调用重载后的"="
}
//20130724 说明: 参见Effective C++ 第60页(条款12的结尾)作者强烈不建议用某个copying函数实现另一个copying函数。所以对于上面这段内容有待商榷。
4格式示例编辑
拷贝构造函数的格式
拷贝构造函数的声明:
class 类名
{
public:
类名(形参参数)//构造函数的声明/原型
类名(类名&对象名)//拷贝构造函数的声明/原型
...
};
拷贝构造函数的实现:
类名::类名(类名&对象名)//拷贝构造函数的实现/定义
{函数体}