概述
我们都知道,编译器会给我们书写的类合成一些函数:构造函数、拷贝构造、拷贝赋值、析构函数等等。合成的函数都是public且inline的。但是什么时候合成呢?带着疑问我们往下看。
1、什么是构造函数?
构造函数就是一类特殊的成员函数,用来控制对象的初始化过程。无论何时类的对象被创建,就会自动调用构造函数,构造函数不能手动调用。
语法:函数名与类名相同,并且没有返回类型的函数。
说明:
-
类可以包含多个构造函数,和重载函数差不多。
-
构造函数不能被声明成const的。
-
跟防控属性无系,可以是public、protected或者private。
class SalesData
{
SalesData(){}; //没有参数的构造函数
SalesData(int a = 5, char c = ‘a’){}; //含有默认参数的“无参构造”
SalesData(int ia, int ib){}; //含有两个int参数的构造函数
}
通过上面的例子,我们可以看出:无参构造并一定是没有参数的,而是可以用无参的形式被调用。
但是实际上我们写代码时,有时会不写构造函数,但是为啥类的对象也可以创建?
2、默认构造
如果我们没有编写构造函数时,编译器会在需要的时候给我们合成一个默认的构造函数。注意是在需要的时候,什么时候是需要的时候?先看一个列子:
class Foo
{
public:
int ival;
Foo* pNext;
};
这里正确的语意是要求Foo有一个构造函数,可以将两个成员变量初始化为0。但是这里编译器并没有合成构造函数。其间的差别在于一个是程序的需要,一个是编译器的需要。程序有需要那是程序员的责任,上面的例子就是程序的需要,而不是编译器的需要。
只有编译器合成出的构造函数是有用的,可以进行一些默认调用时,编译器才会合成。主要分为了下列四种情况:
2.1、类中包含了其它含有默认构造函数的类对象成员变量
class A
{
public:
A(){}; //含有一个默认构造函数
}
class B
{
A a; //拥有含有默认构造函数的类对象成员变量
char* str;
}
int main()
{
B b; //B::a 必须在此处初始化
return 1;
}
被合成的默认构造能够调用类A的构造函数来初始化成员变量a,但它不产生任何代码来初始化B::str。str的初始化是程序员的责任。被合成的默认构造类似这样:
B::B()
{
a.A::A();
}
被合成的默认构造只是满足编译器的需要,而不是程序的需要。为了让程序能够正确的执行,字符指针str也应该被初始化。需要程序员自己书写构造函数:
B::B(){ str = NULL; }
如果我们书写了一个构造函数,则编译器就不会给合成构造函数了。但是上面的列子也没有显示的调用成员变量的构造函数,怎么办呢?
编译器会扩张已存在的构造函数,在其中安插一些代码,使得用户书写的代码被执行之前,先调用必要的构造函数。上面的代码扩展之后
B::B()
{
a.A::A(); //扩展代码
str = NULL;
}
如果有多个成员变量是拥有构造函数的类对象,则按照声明顺序进行赋值。
2.2 类的基类拥有默认构造函数
和上面类似的道理,被合成的构造函数会调用基类的构造函数。调用的顺序根据声明的顺序而定。
如果我们写了派生类的构造,但是没有显示的书写调用基类的构造函数,和上面一样,编译器会扩张现有的每一个构造函数,用以调用所有必要的构造函数
2.3 带有virtual function的类
这里需要的主要作用有两个:
-
一个虚函数表会被编译器产生出来,其中存放类中所有虚函数的地址。
-
在每一个类对象中,一个额外的指针变量会被编译出来,内涵虚函数表的地址。
class A
{
public:
virtual void filp() = 0;
};
2.4 class派生自一个继承串链,其中有一个或更多的虚继承
虚继承的实现在不同的编译器之间有极大的差异。然而,每一种实现方法的共同点在于必须使虚继承类在其每一个派生类中的位置能够在执行期准备妥当。例如:
class X { public: int i;} ;
class A: public virtual X {public: int j;};
class B: public virtual X {public: int d;};
class C: public A, public B {public: int k;};
//无法确定pa->X::i的位置,因为pa的真正类型可以改变
void foo ( const A* pa ) { pa->i = 1024; }
main()
{
foo( new A);
foo( new C) ;
......
}
这里编译器生成的构造函数会记录虚基类在对象中的偏移,以确定虚基类中的数据。
2.5 总结
被合成出来的构造函数只能满足编译器的需要,它并没有初始化成员变量。如果要为成员变量赋值,需要在自定义的构造函数中指定。
3、什么时候不适合默认构造?
- 通过上面的介绍我们知道,默认构造不会初始化成员变量,所以需要初始化成员变量的类需要自定义构造函数。
- 编译器不能合成默认的构造函数时。比如类中包含其他类类型的成员且这个成员没有默认构造函数,基类没有默认构造函数等等
4、 构造函数初始值列表
它负责为新创建的对象和一个或几个数据成员赋初值。不同成员的初始化通过逗号分隔开来,成员变量的初始化顺序和声明顺序相同,而和列表中的顺序无关。例如:
Sales_data(int ia, const string& name) : ivaule(ia),strName(name){};
当某个成员数据被忽略时,与合成默认构造函数相同的方式隐式初始化。
如果编译器支持类内初始化的话,最好使用类内初始化,这样就不用每个构造函数都写一遍了。
为了能够顺利编译,必须使用列表初始化的场景有:
- 类中包含有常量和引用型这种必须显示初始化的成员变量
- 调用一个基类的含参构造函数
- 调用一个成员变量的含参构造函数
5、初始化列表的深度剖析
既然函数体内可以为成员变量进行赋值,为什么要使用初始化列表呢?答案是初始化列表效率高。下面让我们看例子:
class Word
{
int iNum;
string strName;
public:
Word()
{
strName = "";
iNum = 0;
}
};
在这里,Word的构造函数会产生一个临时的string对象,然后将它初始化,之后一个赋值运算符给strName赋值,随后摧毁临时的对象。扩张之后的伪码:
Word::Word()
{
//调用string的构造函数进行初始化
strName.string::string();
//产生临时对象
string temp = string(“”);
//给strName进行赋值,这里调用拷贝构造(后面会讲)
strName.string::operator=(temp);
//摧毁临时对象
temp.string::~string();
iNum = 0;
}
而如果使用初始化列表的方式,则不会进行临时对象的创建,如下:
Word() :strName("")
{
iNum = 0;
}
//C++ 伪码
Word::Word()
{
strName.string::string(""); //直接使用空字符串进行初始化
iNum = 0;
}
在template code(模板编码)中,不使用初始化列表有时会引发错误:
template< class type>
foo<type>::foo(type t)
{
_t = t; //视类型而定,有时会引发大错误
}
所以建议:所有的成员变量初始化操作最好在初始化列表中完成。
需要提醒的是:
- 初始化列表的代码在编译器扩展之后,会在构造函数体内部的代码之前。
- 成员变量的初始化顺序由声明顺序决定。编译器会对初始化列表进行处理并重新排序,以反应声明顺序。
今天就整理到这里,后续整理构造函数使用的一些细节,之后就是其它编译器合成的默认函数。
感谢大家,我是假装很努力的YoungYangD(小羊)。
参考资料:
《c++ primer (第五版)》
《深度探索C++对象模型》