[C++对象模型]复制构造函数的建构操作
关于复制构造函数的简单介绍,可以看我以前写过的一篇文章C++复制控制之复制构造函数该文章中介绍了复制构造函数的定义、调用时机、也对编译器合成的复制构造函数行为做了简单说明。本文因需要会涉及到上文的一些知识点,但还是推荐先阅读上文。
本文主要从编译器角度对复制构造函数进行分析,纠正以前对复制构造函数的一些错误认识。
浅拷贝(deep copy)与深拷贝(shallow copy)
我们首先来看复制构造函数涉及的两个概念:浅拷贝与深拷贝。假设有两个对象:A与B,它们是同类型的,下面分析B=A时浅拷贝与深拷贝行为。
浅拷贝:
浅拷贝简单地把B复制为A的引用或指针,可以认为B复制了A的地址,复制的结果是B与A拥有相同的地址,它们将指向相同的内存区域的相同的数据。在这种情况下,如果对象A被销毁,那么对对象B的某些操作将是非法的。
深拷贝:
深拷贝时使用一个对象的内容来创建同一个类的另一个实例,B复制了A的所有成员,并在内存中不同于A的区域为B分配了存储空间,也即是说B拥有自己的资源。在这种方式下,如果A被销毁时,B依旧有效,因为A与B并没有共享存储空间,重载复制操作符时要采用这种深拷贝方式。
当你明确知道你中程序中使用的是浅拷贝并且明白它带来的后果时你才去使用浅拷贝。而当你有大量的指针要处理时,对指针做浅拷贝是一个糟糕的做法。如果我们类的数据成员都是内置类型而没有指针,那么简单的浅拷贝是可以接受的,反之如果类中有需要深层复制的内容,则我们的复制构造函数必须以深拷贝的方式进行对象的复制。
Memberwise copy 与 Bitwise copy
Memberwise copy:
逐个成员:我们把merberwise copy当成deep copy来理解就行了,这种复制会根据每个成员的类型来进行复制,对于指针类型会复制指针所指的值(重新分配存储区域)。
Bitwise copy:
Bitwise copy 字面上的意思是逐位拷贝。举个例子,对于两个同类型的对象A与B,对象A在内存中占据存储区为0x0-0x9,执行B=A时,使用Bitwise copy拷贝语义,那么将会拷贝0x0到0x9的数据到B的内存地址,也就是说Bitwise是字节到字节的拷贝。这样子理解起来,实际上Bitwise copy = shallow copy。
类的Bitwise copy 语意
《Effective C++》中说到:
如果你自己没声明,编译器就会为它声明一个copy构造函数、一个copy assignment操作符和一个析构函数。
实际上在《深度探索C++对象模型》中对编译器的行为并不是这样描述的。对于默认构造函数与复制构造函数,都需要类满足一定的条件时编译器才会帮你合成。那么需要满足些什么条件呢?这条件就是:类不展现bitwise copy 语意的时候。
类展现Bitwise copy语意
当我们的类中只含有内置类型或复合类型时,类展现了Bitwise copy 语意。这种情况下并不需要合成一个默认复制构造函数,也即编译器不会帮我们合成复制构造函数。如:
//以下声明展现了bitwise copy 语意
class Word
{
public:
Word(chartemp){ str = temp; };
//...
int cnt;
char str;
};
这时候如果我们有两个Word对象的赋值操作如下:
int main()
{
char * temp = "hello";
Word A(temp);
Word B(A);
cout << B.str;
system("pause");
return 0;
}
运行程序,你会神奇地发现程序居然通得过编译,而且B也得到了正确的赋值,就好像类中有了一个复制构造函数一样。不是说编译器在Bitwise copy语意下不会进行复制构造函数的合成吗?
说实话这问题我也很疑惑,查看了许多资料,反复看了《深度探索C++对象模型》后,我最终这样认为:展现了Bitwise copy语意的类编译器不会为它写一个函数实体进行成员的复制。展现Bitwise copy语意的类,类的数据成员按照Memberwise Initialization(注意不同于Memberwise copy)进行初始化,具体是这样的:当类对象以同类型的另一个对象进行初始化时,把每一个内建的或派生的date member(例如一个指针或一数目组)的值,从一个对象拷贝到另一个对象,不过它并不会拷贝其中的member class object,而是以递归的方式施行以上的拷贝。实施这些步骤并不在函数实体内。
类不展现Bitwise copy语意
当类不展现出Bitwise copy语意且类设计者没有为类定义一个复制构造函数,这时编译器就会为合成一个复制构造函数实体。那么在什么情况下一个类才会不展现出Bitwise copy 语意呢?
- 当类内含有一个member object 而后者的类声明中有个复制构造函数时(无论这个构造函数是设计者明确地声明还是编译器合成)。
- 当类继承于一个基类而后者有已给复制构造函数时(同样的,无论基类的构造函数是设计者明确声明的还是合成的)。
- 当类声明了一个或多个虚函数时。
- 当类派生自一个继承串链,其中有一个或多个虚基类时。
前两种情况中,编译器必须将“类成员或基类的复制构造函数调用操作”安插到新合成的复制构造函数中去,如果类设计者已经明确声明了一个复制构造函数,则这些调用操作代码将插入到已有的复制构造函数中去(在函数体的最前端插入)。
后两种操作涉及到了虚表指针与虚基类指针的产生于初值设置。我们知道,当一个类含有虚函数时(无论这虚函数是类本身定义还是继承而来),在编译期间会有以下两个程序扩张操作:
- 为类增加一个虚表(virtual function table),虚表内含有每一个有作用的虚函数的地址。
- 为每一个类对象增加一个虚表指针(vptr),虚表指针指向了该类的虚表。
显然,如果编译器对每个新定义的类对象不能正确地设置好初值,将导致严重的后果。所以编译器需要合成出一个复制构造函数来适当地初始化类对象的vptr。万一类设计者明确定义了自己的复制构造函数,则编译器会把设置vptr的操作插入到已有的复制构造函数中。而vptr的复制又有两种情况:
- 同类型对象间的vptr复制
同类类型的对象各自的vptr总是指向了同一个位置:该类的虚表指针。这时两个对象的vptr的复制都可以直接考”bitwise copy“来完成(除了可能会有的其他指针成员)。所以同类型对象间的vptr复制总是安全的。
-把子类对象vptr复制给父类对象
不用担心把子类对象复制给父类对象时,vptr也会采用bitwise copy来复制,这点编译器给我们做了保证:编译器合成的默认构造函数(或者说在明确声明的复制构造函数中安插的代码)会明确设定父类的vptr指向父类的虚函数表,而不是采用傻瓜式直接复制子类对象vptr。
而对于第4点涉及到虚基类的情况,可以看C++合成默认构造函数的真相中有关虚基类的描述。虚基类的存在需要特殊处理,一个类对象如果以另一个对象作为初值,而后者派生于虚基类,那么这种情况下bitwise copy语意也会失效,编译器会对派生自虚基类的类合成一个默认构造函数,在其中安插一些操作。对于虚继承,编译器有承偌:派生类对象中的虚基类位置在执行期就要准备妥当,维护”位置的完整性“是编译器的责任,而显然的,Bitwise copy 语意会破坏这个位置(这种傻瓜式的复制好像只适用内置类型的复制以及同类型对象间vptr的复制),所以编译器必须在它自己合成出来的复制构造函数中做出仲裁。同样的,如果类设计者明确声明了复制构造函数,则这些冲裁代码将安插在这个复制构造函数中。
总结
在类不满足"Bitwise copy"语意时编译器会采取行动,如果类设计者没有明确定义复制构造函数,则编译器将行动实施于合成构造函数中,否则将这些行动实施于已有的复制构造函数中。值得注意的是,编译器除了对vptr与虚基类的处理能保证安全之外,对于内置类型或复合类型如指针的复制都是采用浅拷贝,所以,当我们的类中含有指针的时候,我们需要自己写一个复制构造函数来对对象的指针进行深拷贝,而vptr与虚基类的问题,就交给编译器吧!