默认构造函数和拷贝构造函数在必要的时候才由编译期合成出来
文章目录
- 拷贝构造函数
- 编译器合成拷贝构造函数的四种情况
- 情况一 一个类有一个带有拷贝构造函数的类对象成员变量
- 情况二 派生类的基类有一个拷贝构造函数
- 类声明了一个或多个虚函数
- 情况四 类派生自一个继承串联且有一个或多个虚基类
- 参考资料
拷贝构造函数
拷贝构造函数,通俗地理解是以一个对象的内容作为另一个对象的初值。
有三种情况,会以另一个对象的内容作另一个对象的初值:
- 对一个对象明确的初始化操作
class X {...};
X x;// 1
X xx = x;// 2
X xx(x);
- 当对象作为参数传进某个函数
extern void foo( X x );void bar()
{X xx;foo( xx );
}
- 当函数传回一个类对象
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]分别对应word1
和 word2
中的 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著,侯捷译