5.4 类的构造函数
简介
类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数被称为构造函数。构造函数的主要任务是初始化对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
构造函数具有如下特点:
Tips:当我们创建类的一个
const
对象时,直到构造函数完成初始化过程时对象才能真正取得其“常量”属性,因此构造函数在const
对象的构造过程中可以向其写值。
- 构造函数名字和类名相同
- 构造函数没有返回类型
- 类可以包含多个构造函数,不同的构造函数之间必须在参数数量或者参数类型上有所区别
- 构造函数不能被声明为
const
的
默认构造函数
Tips:无须任何实参的构造函数被称为默认构造函数,编译器创建的构造函数又被称为合成的默认构造函数。
当我们没有为类的对象提供初始值时,类会通过默认构造函数来控制其默认初始化过程。
1. 合成的默认构造函数
编译器合成的默认构造函数会按照如下规则初始化类的数据成员:
- 如果存在类内初始值,则用它来初始化成员
- 否则默认初始化该成员
2. 不能依赖合成的默认构造函数的类
编译器自动合成的默认构造函数只适用于非常简单的类,对于一个普通的类而言必须定义它自己的默认构造函数,主要原因有三点:
- 编译器只有在类不包含任何构造函数的情况下才会替我们合成一个默认构造函数,一旦我们定义了其他的构造函数,那么除非我们再定义一个默认构造函数,否则类将没有默认构造函数
- 对于某些类而言,合成的默认构造函数可能执行错误的操作:如果定义在块中的内置类型或复合类型的对象被默认初始化,那么它们的值是未定义的
- 某些情况下编译器不能为某些类合成默认构造函数:类中包含一个没有默认构造函数的类类型成员
=default
C++11新标准中,如果我们需要合成的默认构造函数,那么可以在参数列表后面写上=default
来要求编译器生成默认构造函数。
构造函数初始值列表
1. 构造函数初始值列表初始化数据成员
冒号和花括号之间的代码被称为构造函数初始值列表,它负责为新创建的对象的一个或几个数据成员赋初始值。
Tips:如果没有在构造函数的初始值列表中显式地初始化成员,则成员将在构造函数体之前执行默认初始化。
struct Foo {
std::string str;
double price;
};
Foo(const std::string &s) : str(s) { }
Foo(const std::string &s, double d) : str(s), price(d) { }
2. 构造函数的初始值有时必不可少
Tips:如果成员是
const
、引用或者是属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初始值。
在大多数时候我们可以忽略类数据成员初始化(构造函数初始值列表)和赋值(构造函数体内赋值)之间的差异。如果成员是const
或者是引用的话,必须将其初始化。类似的,当成员属于某种类类型且该类没有定义默认构造函数时,也必须将这个成员初始化。
class ConstRef {
public:
constRef(int ii);
private:
int i;
const int ci;
int &ri;
};
随着构造函数体一开始执行,初始化就完成了。我们初始化const或者引用类型的数据成员的唯一机会就是通过构造函数初始值,因此该构造函数的正确形式应该是:
ConstRef::ConstRef(int ii) : i(ii), ci(ii), ri(i) { }
3. 成员初始化的顺序
Tips:最好令构造函数初始值的顺序与成员声明的顺序保持一致,而且如果可能的话,尽量避免使用某些成员初始化其他成员。最好是使用构造函数的参数作为成员的初始值,而尽量避免使用同一个对象的其他成员,这样的好处是可以不必考虑成员的初始化顺序。
成员的初始化顺序与它们在类定义中出现顺序一致:第一个成员先被初始化,然后第二个,以此类推。构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。
默认实参和构造函数
Tips:如果一个构造函数为所有参数都提供了默认实参,那么它实际上也定义了默认的构造函数。
class Foo {
public:
// 定义默认构造函数: 与只接受一个string实参的构造函数功能相同
Foo(std::string s = "") : str(s) { }
private:
std::string str;
};
委托构造函数
C++11新标准扩展了构造函数初始值的功能,使得我们可以定义所谓的委托构造函数,一个委托构造函数使用它所属类的其它构造函数执行它自己的初始化过程,或者它把它自己的一些(或者全部)职责委托给了其他构造函数。
class Foo {
public:
// 非委托构造函数
Foo(std::string s, double d, int i) : var_str(s), var_d(d), var_i(i) { }
// 其余构造函数委托给另一个构造函数
Foo() : Foo("", 3.14, 5) { }
Foo(std::string s) : Foo(s, 0, 0) { }
private:
std::string var_str;
int var_i;
double var_d;
}
转换构造函数:隐式的类类型转换
1. 简介
Tips:能通过一个实参调用的构造函数定义了从构造函数的参数类型向类类型隐式转换的规则。
C++在不同的内置类型之间定义了几种自动转换规则,同样地我们也可以为类定义隐式转换规则。如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,我们也将这种构造函数称为转换构造函数。
举个简单的例子:如果一个类包含接受一个string
类型的构造函数,那么它也就定义了从string
类型到该类隐式转换的规则。
class Foo {
public:
// 定义从std::string到Foo类型的隐式转换规则
Foo(std::string s) : str(s) { }
private:
std::string str;
}
2. 只允许一步类类型转换
编译器只会自动地执行一步类型转换,例如下面代码隐式地使用了两种转换规则,因此它是错误的:
// 错误: 需要用户定义的两种转换:
// 1) 把"tomocat"转换成string
// 2) 再把这个临时的string转换成Foo类型
Foo = "tomocat";
3. 抑制构造函数定义的隐式转换
我们可以将构造函数声明为explicit
来阻止构造函数定义的隐式类类型转换,需要注意如下几点:
- 关键字
explicit
只对一个实参的构造函数有效:需要多个实参的构造函数不能用于执行隐式转换,因此无需将这些构造函数指定为explicit
- 只能在类内声明构造函数时使用
explicit
关键字,在类外部定义时不应该重复 explicit
声明的构造函数只能以直接初始化的形式使用
class Foo {
public:
// 阻止隐式类类型转换
explicit Foo(std::string s) : str(s) { }
private:
std::string str;
}
std::string str = "tomocat";
// 错误: 不支持string到Foo类型的隐式初始化
Foo = str;
// 正确: 显式初始化
Foo(str);
static_cast<Foo>(str);