复制构造函数是重载构造函数的重要形式之一。
在 “将对象直接传递给函数及存在的潜在问题 ” 和 “返回对象及其潜在问题 ” 里已经看到,将对象传递给函数或者从函数中返回对象都有可能产生问题。产生这些问题的关键在于创建了一个按位复制的对象副本。而避免产生这些问题的方法是我们需要精确的定义对象在产生一个副本时的行为,通过自定义这些行为来规避不可预料的副作用,而复制构造函数可以达到这个目的。
当使用一个对象来初始化另外一个对象时会调用复制构造函数。注意:复制构造函数只能用于初始化,而不能用于赋值运算,也就是说复制构造函数不会影响赋值运算。
复制构造函数的通用形式如下:
[C++] 纯文本查看 复制代码 classname (const classname &obj) {
//函数体
}
上面的声明中,参数 obj 是一个对象的引用,这个对象将用来初始化另一个对象。
1. 复制构造函数与参数
当对象被作为参数传递给函数时,编译器将产生一个对象副本,如果此时定义了复制构造函数,那么编译器将调用复制构造函数来获得一个对象的副本。下面程序示例:
[C++] 纯文本查看 复制代码 #include <iostream>
#include <cstdlib>
using namespace std;
class myclass {
int *p;
public:
myclass(int i); //普通构造函数
myclass(const myclass &ob); //复制构造函数
~myclass(); //析构函数
int getval() { return *p; }
};
//复制构造函数
myclass::myclass(const myclass &obj)
{
p = new int;
*p = *obj.p; //值复制
cout << "Copy constructor called.\n";
}
//普通构造函数
myclass::myclass(int i)
{
cout << "Allocating p\n";
p = new int;
*p = i;
}
myclass::~myclass()
{
cout << "Freeing p\n";
delete p;
}
//函数带有一个对象参数,此时编译器调用复制构造函数来创建对象的副本
void display(myclass ob)
{
cout << ob.getval() << "\n";
}
int main()
{
myclass a(10);
display(a);
return 0;
}
运行输出:$ ./cpycon
Allocating p
Copy constructor called.
10
Freeing p
Freeing p 由输出可见,对比与 "将对象直接传递给函数及存在的潜在问题 ",不再出现程序崩溃的事情。下面分析一下函数执行的过程:
在 main() 中创建对象 a 时调用了普通构造函数为成员 a.p 分配了内存空间。在调用 display() 时,对象 a 被作为参数传递给 ob ,正是在这时,a 的复制构造函数被调用,从而创建了对象 a 的一个副本。复制构造函数为副本中的 p 分配内存,然后将对象 a 中的 a.p 中的值赋值给 *p 。这样,变量 a.p 和 ob.p 所指向的内存空间将是互相独立的空间,但内存空间中的包含的值一样。如果没有创建复制构造函数,那么默认的就是按位复制,这会使得变量 a.p 和 ob.p 指向同一块内存。
在 display() 函数返回时,对象 ob 的析构函数会被调用,并且释放掉 ob.p 指向的内存空间。最后,在 main() 返回时,对象 a 的析构函数被调用,并且释放掉 a.p 所指向的内存空间。
从上面看到,通过复制构造函数,我们可以消除在传递对象给函数时所带来的破坏性副作用。
2.复制构造函数与初始化
当使用一个对象来初始化另一个对象时,也将调用复制构造函数。如下所示:
[C++] 纯文本查看 复制代码 #include <iostream>
#include <cstdlib>
using namespace std;
class myclass {
int *p;
public:
myclass(int i);
myclass(const myclass &ob);
~myclass();
int getval() { return *p; }
};
myclass::myclass(const myclass &obj)
{
p = new int;
*p = *obj.p;
cout << "Copy constructor called.\n";
}
myclass::myclass(int i)
{
cout << "Normal constructor allocating p.\n";
p = new int;
*p = i;
}
myclass::~myclass()
{
cout << "Freeing p\n";
delete p;
}
int main()
{
myclass a(10);
myclass b = a; //用一个对象来初始化另一个对象,此时复制构造函数被调用
return 0;
}
运行输出:$ ./cpyinit
Normal constructor allocating p.
Copy constructor called.
Freeing p
Freeing p
注意:复制构造函数只有在初始化对象时才会被调用。下面的代码不会调用复制构造函数:myclass a(2), b(3);
...
b = a;
3. 在返回对象时调用复制构造函数
示例程序:
[C++] 纯文本查看 复制代码
#include <iostream>
using namespace std;
class TDate
{
private:
int year, month, day;
public:
int yy() { return year; }
int mm() { return month; }
int dd() { return day; }
TDate() { }
TDate(int y, int m, int d)
{
cout << "Normall construcgtor called.\n";
year = y;
month = m;
day = d;
}
TDate(const TDate &dd)
{
cout << "Copy constructor called.\n";
year = dd.year;
month = dd.month;
day = dd.day;
cout << year << "===" << month << "===" << day << endl;
}
void print()
{
cout << year << "/" << month << "/" << day << endl;
}
};
TDate tomorrow(TDate date1)
{
cout << "test After and Before\n";
int y, m, d;
y = date1.yy();
m = date1.mm();
d = date1.dd() + 1;
TDate date2(y, m, d);
cout << "check tomorrow...\n";
return date2;
}
int main()
{
TDate d1(2011, 10, 16);
d1.print();
TDate d2;
d2 = tomorrow(d1);
d2.print();
return 0;
}
运行输出:$ ./cpyret2
Normall construcgtor called.
2011/10/16
Copy constructor called.
2011===10===16
test After and Before
Normall construcgtor called.
check tomorrow...
2011/10/17 由上面输出可以看到,复制构造函数的调用在真正执行 tomorrow() 之前。
编译器检测到 tomorrow() 的返回类型是对象并且在 main() 中返回给对象 d2,所以首先调用了复制构造函数,并用对 d1 的引用作为其参数对 d2 这个对象进行了初始化(从复制构造函数的日期输出中可以看到)。当 tomorrow() 函数返回后,再用对象 date2 中的成员对 d2 中的成员进行依次的赋值,所以最后的日期变为 17 号。
上面复制构造函数被调用,引发的原因仍然不是”返回对象“ 而调用复制构造函数,而是”将对象作为函数参数“ 。将 tomorrow() 函数去掉参数后,我们并不会看到复制构造函数被调用,即使 tomorrow() 返回的是一个对象。这种情况跟编译器有关。
可能对于有些编译器来说会中规中矩的在函数返回对象时调用复制构造函数,但 g++ 却不会。比较下面代码:
[C++] 纯文本查看 复制代码 #include <iostream>
using namespace std;
class myclass {
public:
myclass() { cout << "Normal constructor.\n"; }
myclass(const myclass &obj) { cout << "Copy Constructor.\n"; }
~myclass() { cout << "Freeing Constructor.\n"; }
};
myclass f()
{
myclass ob;
return ob;
}
int main()
{
myclass a;
myclass b;
b = f();
return 0;
}
运行输出:$ ./cpyret
Normal constructor.
Normal constructor.
Normal constructor.
Freeing Constructor.
Freeing Constructor.
Freeing Constructor. 从上面的输出可以看到,复制构造函数并没有被调用,即使 f() 返回的是一个对象类型。这种情况对于 g++ 来说是情况却是如此,然而像在 vc2010 里同样编译运行这段程序,输出中可以清楚看到构造函数被调用:Normal constructor.
Normal constructor.
test someting.
Normal constructor.
Copy Constructor.
Freeing Constructor.
Freeing Constructor.
Freeing Constructor.
Freeing Constructor.
在修改上面那个时间设置函数为:
[C++] 纯文本查看 复制代码 #include "stdafx.h"
#include <iostream>
using namespace std;
class TDate
{
private:
int year, month, day;
public:
int yy() { return year; }
int mm() { return month; }
int dd() { return day; }
TDate() { }
TDate(int y, int m, int d)
{
cout << "Normall construcgtor called.\n";
year = y;
month = m;
day = d;
}
TDate(const TDate &dd)
{
cout << "Copy constructor called.\n";
year = dd.year - 1;
month = dd.month - 1;
day = dd.day - 1;
cout << year << "===" << month << "===" << day << endl;
}
void print()
{
cout << year << "/" << month << "/" << day << endl;
}
};
TDate tomorrow()
{
TDate date2(2012, 12, 21);
return date2;
}
int _tmain(int argc, _TCHAR* argv[])
{
TDate d1(2011, 10, 16);
d1.print();
TDate d2;
d2 = tomorrow();
d2.print();
return 0;
}
在 VC 里编译输出:Normall construcgtor called.
2011/10/16
Normall construcgtor called.
Copy constructor called.
2011===11===20
2011/11/20 同样,复制构造函数被调用了。如果换作 g++ ,仍然是不会调用复制构造函数,而仍然只是调用副本来进行按位复制:$ ./cpyret
Normall construcgtor called.
2011/10/16
Normall construcgtor called.
2012/12/21 如果希望在 g++ 里也能调用上面的复制构造函数,那么应该将复制构造函数声明为 “赋值运算符重载函数”,如:
[C++] 纯文本查看 复制代码
#include <iostream>
using namespace std;
class TDate
{
private:
int year, month, day;
public:
int yy() { return year; }
int mm() { return month; }
int dd() { return day; }
TDate() { }
TDate(int y, int m, int d)
{
cout << "Normall construcgtor called.\n";
year = y;
month = m;
day = d;
}
TDate operator=(const TDate &dd)
{
cout << "Copy constructor called.\n";
year = dd.year - 1;
month = dd.month - 1;
day = dd.day - 1;
cout << year << "===" << month << "===" << day << endl;
}
void print()
{
cout << year << "/" << month << "/" << day << endl;
}
};
TDate tomorrow()
{
TDate date2(2012, 12, 21);
return date2;
}
int main()
{
TDate d1(2011, 10, 16);
d1.print();
TDate d2;
d2 = tomorrow();
d2.print();
return 0;
}
运行输出:$ ./cpyret
Normall construcgtor called.
2011/10/16
Normall construcgtor called.
Copy constructor called.
2011===11===20
2011/11/20 至于为什么 g++ 会如此安排,我也说不太清楚,似乎从 3.3 版本之后的编译器就已做如此安排了。也许是为了提醒你,返回一个局部对象时调用一个普通的复制构造函数的危险性较大。比如在返回对象时,传入复制构造函数参数里的是返回对象的一个副本的引用,假设返回的对象是分配了资源的(如内存空间),而在复制构造函数里也仍然使用了同样的空间,那么程序在复制构造函数返回时有可能因为释放同样的内存空间而造成程序的崩溃,并且这个崩溃就发生在复制构造函数里。如 g++ 这种在返回对象时省略了复制构造函数的调用,也可能会出现程序崩溃,但它的崩溃是发生在 main() 退出时。从这点来说,后者的严重情况比前者要轻一些。 |