一个类,如果没有任何的用户声明的的构造函数,那么会有一个默认的构造函数被隐式地声明出来。这个被隐式声明的构造函数,究竟什么时候被合成、被编译器合成的默认构造函数究竟执行怎么样的操作,编译器如何处理用户定义的构造函数,就是本文要探讨的问题。
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,并设定初值)。
对于此类情况,编译