C++ Primer 5th 随堂练习
【C++ Primer】第三章 字符串、向量和数组 (练习)
第七章 类
练习 7.1 - struct Sales_data - v1
使用 2.6.1 节定义的 Sales_data 类为 1.6 节的交易处理程序编写一个新版本。
解答
源程序:
#include <iostream>
using namespace std;
struct Sales_data
{
string bookNo; // 编号
unsigned units_sold = 0; // 销售量
double revenue = 0.0; // 营收
};
int main()
{
Sales_data total;
if (cin >> total.bookNo >> total.units_sold >> total.revenue){
Sales_data trans;
while (cin >> trans.bookNo >> trans.units_sold >> trans.revenue) {
if (total.bookNo == trans.bookNo) {
total.units_sold += trans.units_sold;
total.revenue += trans.revenue;
}
else {
cout << total.bookNo << " " << total.units_sold << " " << total.revenue << endl;
total = trans;
}
}
cout << total.bookNo << " " << total.units_sold << " " << total.revenue << endl;
}
else {
cerr << "No data?!" << endl;
system("pause");
return -1;
}
system("pause");
return 0;
}
练习 7.2 - class Sales_data
曾在 2.6.2 节的练习中编写了一个 Sales_data 类,请向这个类添加 combine 函数和 isbn 成员。
解答
class 实现:
struct 实现见 练习 7.3。
练习 7.3 - struct Sales_data - v2
修改 7.1.1 节的交易处理程序,令其使用这些成员。
解答
源程序:
#include <iostream>
#include <string>
using namespace std;
// 新增两个成员函数
struct Sales_data
{
// 数据成员
string bookNo; // 编号
unsigned units_sold = 0; // 销售量
double revenue = 0.0; // 营收
// 成员函数
string isbn() const { return bookNo; }; // 结构体内声明、体内定义
Sales_data& combine(const Sales_data&); // 结构体内声明、体外定义
};
Sales_data& Sales_data::combine(const Sales_data& rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
int main()
{
Sales_data total;
if (cin >> total.bookNo >> total.units_sold >> total.revenue){
Sales_data trans;
while (cin >> trans.bookNo >> trans.units_sold >> trans.revenue) {
if (total.isbn() == trans.isbn()) { // changed
total.combine(trans); // changed
}
else {
cout << total.bookNo << " " << total.units_sold << " " << total.revenue << endl;
total = trans;
}
}
cout << total.bookNo << " " << total.units_sold << " " << total.revenue << endl;
}
else {
std::cerr << "No data?!" << std::endl;
system("pause");
return -1;
}
system("pause");
return 0;
}
练习 7.4 - class Person - v1
编写一个名为 Person 的类,使其表示人员的姓名和地址。使用 string 对象存放这些元素,接下来的练习将不断充实这个类的其他特征。
解答
类:
class Person
{
private:
string name; // 姓名
string address; // 地址
};
练习 7.5 - class Person - v2
在你的 Person 类中提供一些操作使其能够返回姓名和地址。 这些函数是否应该是 const 的呢?解释原因。
解答
上述两个成员函数都应被定义成 常量成员函数,因为不论是返回姓名还是返回地址,在函数体内都只是读取数据成员的值,而不会做任何改变。
类:
class Person
{
private:
string name; // 姓名
string address; // 地址
public:
string get_name() const { return name; } // 返回姓名
string get_address() const { return address; } // 返回地址
};
练习 7.6
对于函数 add、read 和 print,定义你自己的版本。
解答
见 练习 7.7。
练习 7.7 - struct Sales_data - v3
使用这些新函数重写 7.1.2 节练习中的程序。
解答
源程序:
#include <iostream>
#include <string>
using namespace std;
// 结构体 /
struct Sales_data
{
// 数据成员
string bookNo; // 编号
unsigned units_sold = 0; // 销售量
double revenue = 0.0; // 营收
// 成员函数
string isbn() const { return bookNo; }; // 结构体内声明、体内定义
Sales_data& combine(const Sales_data&); // 结构体内声明、体外定义
};
// 成员函数定义
Sales_data& Sales_data::combine(const Sales_data& rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
// 非成员函数
istream &read(istream &is, Sales_data &item)
{
double price = 0;
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;
return os;
}
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
// 主函数
int main()
{
Sales_data total;
if (read(cin, total)){ // changed
Sales_data trans;
while (read(cin, trans)) { // changed
if (total.isbn() == trans.isbn()) {
total.combine(trans);
}
else {
print(cout, total); // changed
cout << endl;
total = trans;
}
}
print(cout, total); // changed
cout << endl;
}
else {
cerr << "No data?!" << endl;
system("pause");
return -1;
}
system("pause");
return 0;
}
练习 7.8
为什么 read 函数将其 Sales_data 参数定义成普通的引用,而 print 函数将其参数定义成常量引用?
解答
- 一方面,之所以将 read 函数的 Sales_data 形参定义为 普通引用,是因为我们需要从标准输入中读取数据,并将其写入到给定的 Sales_data 对象中,所以需要有修改对象的权限 (读写权限)。
- 另一方面,之所以将 print 函数的 Sales_data 形参定义为 常量引用,是因为我们只需要输出数据,而无需对其进行更改 (只读权限)。
练习 7.9 - class Person - v3
对于 7.1.2 节练习中代码,添加读取和打印 Person 对象的操作。
解答
类:
class Person
{
private:
public:
/* 数据成员 */ // 暂设为公共成员, 以便于 read 和 print 函数能够访问
string name; // 姓名
string address; // 地址
/* 成员函数 */
string get_name() const { return name; } // 返回姓名
string get_address() const { return address; } // 返回地址
};
/* 非成员函数 */
istream &read(istream &is, Person &person)
{
return (is >> person.name >> person.address);
}
ostream &print(ostream &os, const Person &person)
{
return (os << person.name << " " << person.address);
}
练习 7.10
在下面这条 if 语句中,条件部分的作用是什么?
if (read(read(cin, data1), data2))
解答
上述语句等价于:
read(cin, data1); read(cin, data2);
练习 7.11 - struct Sales_data - v4
在你的 Sales_data 类中添加构造函数, 然后编写一段程序令其用到每个构造函数。
解答
源程序:
#include <iostream>
#include <string>
using namespace std;
// 结构体 /
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(n*p){ }
Sales_data(std::istream &is); // 结构体内声明, 体外定义
/* 数据成员 */
string bookNo; // 编号
unsigned units_sold = 0; // 销售量
double revenue = 0.0; // 营收
/* 成员函数 */
string isbn() const { return bookNo; }; // 返回编号
Sales_data& combine(const Sales_data&); // 结合数据
};
/* 成员函数 */
Sales_data& Sales_data::combine(const Sales_data& rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
// 调用函数
istream &read(istream &is, Sales_data &item)
{
double price = 0;
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;
return os;
}
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
// 构造函数 /
Sales_data::Sales_data(std::istream &is) // 结构体内声明, 体外定义
{
read(is, *this); // 注意本构造要放在 read 函数后才行(避免未定义)
}
// 主函数 - 构造函数测试 /
int main()
{
Sales_data item1;
print(cout, item1) << endl;
Sales_data item2("0-985-12345-X");
print(cout, item2) << endl;
Sales_data item3("1-211-13579-Y", 6, 10.00);
print(cout, item3) << endl;
Sales_data item4(cin);
print(cout, item4) << endl;
system("pause");
return 0;
}
控制台交互:
0 0
0-985-12345-X 0 0
1-211-13579-Y 6 60
2-973-24680-Z 4 15.00
2-973-24680-Z 4 60
练习 7.12 - struct Sales_data - v5
把只接受一个 istream 作为参数的构造函数移到类的内部。
解答
结构体:
#include <iostream>
#include <string>
using namespace std;
// 结构体 /
struct Sales_data
{
/* 构造函数 */
Sales_data() = default;
Sales_data(const string &s):bookNo(s) { }
Sales_data(const string &s, unsigned n, double p):bookNo(s), units_sold(n), revenue(n*p){ }
Sales_data(istream &is) { read(is, *this); } // 结构体内声明和定义
/* 数据成员 */
string bookNo; // 编号
unsigned units_sold = 0; // 销售量
double revenue = 0.0; // 营收
/* 成员函数 */
string isbn() const { return bookNo; }; // 返回编号
Sales_data& combine(const Sales_data&); // 结合数据
};
/* 成员函数 */
Sales_data& Sales_data::combine(const Sales_data& rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
// 调用函数
istream &read(istream &is, Sales_data &item)
{
double price = 0;
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;
return os;
}
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
练习 7.13
使用 istream 构造函数重写第229页的程序。
解答
主函数 (结构体来自练习 7.12):
#include <iostream>
#include <string>
using namespace std;
// 主函数
int main()
{
Sales_data total(cin); // changed
if (!total.isbn().empty()){ // changed
istream &is = cin; // changed
while (is) { // changed
Sales_data trans(is); // changed
if (total.isbn() == trans.isbn()) {
total.combine(trans);
}
else {
print(cout, total) << endl; // changed
total = trans;
}
print(cout, total);
cout << endl;
}
}
else {
cerr << "No data?!" << endl;
system("pause");
return -1;
}
system("pause");
return 0;
}
另一实现:
练习 7.14
编写一个构造函数,令其用我们提供的类内初始值显式地初始化成员。
解答
// 构造函数一
Sales_data (const std::string &book) : bookNo(book), units_sold(0), revenue(0) { }
// 构造函数二
Sales_data() : units_sold(0), revenue(0) { }
练习 7.15 - class Person - v4
为你的 Person 类添加正确的构造函数。
解答
类:
class Person
{
private:
public:
/* 数据成员 */ // 暂设为公共成员, 以便于 read 和 print 函数能够访问
string name; // 姓名
string address; // 地址
public:
/* 构造函数 */
Person() = default; // 默认构造函数
Person(const string &nam, const string &adr) : name(nam), address(adr) { } // 列表初始化
//Person(const string &nam, const string &adr) { name = nam; address = adr; } // 等价
Person(std::istream &is) { read(is, *this); } // 输入初始化
public:
/* 成员函数 */
string get_name() const { return name; } // 返回姓名
string get_address() const { return address; } // 返回地址
};
/* 非成员函数 - 调用函数 */
istream &read(istream &is, Person &person)
{
return (is >> person.name >> person.address);
}
ostream &print(ostream &os, const Person &person)
{
return (os << person.name << " " << person.address);
}
练习 7.16
在类的定义中对于访问说明符出现的位置和次数有限定吗? 如果有,是什么?什么样的成员应该定义在 public 说明符之后? 什么样的成员应该定义在 private 说明符之后?
解答
在类的定义中,可以包含 0 个或多个 访问说明符,并且对于某个访问说明符 能出现多少次以及能出现在哪里都没有严格规定。每个访问说明符指定接下来的成员的访问级别,有效范围直到出现下一个访问说明符或者到达类的结尾为止。
通常而言,作为接口的一部分,构造函数和一部分成员函数 应该定义在 public 说明符之后;而 数据成员和作为实现部分的函数 则应该跟在 private 说明符之后。
更一般地,若希望某成员在整个程序内都能够被访问,那么它应该定义为 public;若希望某成员只能在类内部访问,那么它应该定义为 private。
练习 7.17
使用 class 和 struct 时有区别吗 ?如果有,是什么?
解答
迄今为止,class 和 struct 的唯一区别是默认的访问级别不同:class 中没有表示访问权限的成员,其默认类型是 private;而 struct 中没有表示访问权限的成员,其默认类型是 public。
其实,结构体 struct 也可以有构造函数、析构函数、继承机制等。C++ 保留 struct 只是为了支持 C 语言。
建议,当希望做的更像是一种 数据结构 时,用 struct;当希望要做的更像是一种 对象 时,则用 class。
注意,访问控制级别应在程序中明确指出,而不是依靠默认,作为一个良好习惯能够使代码更具可读性。
此外,class 还用于定义模板参数,就像 typename,但 struct 不用于定义模板参数。
练习 7.18
封装 是何含义?它有什么用处?
解答
封装、继承、多态是类的三个特性,就封装而言:
练习 7.19
在你的 Person 类中,你将把哪些成员声明成 public 的? 哪些声明成 private 的? 解释你这样做的原因。
解答
理论上,构造函数、成员函数 getName() 和 getAddress() 应设为 public;数据成员 name 和 address 应设为 private。
函数 是暴露给外部的接口,因此要设为 public,以便于用户在类外访问;数据 则应设为 private 以隐藏使外部不可见,以避免用户的不经意修改甚至破坏。
实际上,当数据成员设为 private 时,非成员函数 / 调用函数 read() 和 print() 就无法访问它们了,以至于无法通过编译。因此,需要在某些程度上作出权衡或修正。
练习 7.20
友元 在什么时候有用?请分别举出使用友元的利弊。
解答
友元为类的非成员接口函数提供了访问其私有成员的能力,这种能力的提升利弊共存。
当非成员函数确实需要访问类的私有成员时,我们可以把它声明成该类的友元。此时,友元可以 “工作在类的内部”,像类的成员一样访问类的所有数据和函数。但是一旦使用不慎 (比如随意设定友元),就有可能破坏类的封装性乃至可维护性。
练习 7.21 - class Sales_data - v6
修改你的 Sales_data 类使其隐藏实现的细节。 你之前编写的关于 Sales_data 操作的程序应该继续使用,借助类的新定义重新编译该程序,确保其正常工作。
解答
#include <iostream>
#include <string>
using namespace std;
class Sales_data {
/* 友元函数 */
friend istream &read(istream &is, Sales_data &item);
friend ostream &print(ostream &os, const Sales_data &item);
friend Sales_data add(const Sales_data &lhs, const Sales_data &rhs);
private:
/* 数据成员 */
string bookNo; // 编号
unsigned units_sold = 0; // 销售量
double revenue = 0.0; // 营收
public:
/* 构造函数 */
Sales_data() = default; // 默认构造函数
Sales_data(const string &s):bookNo(s) { } // 使用构造函数初始值列表初始化
Sales_data(const string &s, unsigned n, double p):bookNo(s), units_sold(n), revenue(n*p){ }
Sales_data(istream &is) { read(is, *this); } // 结构体内声明和定义
/* 成员函数 */
string isbn() const { return bookNo; }; // 返回编号
Sales_data& combine(const Sales_data&); // 结合数据
};
// 成员函数
Sales_data& Sales_data::combine(const Sales_data& rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
// 友元函数 / 调用函数 / 非成员函数
istream &read(istream &is, Sales_data &item)
{
double price = 0;
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;
return os;
}
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
练习 7.22 - class Person - v5
修改你的 Person 类使其隐藏实现的细节。
解答
#include <iostream>
#include <string>
using namespace std;
class Person
{
/* 友元函数 */
friend istream &read(istream &is, Person &item);
friend ostream &print(ostream &os, const Person &item);
private:
/* 数据成员 */
string name; // 姓名
string address; // 地址
public:
/* 构造函数 */
Person() = default; // 默认构造函数
Person(const string &nam, const string &adr) : name(nam), address(adr) { } // 列表初始化
//Person(const string &nam, const string &adr) { name = nam; address = adr; } // 等价
Person(std::istream &is) { read(is, *this); } // 输入初始化
public:
/* 成员函数 */
string get_name() const { return name; } // 返回姓名
string get_address() const { return address; } // 返回地址
};
/* 友元函数 - 非成员函数 */
istream &read(istream &is, Person &person)
{
return (is >> person.name >> person.address);
}
ostream &print(ostream &os, const Person &person)
{
return (os << person.name << " " << person.address);
}
练习 7.23 - class Screen - v1
编写你自己的 Screen 类型。
解答
#include <iostream>
#include <string>
using namespace std;
class Screen
{
public:
/* 别名 */
using pos = std::string::size_type; // typedef std::string::size_type pos;
/* 构造函数 */
Screen() = default;
Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(ht*wd, c){ }
/* 成员函数 */
char get() const { return contents[cursor]; } // 当前光标位置字符
char get(pos r, pos c) const { return contents[r*width+c]; } // 指定坐标位置字符
private:
/* 数据成员 */
pos cursor = 0; // 光标位置
pos height = 0, width = 0; // 屏幕宽高
string contents; // 屏幕内容(像素阵列-平面矩阵)
};
练习 7.24 - class Screen - v2
给你的 Screen 类添加三个构造函数:一个默认构造函数;另一个构造函数接受宽和高的值,然后将 contents 初始化成给定数量的空白;第三个构造函数接受宽和高的值以及一个字符,该字符作为初始化后屏幕的内容。
解答
#include <iostream>
#include <string>
using namespace std;
class Screen
{
public:
/* 别名 */
using pos = std::string::size_type; // typedef std::string::size_type pos;
/* 构造函数 */
// 默认构造函数
Screen() = default;
// 接受宽和高的值,然后将contents初始化成给定数量的空白
Screen(pos ht, pos wd) : height(ht), width(wd), contents(ht*wd, ' ') { } // changed
// 接受宽和高的值以及一个字符,该字符作为初始化后屏幕的内容
Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(ht*wd, c) { }
/* 成员函数 */
char get() const { return contents[cursor]; } // 当前光标位置字符
char get(pos r, pos c) const { return contents[r*width+c]; } // 指定坐标位置字符
private:
/* 数据成员 */
pos cursor = 0; // 光标位置
pos height = 0, width = 0; // 屏幕宽高
string contents; // 屏幕内容(像素阵列-平面矩阵)
};
练习 7.25
Screen 能安全地依赖于拷贝和赋值操作的默认版本吗? 如果能,为什么?如果不能?为什么?
解答
含有指针数据成员的类一般不宜使用默认的拷贝和赋值操作。但如果类的数据成员都是内置类型的,则不受干扰。
Screen 的 4 个数据成员都是内置类型的 (String 类定义了拷贝和赋值运算符),因此直接使用类对象执行拷贝和赋值操作都是可以的。
练习 7.26
将 Sales_data::avg_price 定义成内联函数。
解答
隐式内联 - 把 avg_price 函数的 定义放在类的内部:
class Sales_data
{
public:
double avg_price() const { return (units_sold != 0) ? (revenue/units_sold) : 0}
}
显式内联 - 把 avg_price 函数的 声明放在类的内部,定义放在类的外部,并显式指定 inline 关键字:
class Sales_data
{
double avg_price() const;
}
inline double Sales_data::avg_price() const
{
{ return (units_sold != 0) ? (revenue/units_sold) : 0}
}
练习 7.27 - class Screen - v3
给你自己的 Screen 类添加 move、set 和 display 函数,通过执行下面的代码检验你的类是否正确。
Screen myScreen(5, 5, 'X'); myScreen.move(4, 0).set('#').display(cout); cout << "\n"; myScreen.display(cout); cout << "\n";
解答
类 - 三个版本:
方式一 - 全部成员函数体内声明体内定义 (隐式内联):
#include <iostream>
#include <string>
using namespace std;
class Screen
{
public:
/* 别名 */
using pos = std::string::size_type; // 须定义在使用前
/* 构造函数 */
Screen() = default;
Screen(pos ht, pos wd) : height(ht), width(wd), contents(ht*wd, ' ') { }
Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(ht*wd, c) { }
public:
/* 成员函数 */
// 获取字符
char get() const { return contents[cursor]; } // 获取当前光标位置字符
char get(pos r, pos c) const { return contents[r*width+c]; } // 获取指定坐标位置字符
// 移动光标
Screen &move(pos r, pos c) { // 将光标移动到指定位置
cursor = r * width + c;
return *this;
}
Screen &set(char ch) { // 设置当前光标位置字符
contents[cursor] = ch;
return *this;
}
// 设置字符
Screen &set(pos r, pos c, char ch) { // 设置指定坐标位置字符
contents[r*width+c] = ch;
return *this;
}
Screen &display() { // 显示屏幕阵列到标准输出
cout << contents;
return *this;
}
// 输出屏幕
Screen &display(ostream &os) { // 显示屏幕阵列到指定输出
os << contents;
return *this;
}
private:
/* 数据成员 */
pos cursor = 0; // 光标位置
pos height = 0, width = 0; // 屏幕宽高
string contents; // 屏幕内容(像素阵列-平面矩阵)
};
方式二 - 部分成员函数体内声明体外定义 (显式内联):
#include <iostream>
#include <string>
using namespace std;
class Screen
{
public:
/* 别名 */
using pos = std::string::size_type; // 须定义在使用前
/* 构造函数 */
Screen() = default;
Screen(pos ht, pos wd) : height(ht), width(wd), contents(ht*wd, ' ') { }
Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(ht*wd, c) { }
public:
/* 成员函数 */
// 类体内声明, 体内定义
char get() const { return contents[cursor]; } // 获取当前光标位置字符
char get(pos r, pos c) const { return contents[r*width+c]; } // 获取指定坐标位置字符
// 类体内声明, 体外定义
Screen &move(pos r, pos c);
Screen &set(char ch);
Screen &set(pos r, pos c, char ch);
Screen &display();
Screen &display(ostream &os);
private:
/* 数据成员 */
pos cursor = 0; // 光标位置
pos height = 0, width = 0; // 屏幕宽高
string contents; // 屏幕内容(像素阵列-平面矩阵)
};
/* 成员函数 */
inline Screen &Screen::move(pos r, pos c) // 将光标移动到指定位置
{
cursor = r * width + c;
return *this;
}
inline Screen &Screen::set(char ch) // 设置当前光标位置字符
{
contents[cursor] = ch;
return *this;
}
inline Screen &Screen::set(pos r, pos c, char ch) // 设置指定坐标位置字符
{
contents[r*width+c] = ch;
return *this;
}
inline Screen &Screen::display() // 显示屏幕阵列到标准输出
{
cout << contents;
return *this;
}
inline Screen &Screen::display(ostream &os) // 显示屏幕阵列到指定输出
{
os << contents;
return *this;
}
方式三 - 部分成员函数体内声明体外定义 (显式内联) + 显示功能封装至私有函数:
#include <iostream>
#include <string>
using namespace std;
class Screen
{
public:
/* 别名 */
using pos = std::string::size_type; // 须定义在使用前
/* 构造函数 */
Screen() = default;
Screen(pos ht, pos wd) : height(ht), width(wd), contents(ht*wd, ' ') { }
Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(ht*wd, c) { }
public:
/* 成员函数 */
// 类体内声明, 体内定义
char get() const { return contents[cursor]; } // 获取当前光标位置字符
char get(pos r, pos c) const { return contents[r*width+c]; } // 获取指定坐标位置字符
const Screen& display(ostream &os) const { do_display(os); return *this; }
Screen& display(ostream &os) { do_display(os); return *this; }
Screen& display() { do_display(); return *this; }
// 类体内声明, 体外定义
Screen &move(pos r, pos c);
Screen &set(char ch);
Screen &set(pos r, pos c, char ch);
private:
/* 私有函数成员 */
void do_display(ostream &os) const { os << contents; } // 辅助函数
void do_display() const { cout << contents; } // 辅助函数
private:
/* 数据成员 */
pos cursor = 0; // 光标位置
pos height = 0, width = 0; // 屏幕宽高
string contents; // 屏幕内容(像素阵列-平面矩阵)
};
/* 成员函数 */
inline Screen &Screen::move(pos r, pos c) // 将光标移动到指定位置
{
cursor = r * width + c;
return *this;
}
inline Screen &Screen::set(char ch) // 设置当前光标位置字符
{
contents[cursor] = ch;
return *this;
}
inline Screen &Screen::set(pos r, pos c, char ch) // 设置指定坐标位置字符
{
contents[r*width+c] = ch;
return *this;
}
主函数:
int main()
{
Screen myScreen(5, 5, 'X');
myScreen.move(4, 0).set('#').display(cout);
cout << "\n";
myScreen.display(cout);
cout << "\n";
system("pause");
return 0;
}
输出:
XXXXXXXXXXXXXXXXXXXX#XXXX
XXXXXXXXXXXXXXXXXXXX#XXXX
练习 7.28
如果 move、set 和 display 函数的返回类型不是 Screen& 而是 Screen,则在上一个练习中将会发生什么?
解答
练习 7.29
修改你的 Screen 类,令 move、set 和 display 函数返回 Screen 并检查程序的运行结果,在上一个练习中你的推测正确吗?
解答
推测正确,对比输出可见,有一处因为未使用引用而未能被修改。
// output (with '&')
XXXXXXXXXXXXXXXXXXXX#XXXX
XXXXXXXXXXXXXXXXXXXX#XXXX
^
// output (without '&')
XXXXXXXXXXXXXXXXXXXX#XXXX
XXXXXXXXXXXXXXXXXXXXXXXXX
^
练习 7.30
通过 this 指针使用成员的做法虽然合法,但是有点多余。讨论显示 使用 this 指针访问成员的优缺点。
解答
- 优点:可以非常明确地指出访问的是对象的成员,并且可以在成员函数中使用与数据成员同名的形参,例如:
void setAddr(const std::string &addr) { this->addr = addr; }
- 缺点:会使代码显得多余而不够简洁,例如:
std::string getAddr() const { return this->addr; }
练习 7.31
定义一对类 X 和 Y,其中 X 包含一个指向 Y 的指针,而 Y 包含一个类型为 X 的对象。
解答
class Y;
class X{
Y* y = nullptr;
};
class Y{
X x;
};
练习 7.32 - class Window_mgr + class Screen - v4
定义你自己的 Screen 和 Window_mgr,其中 clear 是 Window_mgr 的成员,是 Screen 的友元。
解答
#include <iostream>
#include <string>
#include <vector>
using namespace std;
/* 类 */
class Window_mgr
{
public:
using ScreenIndex = vector<Screen>::size_type; // 须定义在使用前
inline void clear(ScreenIndex);
private:
vector<Screen> screens;
};
/* 类 */
class Screen
{
friend void Window_mgr::clear(ScreenIndex); // 友元声明
public:
/* 别名 */
using pos = std::string::size_type; // 须定义在使用前
/* 构造函数 */
Screen() = default;
Screen(pos ht, pos wd) : height(ht), width(wd), contents(ht*wd, ' ') { }
Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(ht*wd, c) { }
public:
/* 公共成员函数 - 接口 */
// 类体内声明, 体内定义
char get() const { return contents[cursor]; } // 获取当前光标位置字符
char get(pos r, pos c) const { return contents[r*width+c]; } // 获取指定坐标位置字符
const Screen& display(ostream &os) const { do_display(os); return *this; }
Screen& display(ostream &os) { do_display(os); return *this; }
Screen& display() { do_display(); return *this; }
// 类体内声明, 体外定义
Screen &move(pos r, pos c);
Screen &set(char ch);
Screen &set(pos r, pos c, char ch);
private:
/* 私有函数成员 */
void do_display(ostream &os) const { os << contents; }
void do_display() const { cout << contents; }
private:
/* 私有数据成员 */
pos cursor = 0; // 光标位置
pos height = 0, width = 0; // 屏幕宽高
string contents; // 屏幕内容(像素阵列-平面矩阵)
};
/* 成员函数 */
inline void Window_mgr::clear(ScreenIndex i)
{
Screen &s = screens[i];
s.contents = string(s.height*s.width, ' ');
}
inline Screen &Screen::move(pos r, pos c) // 将光标移动到指定位置
{
cursor = r * width + c;
return *this;
}
inline Screen &Screen::set(char ch) // 设置当前光标位置字符
{
contents[cursor] = ch;
return *this;
}
inline Screen &Screen::set(pos r, pos c, char ch) // 设置指定坐标位置字符
{
contents[r*width+c] = ch;
return *this;
}
练习 7.33
如果我们给 Screen 添加一个如下所示的 size 成员将发生什么情况?如果出现了问题,请尝试修改它。
pos Screen::size() const { return height * width; }
解答
// 修改前, 会显示标识符 pos 未定义的错误
Screen::pos Screen::size() const
{
return height * width;
}
练习 7.34
如果我们把第 256 页 Screen 类的 pos 的 typedef 放在类的最后一行会发生什么情况?
解答
如此会导致编译出错,因为对标识符 pos 的使用出现在其声明之前,然而此时编译器并不知道 pos 的含义,并将其视为 未定义的标识符。
因此,类型名的定义通常出现在类的开始处,这样就能确保所有使用该类型的成员都出现在类名的定义之后。
练习 7.35
解释下面代码的含义,说明其中的 Type 和 initVal 分别使用了哪个定义。如果代码存在错误,尝试修改它。
typedef string Type; Type initVal(); class Exercise { public: typedef double Type; Type setVal(Type); Type initVal(); private: int val; }; Type Exercise::setVal(Type parm) { val = parm + initVal(); return val; }
解答
- 在类中,若成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。
- 因此,重复定义 Type 是错误的行为。然而,虽然重复定义类型名字是错误的行为,但编译器并不为此负责。
- 除了强制指定作用域,最好的解决方式还是尽量不要使用相同的标识符名称!
练习 7.36
下面的初始值是错误的,请找出问题所在并尝试修改它。
struct X { X (int i, int j): base(i), rem(base % j) {} int rem, base; };
解答
struct X {
X (int i, int j): base(i), rem(base % j) {}
int base, rem;
};
练习 7.37
使用本节提供的 Sales_data 类,确定初始化下面的变量时分别使用了哪个构造函数,然后罗列出每个对象所有的数据成员的值。
Sales_data first_item(cin); int main() { Sales_data next; Sales_data last("9-999-99999-9"); }
解答
Sales_data first_item(cin); // 使用 Sales_data(std::istream &is) ; 各成员值从输入流中读取
int main() {
// 使用默认构造函数
Sales_data next; // bookNo = "", cnt = 0, revenue = 0.0
// 使用 Sales_data(std::string s = "");
Sales_data last("9-999-99999-9"); // bookNo = "9-999-99999-9", cnt = 0, revenue = 0.0
}
练习 7.38
有些情况下我们希望提供 cin 作为接受 istream& 参数的构造函数的默认实参,请声明这样的构造函数。
解答
Sales_data(std::istream &is = std::cin) { read(is, *this); } // 形参列表中提供默认实参即可
练习 7.39
如果接受 string 的构造函数和接受 istream& 的构造函数都使用默认实参,这种行为合法吗?如果不,为什么?
解答
不合法。当调用 Sales_data() 构造函数时,无法确定该用哪一个重载的默认构造函数。
练习 7.40
从下面的抽象概念中选择一个(或者你自己指定一个),思考这样的类需要哪些数据成员,提供一组合理的构造函数并阐明这样做的原因。
(a) Book (b) Data (c) Employee (d) Vehicle (e) Object (f) Tree
解答
练习 7.41
使用委托构造函数重新编写你的 Sales_data 类,给每个构造函数体添加一条语句,令其一旦执行就打印一条信息。用各种可能的方式分别创建 Sales_data 对象,认真研究每次输出的信息直到你确实理解了委托构造函数的执行顺序。
解答
练习 7.42
对于你在练习 7.40 中编写的类,确定哪些构造函数可以使用委托。如果可以的话,编写委托构造函数。如果不可以,从抽象概念列表中重新选择一个你认为可以使用委托构造函数的,为挑选出的这个概念编写类定义。
解答
class Book
{
private:
/* 数据成员 */
unsigned isbn_;
std::string name_;
std::string author_;
std::string pubdate_;
public:
/* 构造函数 */
// 默认构造函数
Book(unsigned isbn, std::string const& name, std::string const& author, std::string const& pubdate)
:isbn_(isbn), name_(name), author_(author), pubdate_(pubdate) { }
// 委托构造函数
Book(unsigned isbn) : Book(isbn, "", "", "") {}
// 普通构造函数
explicit Book(std::istream &in) { in >> isbn_ >> name_ >> author_ >> pubdate_; }
};
练习 7.43
假定有一个名为 NoDefault 的类,它有一个接受 int 的构造函数,但是没有默认构造函数。定义类 C,C 有一个 NoDefault 类型的成员,定义 C 的默认构造函数。
解答
#include <iostream>
using namespace std;
// 该类没有显式定义默认构造函数, 编译器也不会为它合成一个
class NoDefault
{
public:
NoDefault(int i) { }
};
class C
{
public:
// NoDefault 没有默认构造函数, 必须显式调用 NoDefault 的带参构造函数初始化 obj
C(int j = 0) : obj(j) { }
private:
NoDefault obj;
};
练习 7.44
下面这条声明合法吗?如果不,为什么?
vector<NoDefault> vec(10); // 初始化含有 10 个 NoDefault 类型对象的 vector
解答
练习 7.45
如果在上一个练习中定义的 vector 的元素类型是 C,则声明合法吗?为什么?
解答
合法,因为不同于 NoDefault,C 是有默认构造函数的。
练习 7.46
下面哪些论断是不正确的?为什么?
- (a) 一个类必须至少提供一个构造函数。
- (b) 默认构造函数是参数列表为空的构造函数。
- (c) 如果对于类来说不存在有意义的默认值,则类不应该提供默认构造函数。
- (d) 如果类没有定义默认构造函数,则编译器将为其生成一个并把每个数据成员初始化成相应类型的默认值。
解答
- (a) 不正确,当类不提供构造函数时,编译器会自动实现一个合成的默认构造函数。
- (b) 不完全正确,为每个参数都提供了默认值的构造函数也是一种默认构造函数。
- (c) 不正确,当编译器确实需要隐式地使用默认构造函数时,没有默认构造函数的类将无法使用 (如练习 7.44),因此哪怕没有意义的值也需要提供默认构造函数实现初始化。
- (d) 不正确,对于编译器合成的默认构造函数而言,类类型的成员执行各自所属类的默认构造函数,内置类型和符合类型的成员只对定义在全局作用域中的对象执行初始化。
练习 7.47
说明接受一个 string 参数的 Sales_data 构造函数是否应该是 explicit 的,并解释这样做的优缺点。
解答
练习 7.48
假定 Sales_data 的构造函数不是 explicit 的,则下述定义将执行什么样的操作?
string null_isbn("9-999-9999-9"); Sales_data item1(null_isbn); Sales_data item2("9-999-99999-9");
解答
练习 7.49
对于 combine 函数的三种不同声明,当我们调用 i.combine(s) 时分别发生什么情况?其中 i 是一个 Sales_data,而 s是一个 string 对象。
(a) Sales_data &combine(Sales_data); (b) Sales_data &combine(Sales_data&); (c) Sales_data &combine(const Sales_data&) const;
解答
(a) Sales_data &combine(Sales_data); // ok
(b) Sales_data &combine(Sales_data&); // error C2664: 无法将参数 1 从 “std::string” 转换为 “Sales_data &”
// 因为隐式转换只有一次
(c) Sales_data &combine(const Sales_data&) const; // 该成员函数是 const 的, 意味着不能改变对象
// 而 combine 函数的本意就是要改变对象
练习 7.50
确定在你的 Person 类中是否有一些构造函数应该是 explicit 的。
解答
explicit Person(std::istream &is){ read(is, *this); }
练习 7.51
vector 将其单参数的构造函数定义成 explicit 的,而 string 则不是,你觉得原因何在?
解答
假如有如下函数:
int getSize(const std::vector<int>&);
若未将该单参数构造函数定义成 explicit 的,则可以如此调用:
getSize(16);
然而,很明显如此调用会让人产生困惑:函数实际上会初始化一个含有 16 个 int 元素的 vector 临时量,然后返回 16。但这样做没有任何意义。
而 string 则不同,其单参数构造函数的参数是 const char *,因此凡是在需要用到 string 的地方都可以用 const char * 来代替(字面值就是 const char *)。例如:
void print(std::string);
print("hello world");
练习 7.52
使用 2.6.1 节的 Sales_data 类,解释下面的初始化过程。如果存在问题,尝试修改它。
Sales_data item = {"987-0590353403", 25, 15.99};
解答
练习 7.53
定义你自己的 Debug 。
解答
class Debug {
public:
constexpr Debug(bool b = true) : hw(b), io(b), other(b) { }
constexpr Debug(bool h, bool i, bool o) : hw(r), io(i), other(0) { }
constexpr bool any() { return hw || io || other; }
void set_hw(bool b) { hw = b; }
void set_io(bool b) { io = b; }
void set_other(bool b) { other = b; }
private:
bool hw; // runtime error
bool io; // I/O error
bool other; // the others
};
练习 7.54
Debug 中以 set_ 开头的成员应该被声明成 constexpr 吗?如果不,为什么?
解答
这些以 set_ 开头的成员不能声明成 constexpr,这些函数的作用是设置数据成员的值,而 constexpr 函数只能保护 return 语句,不允许执行其他任务。
练习 7.55
7.5.5 节的 Data 类是字面值常量类吗?请解释原因。
解答
因为 Data 类是聚合类,所以它也是一个字面值常量类。
练习 7.56
什么是类的静态成员?它有何优点?静态成员与普通成员有何区别?
解答
练习 7.57
编写你自己的 Account 类。
解答
class Account {
public:
void calculate() { amount += amount * interestRate; }
static double rate() { return interestRate; }
static void rate(double newRate) { interestRate = newRate; }
private:
std::string owner;
double amount;
static double interestRate;
static constexpr double todayRate = 12.34;
static double initRate() { return todayRate; }
};
double Account::interestRate = initRate();
练习 7.58
下面的静态数据成员的声明和定义有错误吗?请解释原因。
//example.h class Example { public: static double rate = 6.5; static const int vecSize = 20; static vector<double> vec(vecSize); }; //example.c #include "example.h" double Example::rate; vector<double> Example::vec;
解答
因此,可以将 rate 修改为常量表达式 constexpr,并且将 vec 的声明放在类中、定义和初始化放在类外。
// example.h
class Example {
public:
static constexpr double rate = 6.5;
static const int vecSize = 20;
static vector<double> vec;
};
// example.C
#include "example.h"
constexpr double Example::rate;
vector<double> Example::vec(Example::vecSize);