7.5 构造函数再探
7.5.1 构造函数的初始值列表
如果没有在构造函数的初始值列表中显式地初始化成员,则该成员在构造函数体之前执行默认初始化:
// Sale_data 构造函数的一种写法,虽然合法但是比较草率:没有使用构造函数初始值
Sales_data::Sales_data(const string &s, unsigned cnt, double price)
{
bookNo = s;
units_sold = cnt;
revenue = cnt * price;
}
上例的版本是对数据成员执行赋值操作。
构造函数的初始值有时必不可少
以下情况,数据成员初始值必不可少:
- 成员是
const
或者是引用 - 成员属于某种类类型且该类没有定义默认构造函数
class ConstRef {
public:
ConstRef(int ii);
private:
int i;
const int ci;
int &ri;
};
// 错误:ci 和 ri 必须被初始化
ConstRef::ConstRef(int ii)
{ // 赋值
i = ii; // 正确
ci = ii; // 错误:不能给 const 赋值
ri = i; // 错误:ri 没被初始化
}
// 正确:显式地初始化引用和 const 成员
ConstRef::ConstRef(int ii) : i(ii), ci(ii), ri(i) { }
随着构造函数体一开始执行,初始化就完成了。
初始化 const 或者引用类型的数据成员的唯一机会就是通过构造函数初始值。
成员初始化的顺序
构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序。
成员的初始化顺序与他们在类定义中的出现顺序一致:第一个成员先被初始化,然后第二个,以此类推。
构造函数初始值列表中初始值的前后位置关系不会影响实际的初始值顺序。
一般来说,初始化的顺序没什么特别要求。不过如果一个成员是用另一个成员来初始化的,那么这两个成员的初始化顺序就很关键
class X {
int i;
int j;
public:
X(int val) : // 未定义的:因为 i 在 j 之前被初始化
j(val), i(j) { }
};
最好令构造函数初始值的顺序与成员声明的顺序保持一致。尽量避免使用某些成员初始化其他成员。
默认实参和构造函数
重写一个使用默认实参的 Sales_data 的构造函数
class Sales_data {
public:
// 定义默认构造函数,令其与只接受一个 string 实参的构造函数功能相同
Sales_data(std::string s = "") :
bookNo(s) { }
// 其他构造函数与之前一致
Sales_data(std::string s, unsigned cnt, double rev) :
bookNo(s), units_sold(cnt), revenue(rev * cnt) { }
Sales_data(std::isteam &is) {
read(is, *this);
}
// 其他与之前的版本一致
};
如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。
7.5.2 委托构造函数
**委托构造函数(delegating constructor)**使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或者全部)职责委托给了其他构造函数.
一个委托构造函数也有一个成员初始值的列表和一个函数体。在委托构造函数内,成员初始值列表只有一个唯一的入口,就是类名本身。类名后面紧跟圆括号括起来的参数列表,参数列表必须与类中另外一个委托构造函数匹配。
使用委托构造函数重写Sales_data
类,重写后形式如下:
class Sales_data{
public:
// 非委托构造函数使用对应的实参初始化成员
Sales_data(std::string s, unsigned cnt, double price) :
bookNo(s), units_sold(cnt), revenue(cnt*price) { }
// 其余构造函数全部委托给另一个构造函数
Sales_data() :
Sales_data("", 0, 0) { }
Sales_data(std::string s) :
Sales_data(s, 0, 0) { }
Sales_data(std::istream &is) :
Sales_data() { read(is, *this) }
// 其余部分相同
};
当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。
7.5.3 默认构造函数的作用
默认初始化在以下情况下发生:
- 当我们在块作用域内不使用任何初始值定义一个非静态变量或者数组时
- 当一个类本身含有类类型的成员且使用合成的默认构造函数时
- 当类类型的成员没有在构造函数初始值列表中显式地初始化时
值初始化在以下情况下发生:
- 在数组初始化的过程中如果我们提供的初始值数据少于数组的大小时
- 当我们不使用初始值定义一个局部静态变量时
- 当我们通过书写形如 T() 的表达式显式地请求值初始化时,其中 T 是类型名
类必须包含一个默认构造函数以便在上述情况下使用。
使用默认构造函数
通过如下方式使用默认构造函数
Sales_data obj;
7.5.4 隐式地类类型转换
如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称作转换构造函数(converting constructor)。
能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则。
例如:在 Sales_data 类中,接受 string 的构造函数和接受 istream 的构造函数分别定义了从这两种类型向 Sales_data 隐式转换的规则
string null_book = "9-999-99999-9";
// 构造一个临时的 Sales_data 对象
// 该对象的 units_sold、revenue 等于 0,bookNo 等于 null_book
item.combine(null_book);
只允许一步类类型转换
编译器只会自动地执行一步类型转换。
// 错误:需要用户定义的两种转换:
// 1. 把 "9-999-99999-9" 转换成 string
// 2. 再把这个临时的 string 转换成成 Sales_data
item.combine("9-999-99999-9");
// 正确的做法是显式地转换
// 正确:显式地转换成 string,隐式地转换成 Sales_data
item.combine(string("9-999-99999-9"));
// 正确:隐式地转换成 string,显式地转换成 Sales_data
item.combine(Sales_data("9-999-99999-9"));
类类型转换不是总有效
// 使用 istream 构造函数创建一个函数传递给 combine
item.combine(cin);
上例中,我们构建了一个对象,先将它的值加到 item 中,随后将其丢弃。
抑制构造函数定义的隐式转换
通过将构造函数声明为explicit
阻止构造函数的隐式转换
class Sales_data {
public:
Sales_data() = default;
Sales_data(const std::string &s, unsigned n, double p) :
bookNo(s), units_sold(n), revenue(p * n) { }
explicit Sales_data(const std::string &s) :
bookNo(s) { }
explicit Sales_data(std::istream&);
// 其他成员与之前一致
};
此时,没有任何构造函数能用于隐式地创建 Sales_data 对象
关键字explicit
只对一个实参的构造函数有效。
需要多个实参的构造函数不能用于执行隐式转换。
只能在类内声明构造函数时使用explicit
关键字,在类外部定义时不应重复。
explicit 构造函数只能用于直接初始化
Sales_data item1(null_book); // 正确:直接初始化
Sales_data item2 = null_book; // 错误:不能将 explicit 构造函数用于拷贝形式的初始化过程
当我们用explicit
关键字声明构造函数时,它将只能以直接初始化的形式使用。而且,编译器将不会在自动转换过程中使用该构造函数。
为转换显式地使用构造函数
可以使用explicit
的构造函数显式地强制进行转换
// 正确:实参是一个显式构造的 Sales_data 对象
item.combine(Sales_data(null_book));
// 正确:static_cast 可以使用 explicit 的构造函数
item.combine(static_cast<Sales_data>(cin));
标准库中含有显式构造函数的类
- 接受一个单参数的 const char* 的 string 构造函数不是 explicit 的
- 接受一个容量参数的 vector 构造函数是 explicit 的
7.5.5 聚合类
聚合类(aggregate class)使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。
聚合类的条件如下:
- 所有成员都是 public 的
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类,也没有 virtual 函数
例如,下面的类是一个聚合类
struct Data {
int iVal;
string s;
};
可以提供一个花括号括起来的成员初始值列表,用于初始化聚合类的数据成员
// val1.ival = 0; val1.s = string("Anna");
Data val1 = { 0, "Anna"};
初始值的顺序必须与声明的顺序一致。
7.5.6 字面值常量类
某些类也是字面值类型,和其他类不同,字面值类型的类可能含有 constexpr 函数成员。
数据成员都是字面值类型的聚合类是字面值常量类。
如果一个类不是聚合类,但符合下述要求,也是一个字面值常量类。
- 数据成员必须都是字面值类型
- 类必须至少含有一个 constexpr 构造函数
- 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的 constexpr 构造函数
- 类必须使用析构函数的默认定义,该成员负责销毁类的对象
constexpr 构造函数
构造函数不能是 const 的,但字面值常量类的构造函数可以是 constexpr 的。一个字面值常量类必须至少提供一个 constexpr 构造函数。
constexpr 构造函数可以声明成=default
的形式。
一般而言,constexpr 构造函数体应该是空的。通过前置关键字constexpr
就可以声明一个 constexpr 构造函数了
class Debug {
public:
constexpr Debug(bool b = true) :
hw(b), io(b), other(b) { }
constexpr Debuf(bool h, bool i, bool o) :
hw(h), io(b), other(o) { }
constexpr bool any() { return hw || io || other; }
void set_io(bool b) { io = b; }
void set_hw(bool b) { hw = b; }
void set_other(bool b) { hw = b; }
private:
bool hw; // 硬件错误,而非 IO 错误
bool io; // IO 错误
bool other; // 其他错误
};
constexpr 构造函数必须初始化所有数据成员,初始值或者使用 constexpr 构造函数,或者是一条常量表达式。
constexpr 够着函数用于生成 constexpr 对象以及 constexpr 函数的参数或返回类型
constexpr Debug io_sub(false, true, false); // 调试 IO
if(io_sub.any()) // 等价于 if(true)
ceer << "print appropriate error messages" << endl;
constexpr Debug prod(false); // 无调试
if(prod.any()) // 等价于 if(false)
ceer << "print an error message" << endl;