构造函数初始值列表
习惯上变量定义时立即初始化,而不是先定义再赋值。就对象的数据成员而言,初始化和赋值也有类似的区别。
如果没有在构造函数的初始值列表中显示初始化成员,则该成员将在构造函数体之前进行默认初始化。
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) { }
在很多类中,初始化和赋值的区别事关底层效率问题,前者直接初始化数据成员,后者则先初始化再赋值。
成员初始化的顺序
构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的顺序。
- 成员的初始化顺序与它们在类定义中出现的顺序一致。
- 构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。
- 一般来说,初始化的顺序没有什么特别要求,不过如果一个成员是用另一个成员初始化,那么这两个成员的初始化顺序就很关键。
class X
{
int i;
int j;
public:
X(int val) :j(val), i(j){}
};
因为,i
先初始化,所以相当于是用一个未定义的 j
初始化 i
。
一些编译器当构造函数的初始值列表中数据成员顺序与这些成员声明的顺序不一致时会生成一条警告信息。
- 最好令构造函数初始值的顺序与成员声明的顺序保持一致。
- 如果可能的话,尽量避免使用某些成员初始化其它成员。
默认实参和构造函数
如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。
class Sales_data{
Sales_data (std::string s = ""):bookNo(s){ }
};
当没有给定实参或者给定了一个 string
实参时,两个版本的类创建了相同的对象,因为不提供实参也能调用上述构造函数,所以该构造函数实际就为类提供了默认构造函数。
委托构造函数
C++ 11 新标准扩展了构造函数初始值的功能,使得可以定义所谓的委托构造函数。
一个委托构造函数使用它所属类的其他构造函数执行自己的初始化过程,或者说它把自己的一些(全部)职责委托给了其他的构造函数。
委托构造函数也有一个成员初始值的列表和一个函数体。在委托构造函数内,成员初始值列表只有唯一的一个入口,就是类名本身。和其他成员初始值一样,类名后面紧跟圆括号括起来的参数列表,参数列表必须与类中另外一个构造函数匹配。
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); }
private:
std::string bookNo;
unsigned units_sold;
double revenue;
};
在这个 Sales_data
类中,除了一个构造函数外其他都委托了它们的工作。
第一个构造函数接收三个实参,使用这些实参初始化数据成员,然后结束工作。
第二个定义默认构造函数令其使用三参数的构造函数完成初始化过程。
第三个接收一个 string
的构造函数同样委托给了三参数版本。
第四个接收 istream &
的构造函数也是委托构造函数,它委托给了默认构造函数,默认构造函数接着委托给三参数的构造函数。当接受委托的构造函数执行完后,接着执行 istream &
构造函数体的内容,它的构造函数体调用 read
函数读取给定的 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(){}
NoDefault b_member; //错误,b_member没有初始值
};
使用默认构造函数
Sales_data obj(); //错误声明了一个函数而不是对象,这个函数的返回值是Sales_data类型的对象
Sales_data obj; //正确,使用默认构造函数构造obj对象
隐式的类类型转换
能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则。
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){ }
Sales_data(const std::string &s):bookNo(s){ }
Sales_data(std::istream&);
private:
std::string bookNo;
unsigned units_sold;
double revenue;
};
string null_book = "9-999-999-9";
//构造一个临时的Sales_data对象,该对象的units_sold和revenue是0,bookNo是null_book
item.combine(null_book);
上面的例子使用 一个 string
实参调用了 Sales_data
的 combine
成员,该调用是合法的。编译器用给定的string自动创建一个 Sales_data 对象,新生成的对象传递给 combine
。又因为combine
函数的参数是一个常量引用,所以可以给该参数传递一个临时量。
只允许一步类类型转换
编译器只会自动地执行一步类型转换。
item.combine("9-999-999-9");
上面的表达式是错误的,因为需要执行两步转换:
- 把
"9-999-999-9"
转换成string
。 - 把这个临时的
string
转换成Sales_data
。
如果想完成上述的调用,可以显示地把字符串转换成 string
或者 Sales_data
对象。
item.combine(string("9-999-999-9"));
item.combie(Sales_data("9-999-999-9"));
类类型转换不是总有效
item.combine(cin);
这段代码隐式地将 cin
转换成 Sales_data
,这个转换执行接受一个 istream
的 Sales_data
构造函数,该构造函数通过读取标准输入创建了一个临时的 Sales_data
对象,随后将得到的对象传递给 combine
。
Sales_data
对象是一个临时量,一旦 combine
完成就不能再访问它了。
抑制构造函数定义的隐式类型转换
将构造函数声明为 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&);
private:
std::string bookNo;
unsigned units_sold;
double revenue;
};
此时,之前的两种隐式转换方法都无法执行:
item.combine(null_book);
item.combine(cin);
- 关键字
explicit
只对一个实参的构造函数有效,需要多个实参的构造函数不能用于隐式转换,所以无须将这些构造函数指定为explicit
的。 - 只能在类内声明构造函数时使用
explicit
关键字,类外部定义时不应重复。 - 当使用 explicit 关键字声明构造函数时,它将只能以直接初始化的形式使用。
Sales_data item1(null_book); //正确,直接初始化
Sales_data item2 = null_book; //错误,不能将explicit构造函数用于拷贝形式的初始化过程
为转换显示地使用构造函数
尽管编译器不会将 explicit
的构造函数用于隐式转换过程,但是可以显示的强制进行转换:
item.combine(Sales_data(null_book));
item.combine(static_cast<Sales_data>(cin));
第一个调用中,直接使用 Sales_data
的构造函数,该调用通过接受 string
的构造函数创建一个临时的 Sales_data
对象。
第二个调用中,使用 static_cast
执行了显示的而非隐式的转换,其中,static_cast
使用 istream
构造函数创建了一个临时的 Sales_data
对象。
标准库中含有显示构造函数的类
- 接受一个单参数的
const char*
的string
构造函数,不是explicit
的。 - 接受一个容量参数的
vector
构造函数是explicit
的。
聚合类
聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式,当一个类满足如下条件时,它就是聚合的:
- 所有成员都是
public
的。 - 没有定义任何构造函数。
- 没有类内初始值。
- 没有基类,也没有
virtual
函数。
例如,下面的类就是一个聚合类:
struct Data {
int ival;
string s;
};
初始化聚合类的数据成员:
Data val1 = {0,"Anna"};
- 初始值的顺序必须与声明的顺序一致。
- 如果初始值列表中的元素个数少于类的成员数量,则靠后的成员被值初始化。
- 初始值列表的元素个数绝对不能超过类的成员数量。
显式地初始化类的对象成员的明显缺点:
- 要求类的所有成员都是
public
的。 - 将正确初始化每个对象的每个成员的重任交给类的用户(而非类的作者)。这样的初始化方式容易出错。
- 添加或删除一个成员之后,所有的初始化语句都需要更新。
字面值常量类
除了算术类型、引用和指针外,某些类也是字面值类型。和其他类不同,字面值类型的类可能含有 constexpr
函数成员。这样的成员必须符合 constexpr
函数所有的要求,它们是隐式 const
的。
数据成员都是字面值类型的聚合类是字面值常量类,如果一个类不是聚合类,但它符合下述要求,则它也是字面值常量类:
- 数据成员都是字面值类型。
- 类必须至少含有一个
constexpr
构造函数。 - 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的
constexpr
构造函数。 - 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
constexpr 构造函数
尽管构造函数不能是 const
的,但是字面值常量类的构造函数可以使 constexpr
函数。事实上,一个字面值常量类必须至少提供一个constexpr
构造函数。
constexpr
构造函数可以声明成 =default
的形式或者是删除函数的形式。否则,constexpr
构造函数就必须既符合构造函数的要求(意味着不能包含返回语句),又符合 constexpr
函数的要求(意味着它能拥有唯一可执行语句就是返回语句)。综合以上两点, constexpr
构造函数体一般是空的。通过一个前置关键字 constexpr
就可以声明一个 constexpr
构造函数。
class Debug {
public:
constexpr Debug(bool b = true):hw(b),io(b),other(b){ }
constexpr Debug(bool h,bool i,bool o):hw(h),io(i), 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) { other = b; }
private:
bool hw;
bool io;
bool other;
};
constexpr
构造函数必须初始化所有的数据成员,初始值或者使用constexpr
构造函数或者是一条常量表达式。