前提: 仍有 Sales_data 类的代码:
struct Sales_data {
std::string isbn() const { return bookNo; } // 返回 isbn 编号
Sales_data& combine (const Sales_data&); // 模拟 += 运算
double avg_price() const;
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&);
std::istream &read(std::istream&, Sales_data&);
定义类相关的非成员函数
从上代码可知,add, print, read 不属于类本身,从概念上讲 属于类接口的组成部分。
同样,通常把非成员函数的声明和定义分开。函数从概念是属于类,虽然不定义在类型中,也同样与类声明在同一个头文件中
一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类放在同一个头文件中
定义 read 和 print 函数
简单说,read 和 print 函数作用就是类似于重载 >>, << 运算符
代码其实和直接输入也是非常相似:
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;
}
ostream &print(ostream &os,const Sales_data &item) {
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os;
}
// 使用
int main() {
Sales_data x;
read(cin,x);
print(cout,x);
return 0;
}
关于 read 函数和 print 函数有两点重要的。
定义 add 函数
看形参列表 以及 返回类型,可知调用为: c = add(a,b)。其中 a、b、c 都是 Sales_data 类型
Sales_data add(Sales_data &lhs,Sales_data &rhs) {
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
构造函数
每个类都分别定义了它的对象被初始化的方式,类通过一个或者几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
构造函数的名字和类名相同。但是构造函数没有返回类型;除此以外和普通函数类似,构造函数有一个(可能为空)参数列表和一个函数体(可能为空)。类也可以包含多个构造函数,和重载函数差不多,必须在参数数量或者参数类型上有所区别。
构造函数不能声明成 const,当创建一个 const 对象时,会直到构造函数完成初始化过程,对象真正的取得"常量"属性。因此,构造函数在 const 对象的构造过程中可以向其写值。
合成的默认构造函数
类通过一个特殊的构造函数来控制默认初始化过程,称为默认构造函数。默认构造函数无须任何实参。
默认构造函数的其中一个特殊性是,如果类没有显示的定义构造函数,编译器会隐式的定义一个默认构造函数,称为合成的默认构造函数。
合成的默认构造函数会按照以下规则初始化类的数据成员:
- 如果存在类内的初始值,就用它初始化相应的成员
- 否则,默认初始化该成员
注意:当类没有声明任何构造函数时,编译器才会自动生成默认构造函数
某些类不能依赖于合成的默认构造函数
合成的默认构造函数只适合简单的类。一般来说,对于一个普通的类,都要定义自己的默认构造函数。
原因有三:
- 当类没有声明任何构造函数时,编译器才会自动生成默认构造函数。除非自己再定义一个默认构造函数,否则只要我们定义了一个构造函数,该类就不会有默认构造函数。
- 合成的默认构造函数可能会执行错误的操作。如果定义在块中的内置类型或复合类型(数组和指针等)执行默认初始化,则它们的值是未定义的。即用户创建类的对象时有可能得到未定义的值。(只有类中的内置类型或复合类型都有初始值时,才适合用合成的默认构造函数)
- 类中类的时候。如果类中的类没有默认构造函数,编译器就无法初始化该成员。所以必须自定义默认构造函数。当然还有其他的情况也会导致这个问题。
定义 Sales_data 的构造函数
先给出添加构造函数后的代码,后面会一一解释。
struct Sales_data {
// 新增的构造函数
Sales_data() = default;
Sales_data(const std::string &s): bookNo(s) { }
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p * n){ }
Sales_data(std::istream &);
// 之前已有的
std::string isbn() const { return bookNo; } // 返回 isbn 编号
Sales_data& combine (const Sales_data&); // 模拟 += 运算
double avg_price() const;
std::string bookNo;
unsigned units_sold = 0; // 表示某书的销量
double revenue = 0.0; // 表示某书的总收入
};
= default 的含义
Sales_data() = default;
可以明确一点:该构造函数不接受任何实参,所以是一个默认构造函数。定义这个函数的目的仅仅是因为需要其他形式的构造函数,也需要默认的构造函数。我们希望这个函数的作用完全等同于之前使用的 合成默认构造函数(!在这里有效的原因是 类的内置类型都提供了初始值,如果编译器不支持类内初始值,那么默认构造函数就应该使用构造函数初始值列表来初始化类的每个成员)。
C++11中,可以在参数列表后面写上 = default 来要求编译器生成构造函数。= default 可以声明出现在类的内部,也可以定义出现在类的外部。如果 = default 在类的内部,则默认构造函数是内联的;否则该成员默认情况下不是内联的。
构造函数初始值列表
Sales_data(const std::string &s): bookNo(s) { }
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p * n){ }
两个定义出现了新的部分,即冒号以及冒号和花括号之间的代码。
花括号定义了空函数体,因为构造函数的目的是为数据成员赋初值,一旦没有其他任务需要执行,函数体为空即可。
新出现部分叫做构造函数初始值列表,为相应的成员赋上括号内的初始值。
当某个数据成员被构造函数忽略时,它将以与合成认构造函数相同的方式隐式初始化。
建议:构造函数不应该轻易覆盖掉类内初始值,除非新赋的值和原值不同。
在类的外部定义构造函数
与其他的几个不同,以 istream 为参数的构造函数需要一些实际的操作。
Sales_data::Sales_data(std::istream &is) {
read(is, *this);
}
这个函数没有返回类型,函数名和类名相同。显然是一个构造函数。尽管函数初始值列表是空的,但是执行了构造函数体,所以对象成员仍能被初始化。