C++拷贝构造函数的合成

默认构造函数和拷贝构造函数在必要的时候才由编译期合成出来



拷贝构造函数

拷贝构造函数,通俗地理解是以一个对象的内容作为另一个对象的初值。
有三种情况,会以另一个对象的内容作另一个对象的初值:

  1. 对一个对象明确的初始化操作
class X {...};
X x;

// 1
X xx = x;

// 2
X xx(x);
  1. 当对象作为参数传进某个函数
extern void foo( X x );

void bar()
{
	X xx;
	foo( xx );
}
  1. 当函数传回一个类对象
X foo_bar()
{
	X xx;
	// ...
	return xx;
}

下面的程序代码结合了上面三种情况

#include <iostream>
using namespace std;

class X
{
public:
    X() = default;
    X(const X& x)
    {
		m_x = x.m_x;
		cout << "copy constructor" << endl;
	}
    int m_x;
};

X foo(X x)
{
    x.m_x = 5;
    cout << "foo" << endl;
    return x;
}


int main()
{
    X x{};

    X xx(x);

    foo(xx);
}

输出如下,可以看到调用三次拷贝构造函数

通过VS2022下的汇编码也可以看到调用了三次拷贝构造函数,main 函数中对应前两种情况,foo 函数中对应最后一种情况


编译器合成拷贝构造函数的四种情况

上面的例子中提供了显式的拷贝构造函数,如果类没有提供显式的拷贝构造函数,编译器会作何操作,考虑下面这个例子。

我们有一个类 String,其只包含基础元素

class String{
public:
// ... 没有显式的拷贝构造函数
private:
	char* str;
	int len;
};

那么编译器实际不会合成出一个拷贝构造函数,而是进行逐元素初始化

其完成方式就好像逐个设定每一个成员一样

// 语意相等
verb.str = noun.str;
verb.len = noun.lenl

如果 String 对象是另一个类的成员,比如下面的 Word

class Word {
public:
	Word(String& word, int occurs) : _word(word), _occurs(occurs) {}
private:
	int _occurs;
	String _word;
};

那么先拷贝Word 的内置基础变量 _occurs,然后再拷贝 _word 中的基础变量,[word] 位置对应 _occurs 的地址,后面的 [ebp-30h],[ebp-44h]分别对应word1word2 中的 String 对象成员的地址,可以通过下面的地址推出。


当然如果 _word_occurs 调换顺序,那么初始化的顺序也会调换过来


情况一 一个类有一个带有拷贝构造函数的类对象成员变量

当类中含一个成员对象,这个成员对象对应的类声明有一个拷贝构造函数时(无论是类设计者明确地声明,或是被编译器合成)。
比如下面有一个类 Word 其有一个成员对象

class Word {
public:
    Word( const String& s, int c) : str( s ), cnt( c ) {}
private:
    int cnt;
    String str;
};

成员对象 str 对应的类 String 有一个定义好的拷贝构造函数

class String {
public:
    String(const char* s)
    {
        str = new char[strlen(s) + 1];
        strcpy_s(str, strlen(s) + 1, s);
    }
    String(const String& s) : str(s.str) { std::cout << "调用String的拷贝构造函数" << std::endl; }
private:
    char* str;
};

那么为了调用 String 下的拷贝构造函数,编译器会为 Word 合成一个拷贝构造函数,类似如下代码:

inline Word::Word( const Word& wd )
{	
	str.String::String( wd.str );
	cnt = wd.cnt;
}

汇编代码中也可以看到调用了 Word 的拷贝构造函数


情况二 派生类的基类有一个拷贝构造函数

当类继承自一个基类,该基类存在一个拷贝构造函数(也是无论是明确声明或是被编译器合成得到的),此时编译器都会为该派生类合成一个拷贝构造函数,以调用基类的拷贝构造函数。

比如一个类 WordSuper 派生自上面的 Word

class WordSuper : public Word {
public:
	WordSuper( const String& s, int c ) : Word( s, c ) {}
};

我们知道 Word 类有一个编译合成的拷贝构造函数,那么编译器也会为 WordSuper 合成一个拷贝构造函数

进入到该拷贝构造函数内部,可以看到其调用了 Word 的拷贝构造函数


类声明了一个或多个虚函数

之前谈到虚函数表,要在对象创建时初始化好其虚函数表指针 vptr,那么编译器对于每一个构造函数都应该在代码前插入对虚函数表指针 vptr 的初始化操作,那么如果此时没有拷贝构造函数,编译器应该要合成一个拷贝构造函数。

比如下面的类 X 有一个虚函数 f() ,且没有显式的拷贝构造函数

class X
{
    virtual void f() {
    std::cout << "X::f()" << std::endl;
    }
};

那么编译器会为其生成一个拷贝构造函数

也可以看到编译器合成的拷贝构造函数中对虚函数指针进行了初始化,使其指向合适的虚函数表地址

当然对于上面的 case,逐位拷贝也是可行的,但是考虑到如果此时有一个类 Y 继承自 X 并重写了虚函数 f(),那么其虚函数指针指向不同的虚函数表(当然不重写也是指向不同的虚函数表),那么此时我们用 Y y; X x = y;,如果用逐位拷贝,那么会导致 x 的vptr指向类 Y 的虚函数表,那么是不正确的。


情况四 类派生自一个继承串联且有一个或多个虚基类

这种也和初始化虚函数表指针 vptr 类似,有虚基类,对象在创建时,需要初始化 vbtr,使得其指向合适的虚基类表地址,虚基类表中包含每个虚基类,在该类中的地址偏移。

考虑下面这个 case

#include <iostream>

class ZooAnimal {
public:
    int m_x;
};

class Raccoon : public virtual ZooAnimal
{
public:
    int m_y;
};

class RedPanda : public Raccoon
{
public:
    int m_z;
};
int main()
{
    Raccoon rocky{};
    Raccoon little_critter = rocky;

    RedPanda little_red;
    Raccoon little_critter2 = little_red;
}

使用逐位拷贝来用 Raccoon 对象来初始化 Raccoon 对象是够用的,但是如果用其派生类 RedPanda 来初始化,那么逐位拷贝会出问题,会将 RedPanda 对应的 vbtable 的地址错误地初始化给 Raccoon 类型的对象,所以此时必须要有一个编译器合成的拷贝构造函数来完成正确的初始化 vbptr。

可以看到在编译器合成的拷贝构造函数中,初始化了 vbptr。


参考资料

《深度探索C++对象模型》—— Stanley B.Lippman著,侯捷译

  • 13
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值