C++对象模型:第2章(1)-构造函数

来源:互联网 发布:公司公司招聘数据 编辑:程序博客网 时间:2024/06/02 09:02

一、默认构造函数

以下两个观点是错误的:

(1)任何类,如果没有定义默认构造函数,编译器就会自动合成一个(但是作者Stanley B. Lippman在Primer中说“任何类没有显式定义构造函数就会默认生成一个”,这两种说法有点冲突)。

(2)编译器合成的默认构造函数会显式将类的每个成员设定为默认值。


        有四种情况,会造成编译器必须为未声明构造函数的类合成一个默认构造函数。C++标准将这些合成的默认构造函数称作“隐式有用构造函数(implicit nontrivial default constructors)”。被合成的构造函数只能满足编译器(而不是程序)的需求;它之所以能完成任务是因为以下四种情况。

       如果类不是以下四种情况又没有声明任何构造函数,那么它们拥有的是implicit trivial default constructor,它们实际上并没有被合成出来。


 C++ standard:对于class X ,如果没有任何user-declared constructor,那么会有一个default constructor 被暗中(implicitly)声明出来,一个被暗中声明出来的default constructor将是一个trivial(浅薄而无能,没啥用的)constructor


        默认情况下,全局变量的内存会保证在程序启动时被初始化为0,但是局部变量位于程序的堆栈中,堆对象位于自由空间中,都不会被初始化为0,它们的内容是内存上次被使用时的“遗迹”

#include <iostream>#include <string>using namespace std;class A {public:int val; };int main() {A a1;  // A a1 = A();cout << a1.val << endl;}
上面的程序在VS2010下能够通过编译但是在执行时出错:提示“The variable ‘a1' is being used without being initialized”;但是在GCC下会输出负的随机值。

如果将main函数中第一个语句改为注释的那样:创建一个空的A的对象,然后将其赋值给a1,则会有如下结果:在VS2010下和在GCC均输出0.


要是将class A中再增加一个string成员,如下面代码:

#include <iostream>#include <string>using namespace std;class A {public:int val;         string str;};int main() {A a1;  // A a1 = A();cout << a1.val << endl;}
此时在VS2010下会输出随机值(而不是如第一段程序那样运行时错误);但是在GCC下会输出正的随机值。

如果将main函数中第一个语句改为注释的那样:创建一个空的A的对象,然后将其赋值给a1,则会有如下结果:VS2010会输出随机值,而GCC会输出0.



这两个程序的运行结果似乎表明,默认构造函数的行为很复杂,和编译器相关,具体看下面:

情况1:一个类的某成员带有默认构造函数时

         如果一个类没有任何构造函数,但是它的一个成员对象有默认构造函数,例如上面第二段程序:A类没有构造函数,但是它的成员string类型的str有默认构造函数,那么这个类的implicit default constructor就是“nontrivial”(有用的)的,编译器需要为该类合成一个默认构造函数。不过这个合成操作指引在构造函数真正需要调用时才会发生。被合成的默认构造函数包含必要的代码,能够调用A的成员str的默认构造函数,但是它并不产生任何代码来初始化A::val【因此本文开篇第二种说法错误】。是的,初始化str是编译器的责任,但初始化val则是程序员的责任

那么,程序员可以定义一个显式的构造函数来初始化val,如:

<pre name="code" class="cpp"><pre name="code" class="cpp">class A {public:        A() {  val = 0; }        int val;         string str;};

         这样的话,程序的需求满足了,但是编译器还需要初始化类成员str。由于默认构造函数被显式定义了,那么编译器没办法合成第二个,此时编译器的行动是:“如果类A内含有一个或一个以上的类成员对象,那么类A的每个构造函数必须调用每个类成员的默认构造函数”。编译器会扩张已经存在的构造函数,在其中插入一些代码,使得用户代码执行前,必须先调用必要的默认构造函数,那么扩张后的构造函数类似这样:

<pre name="code" class="cpp">class A {public:        A() {  val = 0; str = string(); }        int val;         string str;};

情况2:带有默认构造函数的基类

         和情况1类似:如果一个没有任何constructor的class派生自一个带有default constructor的base class,那么这个derived class的default constructor会被视为nontrivial,并因此需要被合成出来。

        如果设计者提供了多个构造函数,但其中没有默认构造函数,编译器同样会扩张现有的每个构造函数,将“用以调用所有必要之构造函数”的程序代码加进去。它不会合成新的默认构造函数,因为存在设计者显式定义的构造函数。

情况3:带有虚函数的类

        当一个类声明了或继承的类里有虚函数时,同样要合成默认构造函数。

当没有显式声明一个构造函数时,编译器会进行两个扩张行为:

(1)编译器会产生一个虚函数表(vtbl,virtual function table),表内存放虚函数地址;

(2)在每一个类对象中,编译器会额外合成一个指针成员(vptr,pointer member),该指针成员指向虚函数表的地址

        编译器必须为每个这种类的对象的vptr设定初值,放置适当的虚函数表的地址。对于类所定义的每个构造函数,编译器会安插一些代码来做这样的事情。对于那些未声明任何构造函数的类,编译器会为它们合成一个默认的,以便正确地初始化每个类对象的vptr。

情况4:带有虚基类的类

多重继承和虚继承的内存布局

如下的代码,Top类是一个虚基类:

#include <iostream>#include <string>using namespace std;class Top {public:Top():a(1) {}int a;};class Left: virtual public Top {public:Left(): b(2) {}int b;};class Right: virtual public Top {public:Right():c(3){}int c;};class Buttom: public Left, public Right {public:Buttom(): d(4){}int d;};void foo(const Right *pa) {pa->a = 1024;}int main() {        //这里输出类的大小,分别为4,12,12,24        cout << sizeof(Top) << " " << sizeof(Left) << " " << sizeof(Right) << " " <<sizeof(Buttom) << endl;foo(new Right);foo(new Right);}
在编译的时候函数foo无法确定pa->Top::a的实际位置,因为pa的真正类型是可变的。编译器只有在执行的时候才决定其位置。foo函数可能会被编译器写成这样:

void foo(const  Right *pa) { pa->__vbcX->a = 1024; }

__vbcX是编译器所产生的指针,指向虚基类Top,该指针时再类对象构造期间完成的。对于类定义的每个构造好桉树,编译器都会安插那些“允许每一个虚基类的执行期存取操作”的代码。如果类没有声明任何构造函数,编译器必须为它合成一个默认的。

0 0