在c++中,定义一个空类时,编译器会默认声明6个成员函数,它们分别是
class Empty {
public:
Empty(); //默认构造函数
Empty(const Empty&); //拷贝构造函数
~Empty(); //析构函数
Empty& operator=(const Empty&); //赋值运算符
Empty* operator&(); //取地址运算符
const Empty* operator&() const; //取地址运算符const
};
注意 一下,编译器默认合成的析构函数不是虚函数。
首先,说一下什么是拷贝构造函数(也可以叫复制构造函数),它是一个特殊的构造函数,具有单个形参(此形参是对该类类型的引用,需要用const修饰,否则会无限循环调用复制构造函数)。一般这几种情况会调用它:1.根据另一个同类型的对象初始化一个对象 2.复制一个对象,将它作为实参传递给一个函数 3.从函数返回时复制一个对象。
只要我们不显示的定义拷贝构造函数,编译器都会自动合成一个,这时候编译器只是简单对对象中的数据成员进行赋值,也就是浅拷贝。大致的内部实现如代码所示
#include <iostream>
class Test {
public:
Test(int tmp) {
this->m1 = tmp;
}
//自定义拷贝构造函数
Test(Test& c) {
std::cout << "拷贝构造函数" << std::endl;
m1 = c.m1;
}
int m1;
};
int main() {
Test a(1);
Test b = a;
std::cout << b.m1 << std::endl;
system("pause");
return 0;
}
当然大多数情况下,浅拷贝是可以的,但是当类成员含有指针或者其它占有系统资源的变量时,由于浅拷贝只是简单的赋值,会将新对象的指针也指向以前对象已经分配的堆内存空间,如果这时候以前的对象已经析构,新对象的指针就成为野指针,出现运行错误。
先看下含有指针成员的的类对象浅拷贝代码
#include <iostream>
class Test {
public:
Test(int* pi) {
p = new int(100);
p = pi;
}
~Test() {
if (p != NULL) {
delete p;
p = NULL;
}
}
int *p;
};
int main() {
int i = 5;
Test a(&i);
Test b = a;
std::cout << "对象a中的成员p的地址: " << a.p << std::endl;
std::cout << "对象b中的成员p的地址: " << b.p << std::endl;
std::cout << *(a.p) << std::endl;
std::cout << *(b.p) << std::endl;
system("pause");
return 0;
}
输出结果:
从输出结果可以看出对象a和b中的指针成员p指向的是同一块堆内存地址,若a对象先把内存释放后在引用b中的p就会出现野指针的运行时错误。为了防止这种情况发生,我们需要自定义拷贝构造函数,对指针成员不是简单的赋值,需要重新开辟一块内存,将老对象指针指向的数据拷贝进去。这就是深拷贝。
看看深拷贝的实现:
#include <iostream>
class Test {
public:
Test(int* pi) {
p = new int(100);
p = pi;
}
//自定义拷贝构造函数(深拷贝)
Test(Test& c) {
std::cout << "拷贝构造函数" << std::endl;
p = new int;
if (p != NULL) {
//memcpy(p, c.p, sizeof(c.p));
*p = *(c.p);
}
}
//赋值运算符
Test& operator=(const Test &rhs) {
p = new int;
*p = *(rhs.p);
return *this;
}
~Test() {
if (p != NULL) {
delete p;
p = NULL;
}
}
int *p;
};
int main() {
int i = 5;
Test a(&i);
Test b = a;
std::cout << "对象a中的成员p的地址: " << a.p << std::endl;
std::cout << "对象b中的成员p的地址: " << b.p << std::endl;
std::cout << *(a.p) << std::endl;
std::cout << *(b.p) << std::endl;
system("pause");
return 0;
}
输出结果:
可以看出对象a和b中的指针保存的不是同一块堆内存地址,它们中的数据是一样的,这样就可以防止出现野指针的运行时错误。由于只是做个简单的测试,代码有点偏向伪码,类属性其实需要定义为private,为了测试方便,直接使用public了。
需要注意下,一般情况类需要重载赋值运算符,因为我们不一定是直接初始化对象Test b(a),我们可能会这样写Test b = a这种赋值初始化。所以好的建议是自定义了拷贝构造函数后顺便重载赋值运算符。
也就是说当类中含有指针这种需要占有系统资源的变量时,我们必须自定义拷贝构造函数(深拷贝)。浅拷贝说白了,就是简单赋值,有指针成员时,拷贝的是指针,而深拷贝就是拷贝指针指向的对象。