C++对象模型——默认构造函数的合成
最近在学习C++对象模型,看的书是侯捷老师的《深度探索C++对象模型》,发现自己以前对构造函数存在很多误解,作此笔记记录。
默认构造函数的误解##
1.当程序猿定义了默认构造函数,编译器就会直接使用此默认构造函数####
来一个简单的栗子
class Student;
class School
{
public:
School(){}
...
Student students;
};
我们知道,一个对象,在定义的时候就一定会调用其构造函数。而在我们上面的默认构造函数,明显没有调用students的构造函数,所以,编译器绝对没有直接使用我们的默认构造函数。具体细节,请看下文,这里只提问题。
2.当程序猿没有定义构造函数的时候,编译器就会帮我们定义默认构造函数####
接下来,就让我们带着这些错误的看法来进入正文。
为什么要帮我们定义默认构造函数##
再来个简单的栗子
class Student
{
public:
Student(){} //有定义
void Study(); //只给出了声明,没有定义
...
};
void main()
{
Student stu;
//stu.Study(); //调用没有定义的函数
}
上面是一个可以编译,连接,运行的例子完整代码。其中,Study()函数只有声明,但是没有定义,但是却通过了编译?为什么呢?因为你没有用到它。即使,你将注释的那行代码取消注释,它也不会在编译期出错,只会等到连接的时候编译器才会提示错误。具体可以参考这篇博客,依样画葫芦。你也可以先不看,记住这样的话:编译器没有具体需要用到某个函数时(上面是因为代码中没有调用Study函数),这个函数可以没有实现。所以,你可以在你的代码中很不负责任地声明很多没有用的函数并且不对其中的任何一个进行实现。
回到我们的内容,“为什么要帮我们定义默认构造函数”,答案就是编译器要用到,但是你却没有给出明确定义。注意,这里不是程序需要,而是编译器需要。程序需要用到,是指我们希望class中的成员,基类等能够正常地值初始化。而编译器需要,是指,没有这个函数,编译连接工作就没办法正常地进行。
那么问题就来了,编译器具体什么时候有这个需求。
在四个需求下,编译器需要一个默认构造函数##
第一个需求####
如果一个class没有任何constructor,但它内含一个member object,而后者有default constructor,那么这个class的implicit default constructor就是“nontrivial",编译器需要为该class合成一个default constructor。不过,这个合成操作只有在constructor真正需要被调用时才会发生。
这里引用了书里的话。nontrivial的意思就是有用的。举个例子说明一下。
class Student
{
public:
Student(){}
...
};
class School
{
Student students; //不是继承,是内含
char* name;
...
};
void main()
{
int a;
School school; //合成操作在这里发生
}
上面的例子中,编译器为School类合成了一个default constructor,因为School中包含的Student具有默认的构造函数,而我们在构造School的时候,需要调用Student的默认构造函数,所以编译器就帮我们合成了一个大概样子如下的默认构造函数。
School::School()
{
students.Student();
}
注意:School::name初始化是程序的需求,而不是编译器的需求。所以,合成的构造函数不会完成对name的初始化。时刻分清,编译器的需求与程序的需求不是一回事。
回到上面的程序,编译器在main中的第二行代码中才进行了合成。还记得在上一部分中我们提到的那些只声明没有定义的函数,无独有偶,假如我们在上面的代码中没有实例化School,那么这个合成操作永远不会进行,因为编译器不需要!!!只有当需要用到这个默认构造函数的时候,编译器才会进行合成。
这里还有一个问题,假如我们自己定了构造函数,却没有调用内部对象的构造函数时,编译器还会合成一个新的构造函数吗?否。编译器只会在已有的构造函数里面插入”编译器需要“的代码。再来个简单的栗子。
class Student
{
public:
Student(){}
...
};
class School
{
public:
School(){name = NULL} //没有初始化students
Student students; //不是继承,是内含
Student students2;
char* name;
...
};
//编译器插入自己需要的代码,最后的构造函数类似如下
School::School()
{
//在用户自己的代码前插入,确保所有的对象在使用前已经初始化
students.Student();
students2.Student(); //存在多个对象,按照声明的顺序进行插入
name = NULL;
}
第二个需求####
如果一个没有任何constructor的class派生自一个"带有default constructor"的base class,那么这个derived class的default constructor会被视为nontrivial,并因此需要被合成出来。
这一点与第一个需求很相似。需要记住的有以下几点。
1.在derived class的constructor(已有或者合成)中,编译器除了插入member class object的constructor外,还会插入base class constructor。
2.base class constructor的调用时间在member class object之前。
第三个需求###
class声明(或继承)一个virtual function,当缺乏程序猿声明的constructor,编译器合成一个default constructor。
我们知道,virtual function在调用的过程中,具体的函数是在编译器是不可知的。比如
class Base
{
public:
Base();
virtual void Print();
};
class Derived:public Base
{
public:
Derived();
virtual void Print();
};
void Print(Base *para)
{
para->Print();
}
void main()
{
Base a;
Derived b;
Print(a); //调用Base::Print();
Print(b); //调用Derived::Print();
}
编译器如何得知调用哪一个Print()呢?当class中含有virtual function的时候,编译器会在class中插入“虚表指针",并在构造函数中进行初始化。虚表指针指向了虚函数表,里面记录了各个虚函数的地址。程序之所以能够实现多态,实际上就是因为调用虚函数的时候,动态地使用了这个表里面的地址。
回归一下正题,在这里要强调的是,当存在虚函数的时候,编译器需要构造虚表指针。所以,假如我们没有任何构造函数的时候,编译器就会合成一个默认构造函数,里面满足除了前面“第一个需求,第二个需求”外,还会在在Base class完成构造之后,完成虚表指针的初始化。假如我们已经定义了构造函数,那么就会在base class constructors之后,member initialzation list之前完成虚表指针的初始化。
第四个需求###
class派生自一个继承串链,其中有一个或更多的virtual base classes,当没有程序猿定义的constructor的时候,编译器就会合成一个默认构造函数。举个例子
class X
{
public:
int i;
};
class A:public virtual X{};
class B:public virtual X{};
class C:public A,public B{};
void Foo(const A *pa)
{
pa->i = 1024;
}
void main()
{
Foo(new A);
Foo(new C);
}
编译器没办法确切地知道“经由pa"而存取的X::i的实际偏移位置,因为pa的真正类型可以改变。编译器必须改变”执行存取操作“的那些代码,使X::i可以延迟至执行期才决定下来。对于class定义的每个constructor,编译器会安插那些”允许每一个virtual base class“执行期存取操作的代码。如何class没有声明任何constructor,编译器必须为它合成一个default constructor。
最后这一点的具体实现讲解起来是一个长的过程,限于个人目前理解不透彻,暂时先搁着。有兴趣的读者可以参考《深度探索C++对象模型》5.2节的内容。