使用一个对象的内容作为另一个类对象的初值,有三种情况:
对一个对象做显式的初始化
1
2class X { ... };
X xx = x;当对象被当作一个函数的参数时
当函数返回一个类对象时
C++ 标准规定,如果类没有声明一个拷贝构造,就会有隐式的声明或者隐式的定义出现。同样的,C++标准会把拷贝构造函数分为“有用的”、和“无用的”两种。只有“有用的”的实例才会被合成于程序之中。决定一个拷贝构造函数是否有用的标准在于类是否展现出所谓的“bitwise copy semantics”。
一、Bitwise copy semantics(逐位拷贝)
什么时候一个类不展现出 “bitwise copy semantics” 呢?有 4 种情况:
- 当一个类含有一个成员类对象,并且这个成员类对象所在的类声明有一个拷贝构造时(无论是被程序猿显式声明、还是被编译器隐式合成)
- 当一个类继承一个基类,并且这个基类存在一个拷贝构造函数时(无论是程序猿显式声明、还是被编译器隐式合成)
- 当一个类声明了一个或多个虚函数时
- 当一个类派生自一个继承串链,其中有一个或多个虚基类时
前两种情况中,编译器必须将 数据成员 或 基类 的拷贝构造函数插入到被合成的拷贝构造中。后两种情况解释下:
如果一个类对象中存在虚表指针,因此不能“Bitwise copy semantics”,需要编译器合成出一个拷贝构造将虚表指针初始化
当一个子类对象赋值给基类对象时,他的虚表指针的复制操作需要保证安全。
1
2
3Derived derived;
Base base = derived; // 子类对象直接复制给基类对象,会导致对象被切割
// 同时 Base 类的拷贝构造对于虚表指针的设置,一定是基类的虚表指针,而不是子类的虚表指针。因此编译器如果需要合成拷贝构造,合成出来的拷贝构造函数中,一定会显式的正确设置对象的虚表指针,不会直接拷贝对象虚表指针(尤其对于存在基类和子类的情况)
如果是相同的类,不同的对象之间进行拷贝,那么此时也可以进行“Bitwise copy semantics”
一个类对象如果以另一个对象作为初值,而后者有一个虚基类,那么也会使 “Bitwise copy semantics” 失效。每一个编译器对于虚拟继承的支持承诺,都代表必须让子类对象中的 “虚基类子对象” 位置在执行期前就准备妥当。维护位置的完整性是编译器的责任。“Bitwise copy semantics” 可能会破坏这个位置,所以编译器必须在他自己合成出来的拷贝构造中做仲裁。
问题发生在一个类对象以其 “子类对象” 作为初值时,不会进行 “Bitwise copy semantics”。而对于相同的类对象之间的赋值,则不会有影响。
二、对象作为函数参数
有两种实现方法,一种是:
1 | X xx; |
他进行了一次默认构造,然后又进行了一次拷贝构造(“Bitwise copy semantics” 方式)。效率降低。我们不希望这种传参方式,而是希望以指针或引用的方式。
另一种是:
把实际参数直接建构在其应该的位置上,此位置视函数活动范围的不同,记录于程序堆栈中。在函数返回之前,局部对象的析构(如果有定义的话)会被执行。
在 STL 也是对应 push_back 和 emplace_back 的实现方式
三、对象作为函数返回值
1 | X bar() { |
这个 bar 函数的返回值如何从局部对象 xx 中拷贝过来呢?如下是一种解决方案。
- 首先加上一个额外参数,类型是类对象的一个引用。这个参数用来放置被 “拷贝构建” 而得的返回值
- 在 return 指令之前插入一个拷贝构造操作,以便将待传回对象的内容当作上述新增参数的初值。这个转化操作会重写函数,使他不传回任何值
1 | void bar(X& __result) { // 加上了一个额外参数 |
因此编译器会对代码做转换操作:
1 | 原来是如下这种对象作为函数返回值 |
或者还有其他逻辑实现,如下:
1 | 原来的代码直接使用返回值,用此返回值调用 memfunc 函数 |
同理,如果程序声明了一个函数指针,如下:
1 | X (*pf)(); |