关于编译器为什么需要合成拷贝构造函数以及在什么时候合成拷贝构造函数的文章很多,但是反过来想想:为什么在某些情况下编译器可以不合成拷贝构造函数?
通常需要合成的情况有:
1)如果一个类没有拷贝构造函数,但是含有一个类类型的成员变量,该类型含有拷贝构造函数,此时编译器会为该类合成一个拷贝构造函数;
2)如果一个类没有拷贝构造函数,但是该类继承自含有拷贝构造函数的基类,此时编译器会为该类合成一个拷贝构造函数;
3)如果一个类没有拷贝构造函数,但是该类声明或继承了虚函数,此时编译器会为该类合成一个拷贝构造函数;
4)如果一个类没有拷贝构造函数,但是该类含有虚基类,此时编译器会为该类合成一个拷贝构造函数;
其实我们在拷贝或赋值一个对象或变量时,其本质是在内存上做了一个二进制序列的拷贝。如:
int i = 10;
int j = i; //把i对应的内存上的二进制数据,复制一份到j对应的内存区域上
而c++中对象的本质就是一段“内存区域”,如
class A{
public:
char c;
int num;
};
A a;
对象a在内存上可以理解为一个unsigned char类型的数组,这个数组的大小为8(sizeof(class A)为8,有内存对齐),其中数组的[0]上存放的二进制数据用来表示成员变量c,数组的[1][2][3]是内存对齐的“空隙”,数组的[4][5][6][7]共同表示成员变量num。当运行A aa = a;
这样的代码时,本质把这个unsigned char数组复制到aa对应的内存区域上了。请看下面的代码:
class A{
public:
char c;
int num;
};
int main() {
A a;
a.c = 'A';
a.num = 100;
cout<<sizeof(class A)<<endl; //大小为8
reinterpret_cast<unsigned char*>(&a)[1] = 'a' ; //在内存对齐的“空隙”中放入一些字符
reinterpret_cast<unsigned char*>(&a)[2] = 'b' ;
reinterpret_cast<unsigned char*>(&a)[3] = 'c' ;
A b = a; //没有自定义拷贝构造函数,编译器也不会合成,
cout<<b.c<<endl; //输出 A
cout<<b.num<<endl; //输出 100
cout<<reinterpret_cast<unsigned char*>(&b)[1]<<endl; //输出 a
cout<<reinterpret_cast<unsigned char*>(&b)[2]<<endl; //输出 b
cout<<reinterpret_cast<unsigned char*>(&b)[3]<<endl; //输出 c
return 0;
}
有趣的是在对象b的内存对齐“空隙”上,竟然也能输出在对象a的对齐“空隙”上插入的字符,这说明在运行A aa = a;
时只是简单的复制二进制数据罢了。作为对比,请看下面的代码:
class A{
public:
char c;
int num;
A(){}
A(const A &a){ //自己定义一个拷贝构造函数
c = a.c;
num = a.num;
}
};
int main() {
A a;
a.c = 'A';
a.num = 100;
cout<<sizeof(class A)<<endl;
reinterpret_cast<unsigned char*>(&a)[1] = 'a' ;
reinterpret_cast<unsigned char*>(&a)[2] = 'b' ;
reinterpret_cast<unsigned char*>(&a)[3] = 'c' ;
A b = a; //调用自己定义的拷贝构造函数
cout<<b.c<<endl;
cout<<b.num<<endl;
cout<<reinterpret_cast<unsigned char*>(&b)[1]<<endl; //输出乱码
cout<<reinterpret_cast<unsigned char*>(&b)[2]<<endl; //输出乱码
cout<<reinterpret_cast<unsigned char*>(&b)[3]<<endl; //输出乱码
return 0;
}
不同于之前代码,这里在输出对象b中内存对齐“空隙”时乱码了,说明对象a中内存对齐“空隙”上字符并没有拷贝过来,原因我们自己定义了拷贝构造函数。
综上,除非出现编译器必须要合成拷贝构造函数的四种情况之外,在拷贝对象只需要简单的复制下内存上的二进制数据就行了,因此编译器可以不合成拷贝构造函数。
2018-7-6更新:关于对象在内存上的布局,是个很复杂的问题,类中的成员的类型有很多种可能性,如虚的,非虚的,静态的,非静态的,引用的,非引用的,const的,非const的,枚举的,这都会影响到对象的布局。本人水平有限,如果没有理解错的话,只有满足标准布局类型的类才能使用reinterpret_cast转化成数组来理解,至于什么是标准布局类型可以参考cppreference.com中相关条目。