C++默认合成的函数的误解
最近看《深度探索C++对象模型》时,对对象的构造有了粗略的了解,同时也发现了其中的一些内容和我在其他书本中了解的有些不同的地方。在这里记录下来。
《Effective C++》一书中 条款05:了解C++编写并调用哪些函数
当然这些内容在《c++ primer》也有讲到。
其中讲的是class Empty {};一个空类在使用的时候哪些函数会被默认生成。
在这里我定义一个有两个int 类型成员的类作为代替说明。
class Point
{
private:
int x;
int y;
};
//这就好像你写下
class Point
{
public:
Point(){}
Point(const Point &rhs)
{
x = rhs.x;
y = rhs.y;
}
~Point(){}
Point & operator = (const Point & rhs)
{
x = rhs.x;
y = rhs.y;
return *this;
}
private:
int x;
int y;
};
class Point
{
int x;
int y;
};
int main(int argc,char **argv)
{
Point e1; //默认构造 a
Point e2(e1);//copy 构造 b
e2 = e1; //copy assignment 操作 c
return 0;
}
根据书本《Effective C++》和《c++ primer》我们认为 在a 处 e1 需要调用Point的构造函数,由于我们没有定义任何一个构造函数,于是编译器会为Point 生成一个构造函数,并在a处调用它。b,c 处类似,当然在主函数返回之前,还需要对 e1,e2 调用析构函数。
但是真的事实如此吗?编译器真的会为这个类合成这些函数吗?
我们首先编译上面的这段代码,生成可执行文件。
然后使用objdump 进行反汇编。
反汇编后的输出文件片段,
虽然对汇编代码不是很熟悉,但是也可以发现在main中没有任何调用函数的操作,也就是构造函数、拷贝构造函数、拷贝赋值运算、析构函数都没有被调用。同样在反汇编后的文件中,也没有有关Point的函数被合成。所以当我们写下一个类时,并在使用时,那些可能会被调用的操作,实际上并没有被调用。那在什么情况下才会被调用。
1、我们真的定义这些函数。
//把刚才的 class Point {int a; int b;};替换为如下代码
class Point
{
public:
Point(){}
Point(const Point &rhs)
{
x = rhs.x;
y = rhs.y;
}
~Point(){}
Point & operator = (const Point & rhs)
{
x = rhs.x;
y = rhs.y;
return *this;
}
private:
int x;
int y;
};
然后重复编译和反汇编操作,生成的代码如下。
可以发现这些操作真的被调用了。
2、我们没有定义这些操作,但是编译器需要合成一个,已完成某种操作。但是编译器合成的操作只能满足编译器的需求,并没有满足程序的需求,程序的需求是希望对x,y 执行初始化,但编译器并没有这个责任。
对于一个构造函数,如果我们没有定义任何构造函数,编译器在什么情况下会合成一个呢
- 含有Member Class Object 而且它含有一个 Default Constructor,被合成出来的Default Constructor 的操作就是为了调用 Member Class Object 的Default Constructor
- 带有Base Class ,并且Base Class 中含有Default Constructor ,被合成出来的Default Constructor 的操作就是为了调用 Base Class Object 的Default Constructor,Base Class 可能有多个,根据声明顺序被调用。
- Class 里 含有 Virtual Function,无论是自己声明,或者来自继承链,编译器都会为该类安插一个vptr 指针,以指向该类的virtual function table,在构造函数中设置vptr的值,以通过该vptr 来完成某种特定的操作。
- Class 带有一个Virtual Base Class,带有Virtual Base Class会使对Virtual Base Class的成员的存取操作变得复杂,编译器会安插指针,已完成正确的操作。Virtual base Class 比较复杂,这里说的比较简单
对于构造函数如果没有定义,编译器会按照上面的规则来合成。
其他的函数,如果没有定义,能被合成出来也是有一定规则的。后面会在补充。
所以编译器合成出来的构造函数是为了完成某种操作,如果没有任何操作需要完成,那编译器也就没有必要为它合成一个。否则的话就浪费了无用的函数调用。