1. 默认构造函数
默认构造函数为无参构造函数,当类中没有定义构造函数(包括拷贝构造函数)时,编译器默认提供一个无参构造函数,并且其函数体为空。如果类中已经定义了构造函数(包括拷贝构造函数),编译器不会再提供默认的无参构造函数。
因此,对于空类:
class Tset
{
}
至少存在默认构造函数、默认析构函数、默认拷贝函数、重载赋值运算符函数。
2. 拷贝构造函数
在定义类对象时,无论是使用函数的方式即 () 还是等号的方式即 = 把一个已有对象初始化所定义的对象,都将调用拷贝构造函数。即 classname obj_A = obj_B; 等效于 classname obj_A(obj_B);。
值得注意的是,拷贝构造函数的形参类型应为 “const classname&”。
- 示例:
class Test
{
public:
Test(const Test& obj)
{
...
}
Test()
{
...
}
}
当类中自定义了拷贝构造函数,则必须自定义构造函数,因为此时编辑器不再提供默认构造函数。当类中没有定义拷贝构造函数时,编译器会提供一个默认拷贝构造函数,默认拷贝构造函数内会进行成员变量的浅拷贝(见第 3 节)。
在函数中,当使用对象作为形参或者返回值时,会调用拷贝构造函数。因此,一般地在对对象的传递中使用引用的方式来避免调用拷贝构造函数这一开销,但需把局部对象作为返回值时则不能使用引用的方式。
3. 浅拷贝与深拷贝
3.1 浅拷贝
在上文中提到,默认的拷贝构造函数为浅拷贝。何为浅拷贝?浅拷贝的意思是只进行数据值的拷贝。
- 实验:
class Test
{
private:
int num;
char* p;
public:
Test()
{
num = 10;
p = new char;
}
void free()
{
delete p;
}
}
int main(int argc, char* argv[])
{
Test obj_A;
Test obj_B = obj_A;
}
以上通过浅拷贝 obj_B.num 被赋值为 10,因此对于一般的变量,浅拷贝足够完成拷贝功能。同时,指针 p 同样只会进行值拷贝,即 obj_A.p 与 obj_B.p 指向相同的堆地址。
如此一来,假设 obj_A 进行内存释放后,obj_B.p 指向的便是一段非法的堆内存,进而造成对非法的内存访问;又或者 obj_A 与 obj_B 分别对内存进行释放,这样对同一段内存进行了二次释放,导致程序异常,这明显是错误的。
不同对象都应确保使用独立的内存空间。
3.2 深拷贝
深拷贝便是为了解决浅拷贝存在的资源分配问题而存在的。深拷贝中的“深”便体现在不仅是对值的拷贝,还体现在对资源的再分配。因此,深拷贝必须通过自定义的拷贝构造函数来实现,因为编译器根本不知道对象所需的资源是什么。
- 实验:
class Test
{
private:
int m_num;
char* m_p;
public:
Test()
{
m_num = 10;
m_p = new char;
}
Test(const Test& obj)
{
this->m_num = obj.m_num;
m_p = new char;
*m_p = *obj.m_p;
// 可以简写为 m_p = new char(*obj.m_p);
}
void free()
{
delete m_p;
}
}
int main(int argc, char* argv[])
{
Test obj_A;
Test obj_B = obj_A;
}
以上 obj_A.p 与 obj_B.p 分别指向不同的堆地址,而 *obj_A.p 与 *obj_B.p 是相等的。
因此,当类中涉及到资源分配如动态申请内存、打开文件、使用网络端口等,应使用深拷贝,即需自定义拷贝构造函数。而当类中仅涉及普通的赋值操作时,使用默认的拷贝构造函数进行浅拷贝即可,无需自定义拷贝构造函数。
对于深拷贝,还需要重载运算符 “=”(见【从 C 向 C++ 进阶】- 类 - 12. 运算符重载进阶)。
浅拷贝与深拷贝可以总结为:浅拷贝后对象的物理状态相同,深拷贝后对象的逻辑状态相同。