前提
有 Sales_data 类:
class Sales_data {
// 做友元声明
friend Sales_data add(const Sales_data &,const Sales_data &);
friend std::istream& read(std::istream&, Sales_data &);
friend std::ostream& print(std::ostream&, const Sales_data &);
// 剩下的与之前一致
public:
Sales_data() = default;
Sales_data(const std::string &s,unsigned n,double p):
bookNo(s), units_sold(n), revenue(p * n) { }
Sales_data(const std::string &s): bookNo(s) { }
Sales_data(std::istream &);
std::string isbn() const { return bookNo; }
Sales_data &combine(const Sales_data &);
private:
double avg_price() const {
return units_sold ? revenue / units_sold : 0;
}
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
// Sales_data 接口的非成员组成部分的声明
Sales_data add(const Sales_data &,const Sales_data &);
std::istream& read(std::istream&, Sales_data &);
std::ostream& print(std::ostream&, const Sales_data &);
构造函数初始值列表
当我们定义变量时,习惯于对其进行初始化,而非先定义再赋值
string foo = "hello world";
int x = 0;
就对象的数据成员而言,初始化和赋值也有类似的区别。如果没有在构造函数的初始值列表中显式地初始化成员,则该成员将在构造函数体之前执行默认初始化。
例如:
// Sales_data 函数的一种写法,合法但比较草率:没有使用构造函数初始值
Sales_data::Sales_data(const string &s,unsigned cnt,double price) {
bookNo = s;
units_sold = cnt;
revenue = cnt * price;
}
// 原写法:
Sales_data::Sales_data(const std::string &s,unsigned n,double p):
bookNo(s), units_sold(n), revenue(p * n) { }
与原构造函数的效果完全相同。区别是原来的版本初始化了它的数据成员,而新版本是对数据成员进行了赋值操作。这一区别的会有的深层次影响完全依赖于数据成员的类型。
构造函数的初始值有时必不可少
有时候可以忽略数据成员舒适化与赋值之间的差异,但并非总能这样。如果成员是 const 或者引用,就必须将其初始化。同样,当成员是某种类类型且该类没有默认构造函数时,也必须将这个成员初始化。
上述情况都是很简单的。不举例子。
建议:使用构造函数初始值。
在很多类中,初始化和赋值的区别事关底层的效率问题。前者是直接初始化,而后者是先初始化再赋值。
除了效率问题更重要的是,一些数据成员必须被初始化。建议使用构造函数初始值,这样能避免某些意想不到的错误,特别是遇到有的类含有需要构造函数初始值的成员时。
成员初始化的顺序
构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序。
成员的初始化顺序与它们在类定义中的出现顺序一致。构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。
所以,当一个成员是用另一个成员来初始化,那么两个成员的初始化顺序就很关键。
class X {
int i,j;
public:
// 为定义的:i 在 j 之前被初始化
X(int val): j(val),i(j) { }
};
在此例中,从构造函数初始值的形式上仿佛先初始化 j 然后初始化 i,而实际并非如此,上面已经提到过了。所以此例中,试图使用未定义的值 j 初始化 i 。
最好令构造函数初始值的顺序与成员声明顺序一致。而且如果可以,尽量避免用某些成员初始化其他成员。
默认实参和构造函数
Sales_data 默认构造函数的行为与只接受一个 string 实参的构造函数差不多。唯一的区别是接受 string 实参的构造函数使用这个实参初始化 bookNo,而默认构造函数隐式地使用 string 的默认构造函数初始化 bookNo。我们可以把它们重写成一个使用默认实参的构造函数
class Sales_data {
public:
// 定义默认构造函数,令其与只接受一个 string 实参的构造函数功能相同
Sales_data(std::string s = ""): bookNo(s) { }
//其他的与之前一致
};
其实上述构造函数实际为类提供了默认构造函数,因为没有实参时也能够调用此函数。
当一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。
当构造函数含有默认实参时,需要多加注意。因为 当我们定义,并不初始化时,可能会导致编译器不知道调用哪个构造函数。
委托构造函数
C++11 扩展了构造函数初始值功能,使得我们可以定义委托构造函数。一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把自己的一些(或者全部)职责委托给了其他构造函数。
委托构造函数与其他构造函数没有太大的差别。具体见代码,使用委托构造函数重写 Sales_data 类。
class Sales_data {
public:
// 非委托构造函数使用对应的实参初始化成员
Sales_data(std::string s,unsigned cnt,double price):
bookNo(s),units_sold(cnt),revenue(price * cnt) { }
// 其余构造函数全部委托给另一个构造函数
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); }
};
由上代码可见,除了第一个构造函数,其他的都委托了它们的工作。
接受 istream& 的构造函数,它委托给了默认构造函数,默认构造函数又接着委托给三参数构造函数。当这些受委托的构造函数执行完后,接着执行 istream& 构造函数体的内容。
当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。在 Sales_data 类中,受委托的构造函数体恰好是空的。假如函数体包含有代码的话,将先执行这些代码,然后控制权才会交给委托者的函数体。
执行顺序:受委托的构造函数的初始值列表和函数体被依次执行,然后再执行委托者的函数体。当然,存在嵌套的情况。
默认构造函数的作用
当对象被默认初始化或值初始化时自动执行默认构造函数。默认初始化在以下情况下发生:
- 当我们在块作用域内不使用任何初始值定义一个非静态变量或者数组时
- 当一个类本身含有类类型的成员且使用合成的默认构造函数时
- 当类类型的成员没有在构造函数初始值列表中显示地初始化时
值初始化在以下情况下发生:
- 数组初始化的过程中如果我们提供初始值数量少于数组的大小时
- 当我们不使用初始值定义一个局部静态变量时
- 当我们书写形如 T() 的表达式显式地请求值初始化时,其中 T 是类型名 (vector 的一个构造函数只接受一个实参用于说明 vector 大小,它就是使用一个类似这种形式的实参来对它的元素初始化器进行值初始化)
类必须包含一个默认构造函数以便上述情况下使用,其中的大多数情况非常容易判断。
不那么明显的一种情况是类的某些数据成员缺少默认构造函数:
class NoDefault {
public:
NoDefault(const std::string &);
// 还有其他成员,但是没有其他构造函数了
};
struct A {
NoDefault my_mem;
};
A a; // 错误,没有为 A 合成构造函数
struct B {
B() { } // 错误,b_member 没有初始值
NoDefault b_member;
};
在实际中,如果定义了其他构造函数,最好也提供一个默认构造函数
使用默认构造函数
下面的 obj 声明可以正常通过编译:
Sales_data obj(); // 正确,定义了一个函数而非对象
if(obj.isbn() == primer_5th_ed.isbn()) // 错误:obj 是一个函数
如果想定义一个使用默认构造函数进行初始化的对象,正确方法是去掉对象名之后的空括号对:
Sales_data obj; // 正确:obj 是一个默认初始化对象