前提
有 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 &is) {
read(is, *this);
}
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::ostream& print(std::ostream&, const Sales_data &);
istream &read(istream &is,Sales_data &item) {
double price = 0; // price 表示某书单价
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;
}
隐式的类类型转换
想必都知道C++语言在内置类型之间定义了几种转换规则。同样的,类也可以定义隐式转换机制。如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称作转换构造函数。只有将会介绍如何定义一种类类型转换为另一种类类型的转换规则。
Note:能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则
所以,在 Sales_data 类中便有从 string 和 istream 向 Sales_data 隐式转换的规则。即,在需要使用 Sales_data 的地方,我们可以使用 string 或者 istream 作为替代:
string null_book = "9-999-99999-9";
// 构造一个临时的 Sales_data 对象
// 该对象的 units_sold 和 revenue 为 0,bookNo 等于 null_book
item.combine(null_book);
代码中的调用是合法的,编译器用给定的 string 自动创建了一个 Sales_data 对象。新生村的这个临时 Sales_data 对象被传递给 combine。因为 combine 函数的参数是一个常量引用,所以可以给该参数传递一个临时量。
只允许一步类类型转换
编译器只会自动地执行一步类型转换。例如,下面的代码隐式地使用了两种类型转换,所以是错误的:
// 错误:需要用户定义的两种转换:
// (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"));
item.combine(Sales_data("9-999-99999-9"));
类类型转换不是总有效
是否需要从 string 到 Sales_data 的转换依赖于我们对用户使用该转换的看法。在此例中,这种转换可能是对的。null_book 中的 string 可能表示了一个不存在的 ISBN 编号。
另一个是从 istream 到 Sales_data 的转换:
// 使用 istream 构造函数创建一个函数传递给 combine
item.combine(cin);
这段代码隐式地把 cin 转换成 Sales_data。这个转换执行了接受一个 istream 的 Sales_data 构造函数。该构造函数通过读取标准输入创建了一个临时的 Sales_data 对象,随后将得到的对象传递给 combine。
Sales_data 对象是个临时量,一旦 combine 完成我们就不能再访问它。实际上,我们创建了一个对象,先将它的值加到 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&);
// 其他成员和之前一致
};
所以,之前的两种用法都无法通过编译:
item.combine(null_book); // 错误:string 构造函数时 explicit
item.combine(cin); // 错误:istream 构造函数时 explicit
关键词 explicit 只对一个实参的构造函数有效。需要多个实参的构造函数不能用于执行隐式转换,所以无须将这些构造函数指定为 explicit 。只能在类内声明构造函数时使用 explicit 关键词,在类外部定义时不应重复。
// 错误:explicit 关键字只允许出现在类内构造函数声明处
explicit Sales_data::Sales_data(istream& is) {
read(is,*this);
}
explicit 构造函数只能用于直接初始化
发生隐式转换的一种情况是当我们执行拷贝形式的初始化时(使用 =)。此时,只能使用直接初始化而不能使用 explicit 的构造函数:
Sales_data item1(null_book); // 正确,直接初始化
Sales_data item2 = null_book; // 错误:不能将 explicit 构造函数用于拷贝形式的初始化过程
Note:当用 explicit 关键字声明构造函数时,它将只能以直接初始化的形式使用。而且,编译器将不会在自动转换过程中使用该构造函数
为转换显示地使用构造函数
尽管编译器不会将 explicit 的构造函数用于隐式转换过程,但是我们可以使用这样的构造函数显式地强制进行转换:
item.combine(Sales_data(null_book)); // 正确:实参是一个显式构造的 Sales_data 对象
// 正确:static_cast 可以使用 explicit 的构造函数
item.combine(static_cast<Sales_data>(cin));
很明显,第一个调用是直接使用的想应的构造函数,而第二个调用是显示强制转换,而非隐式转换,所以依旧可以。
标准库中含有显示构造函数的类
我们用过的一些标准库中的类含有单参数的构造函数:
- 接受一个单参数的 const char * 的 string 构造函数,不是 explicit
- 接受一个容量参数的 vector 构造函数是 explicit 的
聚合类
聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。
一个聚合类,满足如下条件:
- 所有成员都是 public
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类,也没有 virtual 函数。(关于这些将会在之后介绍)
可以用一个花括号括起来的成员初始值列表初始化聚合类的数据成员。顺序与 构造函数初始值列表的初始化顺序相同(即按照声明顺序)。
当初始化列表的元素个数少于类成员数量时,靠后的成员被值初始化。初始值列表的元素个数不能超过类的成员数量。
显示地初始化类的对象成员存在三个明显的缺点:
- 要求类的所有成员都是 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 h) { hw = h; }
void set_other(bool o) { other = o; }
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())
cerr << "print appropriate error messages" << endl;
constexpr Debug prod(false); // 无调试
if(prod.any())
cerr << "print an error message" << endl;