Copy Constructor
用一个类对象去另一个同类对象有三种情形:
Class Node;
Node n;
// 直接用另一个对象初始化
Node m = n;
// 这种初始化方式与Node m; m = n; 不同,这种是赋值,而不是初始化,首先调用Node的默认构造函数去初始化m,然后调用赋值构造函数,用n去“初始化”m
// 对象作为函数参数
void fun(Node n);
// 对象是函数的返回值
Node getNode();
Node m = getNode();
拷贝构造函数的形式可以,自定义的拷贝构造函数可以是一个多参数的函数,但除第一个参数外,其他参数必须有默认值,以下两种拷贝构造函数都是合法的:
class Node;
Node::Node(const& n);
Node::Node(const& m, int val=0);
默认的逐个成员初始化(default memberwise initializaiton)
对于一个没有显式声明的类来说,当用一个类对象去初始化另一个类对象的时候,会采用“默认的逐个成员初始化”。这种默认的成员初始化方式通过拷贝基本类型或者派生类型(如pointer或者array)的值来实现。但是类对象的初始化却不是直接拷贝的,而是递归的进行“默认逐成员初始化”。(这部分很好理解,不再举例)
默认的逐成员初始化这种方式在c++中是通过拷贝构造函数来实现的。但是c++的标准同样有解释到,编译器不会为所有的类合成拷贝构造函数,编译器不会为具有按位拷贝语义(bitwise copy semantics)的类合成拷贝构造函数,与对待默认构造函数一样,只有在具有实现需要(implementation needed)时才会去给没有显式声明的拷贝构造函数去合成拷贝构造函数。而这里的“实现需要”是由这个类是否具有按位拷贝语义而决定的。
(这两天工作比较忙,晚上没时间看书,文章写了一半就停了。今天周五,早下班,终于可以把这篇补上了)
按位拷贝语义(bitwise copy semantics)
当一个类表现出按位拷贝语义时,编译器不会为该类合成拷贝构造函数。按位拷贝语义,顾命思议就是二进制的比特级拷贝,这个词很好理解,但是对于计算机的任何数据,应该都可以按位拷贝吧。但按照书里想要表达的意思,按位拷贝并不是每个类都具有的特质。什么时候一个类才会表现出按位拷贝语义?书里没有明确指出,我个人的理解是,当一个类对象按位拷贝不影响其本身或者其他类对象的初始化时,即具有拷贝语义。(描述的好像不是很清晰)比如对于string类来说,其内部没没有直接保存其所表示的字符,而是保存了字符所在的地址和长度,对于string类对象来说其按位拷贝只能拷贝字符串的地址和长度等信息,是一种浅拷贝,并不能真正的拷贝string对象,因此string不具有按位拷贝语义。
虽然书里没有明确指出什么是拷贝语义,但是书里指出了当一个类不具有拷贝语义的四种情况:
-
当一个类的成员具有显式拷贝构造函数时。(此时如果采用按位拷贝可能会影响其类对象的初始化,其类对象可能并不支持按位拷贝)
-
当一个类的基类具有拷贝构造函数。(不论是显式的还是合成的)
-
当类具有虚函数时。
-
当类的继承层次中存在虚基类时。
前两种情况,按位拷贝有可能影响类成员或者基类的初始化,合成的构造函数会调用类成员或基类的构造函数,并完成其他成员的拷贝。后两种情况需要展开讨论。(一旦有虚表、虚表指针,就有可能需要编译器做一些额外的工作来实现多态,这些额外的工作可能就需要放在默认构造函数或者拷贝构造函数里了。)
重置虚表指针
其实如果只有虚函数,而没有任何继承关系,这个类也是具有拷贝语义的。因为按位拷贝不会影响虚表指针指向该类的虚表(虽然如此,但是没有继承关系的类,也不应该声明虚函数)。一旦一个含有虚函数的类出现在了继承层次中,这个时候按位拷贝就不一定能够让一个类的虚表指针指向该类的虚表了。对于以下代码:
class Base
{
public:
virtual void Func();
};
class Derived public Base;
Derived d;
Derived e = d;
Base b = d;
采用d初始化e,此时即使使用按位拷贝也不影响e中的虚表指针指向类Base的虚表。但是,当采用d去初始化其基类对象时,这个时候如果按位拷贝的话,将会使基类对象的虚表指针指向派生类的虚表了,影响了基类对象的正常初始化。这个时候就需要编译器的辅助来将初始化的派生类对象中虚表指针指向基类的虚表(如果不支持派生类对象初始化基类对象的话,这个时候也可以按位拷贝了)。
处理虚基类对象
当类的继承层次中出现虚基类时,与处理虚表指针一样,同样需要编译器来做到合理的初始化。对于虚基类来说,其在派生类中之存在一个实体,其背后的原理是通过指针来实现的。如,对于一下代码:
class Top;
class Left : virtual public Top
{};
class Right : virtual public Top
{};
class Bottom : public Left, public Right
{};
void VirtualInherient()
{
Top t;
Left l;
Right r;
Bottom b;
cout << "size of Top is " << sizeof(t) << endl;
cout << "size of Left is " << sizeof(l) << endl;
cout << "size of Right is " << sizeof(r) << endl;
cout << "size of Bottom is " << sizeof(b) << endl;
}
/*
* 以上代码的输出:
* 1
* 8
* 8
* 16
*/
对于类Top来说,其不含任何成员变量,所以其size为1,而对于类Left和Right的对象来说,其size为8,为什么是8,而不是1?这是因为Left (Right) 是虚继承的Top,类Lef (Right)的内部维护来一个指针Top *,指向一个Top对象,该指针的size是8。那么对于Bottom类来说,其继承了类Left和Right的Top *指针,因此其内部有两个指针,所以其size是16,当然类Bottom内的两个Top *指针指向同一个Top对象,这也就相当于类Bottom内只有一个Top实体。
了解了虚继承的原理之后就很容易想到, 在虚继承层次中,如果用一个派生类去初始化一个基类,比如用Bottom对象去初始化Left对象,如果按位拷贝基类指针的话,将会使初始化形成的Left对象与Bottom对象内的Top *指针指向同一个Top对象,显然这是一种浅拷贝,不符合拷贝的含义。因此在虚继承层次中需要编译器合成拷贝构造函数来正确的初始化其内部的虚对象指针。(如果有显式的拷贝构造函数,编译器会将相应的代码插入到程序员定义的拷贝构造函数中)
总结
总的来说,如果按位拷贝不能正确的初始化对象,那么编译器就会生成合成拷贝构造函数(代码)来完成对象的初始化工作。
文章评论