C++对象模型之默认构造函数的构造操作

一个类,如果没有任何的用户声明的的构造函数,那么会有一个默认的构造函数被隐式地声明出来。这个被隐式声明的构造函数,究竟什么时候被合成、被编译器合成的默认构造函数究竟执行怎么样的操作,编译器如何处理用户定义的构造函数,就是本文要探讨的问题。

1、默认构造函数何时被合成

如果一个类没有任何的用户声明的构造函数,那么在当编译器需要的时候,编译器会为类合成一个默认的构造函数,它只用于执行编译器所需要的操作。注意,默认的构造函数是在编译器需要的时候被合成出来,而不是程序需要的时候,如果程序需要,则默认的构造函数应该由程序员实现。

那么编译器需要的时候是什么时候呢?正确来说,编译器需要的时候是遇到如下四种情况的时候,它需要为以下四种类型的类合成一个默认的构造函数:

1)类的成员变量带有默认构造函数

2)类的基类带有默认构造函数

3)类带有virtual函数

4)类带有一个virtual基类
且合成操作只有在构造函数真正需要被调用时才会被合成。

现在还有一个问题就是,在C++中各个不同的编译模块中,编译器如何避免合成多个默认呢?解决方法是把合成的默认构造函数、复制构造函数、析构函数、赋值操作运算符等都以inline的方式完成。如果函数太复杂,不适合做成inline,就会成成一个显式非inline的static函数。无论是inline还是非inline的static函数,其作用都是为了不被文件以外者访问。

对于下面的这段程序:

class X
{
    public:
        int mData;
        int *mPtr;
}; 

类X没有声明任何的构造函数,但是它并不属于上述所说的4种类型的类,所以编译器并不会为类X合成一个默认的构造函数。

下面详细分析编译器为上述4种类型的类合成的默认构造函数的行为。

2、 类的成员变量带有默认构造函数 (即类含有成员类对象(member class objects ))

这种情况是指:一个类没有任何构造函数,但它的成员变量是一个有默认构造函数的类的变量。

例如,如下代码所示:

class X
{
    public:
        X(){mData = 0; cout << "X::X()" << endl;}
        int mData;
};

class Xs
{
    public:
        X mX;
        int mN;
};

int main()
{
    Xs xs;
    cout << xs.mX.mData << endl;
    cout << xs.mN << endl;
    return 0;
} 

类Xs没有定义任何构造函数,但是其成员变量mX是类型是X,且类X拥有一个默认的构造函数,其运行结果如下:

从运行的结果可以看出,在编译器为类Xs合成的默认构造函数中,调用了X的构造函数为其成员变量mX进行初始化,但是该合成的默认构造函数却没有对类Xs的成员变量mN进行初始化。可见,编译器为类Xs合成的构造函数如下伪代码所示:

inline Xs::Xs()
{
    mX.X::X();
}

有时候我们会为类定义一个默认构造函数,但是在该构造函数中,只初始化部分的成员变量,那么会发生什么样的行为和效果呢?保持类X和main函数的测试代码不变,修改类Xs的代码为如下:

class Xs
{
    public:
        Xs()
        {
            cout << "Xs::Xs()" << endl;
            mN = 1;
        }
        X mX;
        int mN;
}; 

可以看到,我们为类Xs添加了一个默认的构造函数,但在该构造函数中,我们只为成员变量mN进行初始化,其运行结果如下:

由运行结果可以看出,虽然在类的Xs的默认构造函数中,我们没有显示地对mX进行初始化,但是类X的默认构造函数还是被调用了,且其调用顺序还在类Xs的默认构造函数函数体代码的前面。

由此可见,编译器的行为是:如果类内含有一个或多个类成员对象(member class object),那么该类的每一个构造函数必须调用每一个类成员的默认构造函数(按照成员声明顺序)。编译器会扩张已存在的构造函数,在其中安插一些代码,使得用户的代码被执行之前,先调用必要类成员的默认构造函数。

对于此时类Xs的构造函数可用以下伪代码表示:

inline Xs::Xs()
{
    mX.X::X();
    cout << "Xs::Xs()" << endl;
    mN = 1;
} 

对于此类情况,编译器合成默认的构造函数或向已有的默认构造函数中插入代码的意义在于:使每个类成员变量都得到初始化。

3、类的基类带有默认构造函数

这种情况是指:一个没有任何构造函数的类派生自一个带有默认构造函数的类。

例如,如下代码所示:

class X
{
    public:
        X(){mData = 0; cout << "X::X()" << endl;}
        int mData;
};
class XX : public X
{
    public:
        int mN;
};
class XXX : public XX
{
};

int main()
{
    XX xx;
    cout << xx.mData << endl;
    cout << xx.mN << endl;

    XXX xxx;
    return 0;
}

类XX没有任何构造函数,但是其基类X存在一个默认的构造函数,其运行结果如下:

从运行结果可以看出,编译器合成的构造函数调用上一层基类的默认构造函数。对于一个后继派生的class而言,这个合成的默认构造函数与一个被显式提供的默认构造函数无异。

如果类的设计者提供了一个或多个构造函数(包括默认构造函数),但是在其提供的构造函数中并不显式地调用其基类的构造函数,编译器会如何处理呢?

为类XX加上两个构造函数,修改main函数的测试代码,如下所示:

class XX : public X
{
    public:
        XX()
        {
            mN = 1;
        }
        XX(int n)
        {
            mN = n;
        }
        int mN;
};

int main()
{
    XX xx1;
    XX xx2(2);
    return 0;
} 

类XX的构造函数都没有显式地调用其基类的构造函数。其运行结果如下:

从运行结果可以证明:编译器会扩张派生类的每一个现在的构造函数,将要调用的所有必要的默认构造的程序代码加插进去。注意:由于已经存在用户自定义的构造函数,所以编译器不再合成新的构造函数。

对于此类情况,编译器合成默认的构造函数或向已有的默认构造函数中插入代码的意义在于:确保类的基类子对象得到初始化。

4、类带有virtual函数

这种情况是指:类声明或继承了一个或多个virtual函数。

例如,对于以下的代码:

class X
{
    public:
        virtual ~X()
        {
            cout << "X::~X()" << endl;
        }
        virtual void print()
        {
            cout << "X::print()" << endl;
        }
        int mData;
};
class XX : public X
{
    public:
        virtual ~XX()
        {
            cout << "XX::~XX()" << endl;
        }
        virtual void print()
        {
            cout << "XX::print()" << endl;
        }
        int mN;
};
int main()
{
    X *x = new XX;
    x->print();
    delete x;
    return 0;
} 

类X有一个virtual函数print和一个virtual析构函数,其运行结果如下:

从运行结果可以看出,利用X的指针调用print函数,调用的是类XX的print函数,且在delete析构时也是调用了类XX的析构函数,先析构派生类再析构基类。

所以,由于虚函数的加入,编译器会在编译期间发生如下操作,以使虚函数机制发挥作用:
1)虚函数表(vtbl)会被编译器产生而来,用于存放为类的virtual函数地址。

2)在每一个类的对象内,一个额外的指针成员会被编译器合成出来,这个指针就是指向虚函数表的指针(vptr)。

此外,编译器还会改写虚函数的调用。例如,上述的main函数可能会被改写成如下的伪代码

int main()
{
     X *x = malloc(sizeof(XX));
    x->vptr = XX::vtbl;
    (x->vptr[1])(x); // 1为print函数在vtbl中的索引
    (x->vptr[0](x); // 0为析构函数在vtbl中的索引
    free(x);
    return 0;
} 

所以,编译器合成的默认构造函数会为类安插一个vptr,并指定其初值,使其指向该类的虚函数表。若该类已经定义了构造函数,编译器会为每个构造函数安插代码来做同样的事情(即安插vptr,并设定初值)。

对于此类情况,编译器合成默认的构造函数或向已有的默认构造函数中插入代码的意义在于:使virtual函数机制(多态)可以正确地生效。

5、类带有一个virtual基类

这种情况是指类派生自一个继承串链,其中有一个或更多的virtual基类。

virtual基类的实现方法 在不同的编译器之间有极大的差异,但是每一种实现的共同点是必须使virtual基类在其每一个派生类对象中的位置,能够在执行期间准备妥当。

例如,对于以下的代码(并不是设计良好的代码):

class X
{
    public:
        X() {mData = 100;}
        int mData;
};

class X1 : virtual public X
{
    public:
        X1() {mX1 = 101;}
        int mX1;
};

class X2 : virtual public X
{
    public:
        X2() {mX2 = 102;}
        int mX2;
};

class XX : public X1, public X2
{
    public:
        XX() {mXX = 103;}
        int mXX;
};

int main()
{
    cout << "sizeof(XX): " << sizeof(XX) << endl;
    XX xx;
    int *p = (int*) &xx;
    for (int i = 0; i < sizeof(XX) / sizeof(int); ++i, ++p)
    {
        cout << *p << endl;
    }
    return 0;
}

在main函数中遍历并输出对象的内容,其运行结果如下:

从结果可以看到,即像在所有的类中都没有定义virtual函数,编译器还是会为子类XX插入了两个vptr,其中第一个vptr属于X1,第二个vptr属于X2。至于它有什么作用,我还不清楚,但是可以肯定的是编译器会为我们定义的构造函数安插必要的代码来实现virtual基类的机制。若类没有声明任何的构造函数,编译器必须合成一个默认构造函数来完成相同的操作。

注:若基类X中有virtual函数,则还会安插一个X::vptr,它的位置在100之前,即在基类X的成员变量之前。

6、总结

满足上述4种情况之一或以上的类,若没有声明任何的构造函数,编译器会为其合成一个默认的构造函数;若已声明一个或多个构造函数,则编译器会为每个构造函数安插一定的代码来完成编译必要的工作。这些被合成的构造函数称为implicit nontrivial default constructors(隐式有效的默认构造函数)。

不满足上述4种情况的类,而又没有声明任何构造函数,则该类拥有的是implicit trivial default constructors(隐式无效的默认构造函数),实际上不会被合成出来。

合成出来的默认构造函数,只会初化化基类子对象和成员类对象,其他的非static成员变量并不会被初始化。

此外,C++有两个常见的误解:

1)任何class如果没有定义默认构造函数,就会被合成出来。
2)编译器合成的默认构造函数,会显式设定class内每一个成员数据的默认值。

时间: 2024-10-26 06:50:12

C++对象模型之默认构造函数的构造操作的相关文章

默认构造函数的构造操作

以下四种情况,编译器将会合成(即由编译器负责)一个nontrivial默认构造函数. 1.成员对象带有默认构造函数 例如: #include <iostream> using namespace std; class Foo { public: Foo() { cout << "Foo" << endl; } // Foo类含有默认构造函数 }; class Bar { public: Foo foo; int x; }; int main() {

C++对象模型之复制构造函数的构造操作

复制构造函数用于根据一个已有的对象来构造一个新的对象. 1.构造函数何时被调用 有三种情况会以一个对象的内容作为另一个类的对象的初值构造一个对象,分别是: 1)对一个对象做显示的初始化操作时,如 class X { ... }; X x; X xx = x; // 或 X xx(x); 2)当对象被当作参数传递给某个函数时 3)当函数返回一个类的对象时 2.默认的成员复制初始化 如果class没有提供一个显式的复制构造函数,当class的对象以另一个对象作为初值进行构造时,其内部是以这样的方式完

复制构造函数的构造操作

今天真机调试的时候莫名其妙遇到了这样的一个问题: This product type must be built using a provisioning profile, however no provisioning profile matching both the identity "iPhone Developer" and the bundle identifier..... 具体如下图所示: 十分蛋疼, 发现不管是从网上下的demo, 还是自己的过程.凡事真机测试的时候都

C++对象模型之默认构造函数

在不声明自定义构造函数时,编译器会自动生成一个默认构造函数.但是这个默认构造函数有可能是一个trivial(无用的) constructor,也可能是nontrivial constructor. 举个例子 class Foo { public: int val; Foo* pnext; } void foo_bar() { Foo bar; if(bar.val || bar.pnext) //...do something } 之前的想法是Foo有一个默认构造函数,可以将var和pnext初

C++ 合成默认构造函数的真相

对于C++默认构造函数,我曾经有两点误解: 类如果没有定义任何的构造函数,那么编译器(一定会!)将为类定义一个合成的默认构造函数. 合成默认构造函数会初始化类中所有的数据成员. 第一个误解来自于我学习C++的第一本书 <C++ Primer>,在书中392页:“只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数”. 实际上这句话也没有说错,它说明了默认构造函数定义的必要非充分条件,然而却给当时初学C++的我造成了一定的误解. 第二个误解依旧来自于Primer中的一句话:“合成的

深度探索C++对象模型之第二章:构造函数语意学之Default constructor的构造操作

C++新手一般由两个常见的误解: 如果任何class没有定义默认构造函数(default constructor),编译器就会合成一个来. 编译器合成的的default constructor会显示的设定“class内每一个data member的默认值” 一.编译器在哪种情况下才会合成默认构造函数: 对于未声明构造函数的类,只有在以下四种情况下编译器才会为它们合成默认构造函数: 类的成员有一个类对象(Member Class Object),且该成员含有默认构造函数(default Const

c++值构造兼默认构造函数

friend ostream& operator<< (ostream&,String&); public: String(const char* str=NULL); //赋值构造兼默认构造函数(char) String(const String &other); //赋值构造函数(String) String& operator=(const String&other);       //operator= String operator+(

【C++对象模型】构造函数语意学之一 默认构造函数

默认构造函数,如果程序员没有为类定义构造函数,那么编译器会在[需要的时候]为类合成一个构造函数,而[需要的时候]分为程序员需要的时候和编译器需要的时候,程序员需要的时候应该由程序员来做工作,编译器需要的时候则由编译器来做工作. C++中,全局变量 / 对象的内存会被清零(如果类对象没有程序员定义的构造函数的时候), 而堆heap 或 栈stack上的变量或对象则不会被清零,其内存只取决于上一次这段内存的值. 而编译器什么情况下会为类合成默认构造函数呢?会在以下四种情况合成默认的构造函数: 1.类

C++:派生类的默认构造函数和拷贝构造函数调用基类构造函数的机制(含程序验证)

1.如果基类定义了不带参数的默认构造函数,则编译器为派生类自动生成的默认构造函数会调用基类的默认构造函数. 2.如果基类定义了拷贝构造函数,则编译器为派生类自动生成的拷贝构造函数同样会调用基类的拷贝构造函数. 3.如果基类定义了带参数的构造函数,派生类没有定义任何带参数的构造函数,则不能直接调用基类的带参构造函数,程序编译不通过. 代码如下: #include<iostream> using namespace std; //基类Game,定义了两个构造函数和一个拷贝构造函数 class Ga