# Chapter 7 Classes
# Defining Abstract Data Types
# 定义抽象数据类型
- 类背后的基本思想:数据抽象(data abstraction)和封装(encapsulation)。
- 数据抽象是一种依赖于接口(interface)和实现(implementation)分离的编程技术。
# 类成员 (Member)
- 必须在类的内部声明,不能在其他地方增加成员。
- 成员可以是数据,函数,类型别名。
# 类的成员函数
- 成员函数的声明必须在类的内部。
- 成员函数的定义既可以在类的内部也可以在外部。
- 使用点运算符
.
调用成员函数。 - 必须对任何
const
或引用类型成员以及没有默认构造函数的类类型的任何成员使用初始化式。 ConstRef::ConstRef(int ii): i(ii), ci(i), ri(ii) { }
- 默认实参:
Sales_item(const std::string &book): isbn(book), units_sold(0), revenue(0.0) { }
*this
:- 每个成员函数都有一个额外的,隐含的形参
this
。 this
总是指向当前对象,因此this
是一个常量指针。- 形参表后面的
const
,改变了隐含的this
形参的类型,如bool same_isbn(const Sales_item &rhs) const
,这种函数称为 “常量成员函数”(this
指向的当前对象是常量)。 return *this;
可以让成员函数连续调用。- 普通的非
const
成员函数:this
是指向类类型的const
指针(可以改变this
所指向的值,不能改变this
保存的地址)。 const
成员函数:this
是指向 const 类类型的const
指针(既不能改变this
所指向的值,也不能改变this
保存的地址)。
- 每个成员函数都有一个额外的,隐含的形参
# 非成员函数
- 和类相关的非成员函数,定义和声明都应该在类的外部。
# 类的构造函数
- 类通过一个或者几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。
- 构造函数是特殊的成员函数。
- 构造函数放在类的
public
部分。 - 与类同名的成员函数。
Sales_item(): units_sold(0), revenue(0.0) { }
=default
要求编译器合成默认的构造函数。(C++11
)- 初始化列表:冒号和花括号之间的代码:
Sales_item(): units_sold(0), revenue(0.0) { }
# Exercise 7.1
使用 2.6.1 节定义的 Sales_data
类为 1.6 节的交易处理程序编写一个新版本。
解:
#include <iostream> | |
#include <string> | |
using std::cin; using std::cout; using std::endl; using std::string; | |
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 | |
{ | |
std::cerr << "No data?!" << std::endl; | |
return -1; | |
} | |
return 0; | |
} |
# Exercise 7.2
曾在 2.6.2 节的练习中编写了一个 Sales_data
类,请向这个类添加 combine
函数和 isbn
成员。
解:
#include <string> | |
struct Sales_data { | |
std::string isbn() const { return bookNo; }; | |
Sales_data& combine(const Sales_data&); | |
std::string bookNo; | |
unsigned units_sold = 0; | |
double revenue = 0.0; | |
}; | |
Sales_data& Sales_data::combine(const Sales_data& rhs) | |
{ | |
units_sold += rhs.units_sold; | |
revenue += rhs.revenue; | |
return *this; | |
} |
# Exercise 7.3
修改 7.1.1 节的交易处理程序,令其使用这些成员。
解:
#include <iostream> | |
using std::cin; using std::cout; using std::endl; | |
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()) | |
total.combine(trans); | |
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; | |
return -1; | |
} | |
return 0; | |
} |
# Exercise 7.4
编写一个名为 Person
的类,使其表示人员的姓名和地址。使用 string
对象存放这些元素,接下来的练习将不断充实这个类的其他特征。
解:
#include <string> | |
class Person { | |
std::string name; | |
std::string address; | |
}; |
# Exercise 7.5
在你的 Person
类中提供一些操作使其能够返回姓名和地址。
这些函数是否应该是 const
的呢?解释原因。
解:
#include <string> | |
class Person | |
{ | |
std::string name; | |
std::string address; | |
public: | |
auto get_name() const -> std::string const& { return name; } | |
auto get_addr() const -> std::string const& { return address; } | |
}; |
应该是 const
的。因为常量的 Person
对象也需要使用这些函数操作。
# Exercise 7.6
对于函数 add
、 read
和 print
,定义你自己的版本。
解:
#include <string> | |
#include <iostream> | |
struct Sales_data { | |
std::string const& isbn() const { return bookNo; }; | |
Sales_data& combine(const Sales_data&); | |
std::string bookNo; | |
unsigned units_sold = 0; | |
double revenue = 0.0; | |
}; | |
// member functions. | |
Sales_data& Sales_data::combine(const Sales_data& rhs) | |
{ | |
units_sold += rhs.units_sold; | |
revenue += rhs.revenue; | |
return *this; | |
} | |
// nonmember functions | |
std::istream &read(std::istream &is, Sales_data &item) | |
{ | |
double price = 0; | |
is >> item.bookNo >> item.units_sold >> price; | |
item.revenue = price * item.units_sold; | |
return is; | |
} | |
std::ostream &print(std::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; | |
} |
# Exercise 7.7
使用这些新函数重写 7.1.2 节练习中的程序。
int main() | |
{ | |
Sales_data total; | |
if (read(std::cin, total)) | |
{ | |
Sales_data trans; | |
while (read(std::cin, trans)) { | |
if (total.isbn() == trans.isbn()) | |
total.combine(trans); | |
else { | |
print(std::cout, total) << std::endl; | |
total = trans; | |
} | |
} | |
print(std::cout, total) << std::endl; | |
} | |
else | |
{ | |
std::cerr << "No data?!" << std::endl; | |
return -1; | |
} | |
return 0; | |
} |
# Exercise 7.8
为什么 read
函数将其 Sales_data
参数定义成普通的引用,而 print
函数将其参数定义成常量引用?
解:
因为 read
函数会改变对象的内容,而 print
函数不会。
# Exercise 7.9
对于 7.1.2 节练习中代码,添加读取和打印 Person
对象的操作。
解:
#include <string> | |
#include <iostream> | |
struct Person | |
{ | |
std::string const& getName() const { return name; } | |
std::string const& getAddress() const { return address; } | |
std::string name; | |
std::string address; | |
}; | |
std::istream &read(std::istream &is, Person &person) | |
{ | |
return is >> person.name >> person.address; | |
} | |
std::ostream &print(std::ostream &os, const Person &person) | |
{ | |
return os << person.name << " " << person.address; | |
} |
# Exercise 7.10
在下面这条 if
语句中,条件部分的作用是什么?
if (read(read(cin, data1), data2)) // 等价 read (std::cin, data1);read (std::cin, data2); |
解:
read
函数的返回值是 istream
对象,if
语句中条件部分的作用是从输入流中读取数据给两个 data
对象。
# Exercise 7.11 :
在你的 Sales_data
类中添加构造函数,
然后编写一段程序令其用到每个构造函数。
解:
头文件:
#include <string> | |
#include <iostream> | |
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); | |
std::string isbn() const { return bookNo; }; | |
Sales_data& combine(const Sales_data&); | |
std::string bookNo; | |
unsigned units_sold = 0; | |
double revenue = 0.0; | |
}; | |
// nonmember functions | |
std::istream &read(std::istream &is, Sales_data &item) | |
{ | |
double price = 0; | |
is >> item.bookNo >> item.units_sold >> price; | |
item.revenue = price * item.units_sold; | |
return is; | |
} | |
std::ostream &print(std::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; | |
} | |
// member functions. | |
Sales_data::Sales_data(std::istream &is) | |
{ | |
read(is, *this); | |
} | |
Sales_data& Sales_data::combine(const Sales_data& rhs) | |
{ | |
units_sold += rhs.units_sold; | |
revenue += rhs.revenue; | |
return *this; | |
} |
主函数:
int main() | |
{ | |
Sales_data item1; | |
print(std::cout, item1) << std::endl; | |
Sales_data item2("0-201-78345-X"); | |
print(std::cout, item2) << std::endl; | |
Sales_data item3("0-201-78345-X", 3, 20.00); | |
print(std::cout, item3) << std::endl; | |
Sales_data item4(std::cin); | |
print(std::cout, item4) << std::endl; | |
return 0; | |
} |
# Exercise 7.12
把只接受一个 istream
作为参数的构造函数移到类的内部。
解:
#include <string> | |
#include <iostream> | |
struct Sales_data; | |
std::istream &read(std::istream&, 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(n*p){ } | |
Sales_data(std::istream &is) { read(is, *this); } | |
std::string isbn() const { return bookNo; }; | |
Sales_data& combine(const Sales_data&); | |
std::string bookNo; | |
unsigned units_sold = 0; | |
double revenue = 0.0; | |
}; | |
// member functions. | |
Sales_data& Sales_data::combine(const Sales_data& rhs) | |
{ | |
units_sold += rhs.units_sold; | |
revenue += rhs.revenue; | |
return *this; | |
} | |
// nonmember functions | |
std::istream &read(std::istream &is, Sales_data &item) | |
{ | |
double price = 0; | |
is >> item.bookNo >> item.units_sold >> price; | |
item.revenue = price * item.units_sold; | |
return is; | |
} | |
std::ostream &print(std::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; | |
} |
# Exercise 7.13
使用 istream
构造函数重写第 229 页的程序。
解:
int main() | |
{ | |
Sales_data total(std::cin); | |
if (!total.isbn().empty()) | |
{ | |
std::istream &is = std::cin; | |
while (is) { | |
Sales_data trans(is); | |
if (!is) break; | |
if (total.isbn() == trans.isbn()) | |
total.combine(trans); | |
else { | |
print(std::cout, total) << std::endl; | |
total = trans; | |
} | |
} | |
print(std::cout, total) << std::endl; | |
} | |
else | |
{ | |
std::cerr << "No data?!" << std::endl; | |
return -1; | |
} | |
return 0; | |
} |
# Exercise 7.14
编写一个构造函数,令其用我们提供的类内初始值显式地初始化成员。
Sales_data() : units_sold(0) , revenue(0) { } |
# Exercise 7.15
为你的 Person
类添加正确的构造函数。
解:
#include <string> | |
#include <iostream> | |
struct Person; | |
std::istream &read(std::istream&, Person&); | |
struct Person | |
{ | |
Person() = default; | |
Person(const std::string& sname, const std::string& saddr) :name(sname), address(saddr) {} | |
Person(std::istream &is) { read(is, *this); } | |
std::string getName() const { return name; } | |
std::string getAddress() const { return address; } | |
std::string name; | |
std::string address; | |
}; | |
std::istream &read(std::istream &is, Person &person) | |
{ | |
is >> person.name >> person.address; | |
return is; | |
} | |
std::ostream &print(std::ostream &os, const Person &person) | |
{ | |
os << person.name << " " << person.address; | |
return os; | |
} |
# Access Control and Encapsulation
# 访问控制与封装
- 访问说明符(access specifiers):
public
:定义在public
后面的成员在整个程序内可以被访问;public
成员定义类的接口。private
:定义在private
后面的成员可以被类的成员函数访问,但不能被使用该类的代码访问;private
隐藏了类的实现细节。
- 使用
class
或者struct
:都可以被用于定义一个类。唯一的却别在于访问权限。- 使用
class
:在第一个访问说明符之前的成员是priavte
的。 - 使用
struct
:在第一个访问说明符之前的成员是public
的。
- 使用
# 友元
- 允许特定的非成员函数访问一个类的私有成员.
- 友元的声明以关键字
friend
开始。friend Sales_data add(const Sales_data&, const Sales_data&);
表示非成员函数add
可以访问类的非公有成员。 - 通常将友元声明成组地放在类定义的开始或者结尾。
- 类之间的友元:
- 如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
# 封装的益处
- 确保用户的代码不会无意间破坏封装对象的状态。
- 被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码。
# Exercise 7.16
在类的定义中对于访问说明符出现的位置和次数有限定吗?
如果有,是什么?什么样的成员应该定义在 public
说明符之后?
什么样的成员应该定义在 private
说明符之后?
解:
在类的定义中对于访问说明符出现的位置和次数没有限定。
每个访问说明符指定了接下来的成员的访问级别,其有效范围直到出现下一个访问说明符或者达到类的结尾处为止。
如果某个成员能够在整个程序内都被访问,那么它应该定义为 public
;
如果某个成员只能在类内部访问,那么它应该定义为 private
。
# Exercise 7.17
使用 class
和 struct
时有区别吗?如果有,是什么?
解:
class
和 struct
的唯一区别是默认的访问级别不同。
# Exercise 7.18
封装是何含义?它有什么用处?
解:
将类内部分成员设置为外部不可见,而提供部分接口给外面,这样的行为叫做封装。
用处:
- 1. 确保用户的代码不会无意间破坏封装对象的状态。
- 2. 被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码。
# Exercise 7.19
在你的 Person
类中,你将把哪些成员声明成 public
的?
哪些声明成 private
的?
解释你这样做的原因。
构造函数、 getName()
、 getAddress()
函数将设为 public
。name
和 address
将设为 private
。
函数是暴露给外部的接口,因此要设为 public
;
而数据则应该隐藏让外部不可见。
# Exercise 7.20
友元在什么时候有用?请分别举出使用友元的利弊。
解:
当其他类或者函数想要访问当前类的私有变量时,这个时候应该用友元。
利:
与当前类有关的接口函数能直接访问类的私有变量。
弊:
牺牲了封装性与可维护性。
# Exercise 7.21
修改你的 Sales_data
类使其隐藏实现的细节。
你之前编写的关于 Sales_data
操作的程序应该继续使用,借助类的新定义重新编译该程序,确保其正常工作。
解:
#include <string> | |
#include <iostream> | |
class Sales_data { | |
friend std::istream &read(std::istream &is, Sales_data &item); | |
friend std::ostream &print(std::ostream &os, const Sales_data &item); | |
friend Sales_data add(const Sales_data &lhs, const Sales_data &rhs); | |
public: | |
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) { read(is, *this); } | |
std::string isbn() const { return bookNo; }; | |
Sales_data& combine(const Sales_data&); | |
private: | |
std::string bookNo; | |
unsigned units_sold = 0; | |
double revenue = 0.0; | |
}; | |
// member functions. | |
Sales_data& Sales_data::combine(const Sales_data& rhs) | |
{ | |
units_sold += rhs.units_sold; | |
revenue += rhs.revenue; | |
return *this; | |
} | |
// friend functions | |
std::istream &read(std::istream &is, Sales_data &item) | |
{ | |
double price = 0; | |
is >> item.bookNo >> item.units_sold >> price; | |
item.revenue = price * item.units_sold; | |
return is; | |
} | |
std::ostream &print(std::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; | |
} |
# Exercise 7.22
修改你的 Person
类使其隐藏实现的细节。
解:
#include <string> | |
#include <iostream> | |
class Person { | |
friend std::istream &read(std::istream &is, Person &person); | |
friend std::ostream &print(std::ostream &os, const Person &person); | |
public: | |
Person() = default; | |
Person(const std::string sname, const std::string saddr):name(sname), address(saddr){ } | |
Person(std::istream &is){ read(is, *this); } | |
std::string getName() const { return name; } | |
std::string getAddress() const { return address; } | |
private: | |
std::string name; | |
std::string address; | |
}; | |
std::istream &read(std::istream &is, Person &person) | |
{ | |
is >> person.name >> person.address; | |
return is; | |
} | |
std::ostream &print(std::ostream &os, const Person &person) | |
{ | |
os << person.name << " " << person.address; | |
return os; | |
} |
# Additional Class Features
# 类的其他特性
- 成员函数作为内联函数
inline
:- 在类的内部,常有一些规模较小的函数适合于被声明成内联函数。
- 定义在类内部的函数是自动内联的。
- 在类外部定义的成员函数,也可以在声明时显式地加上
inline
。
- 可变数据成员 (mutable data member):
mutable size_t access_ctr;
- 永远不会是
const
,即使它是const
对象的成员。
- 类类型:
- 每个类定义了唯一的类型。
# Exercise 7.23
编写你自己的 Screen
类型。
解:
#include <string> | |
class Screen { | |
public: | |
using pos = std::string::size_type; | |
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; | |
std::string contents; | |
}; |
# Exercise 7.24
给你的 Screen
类添加三个构造函数:一个默认构造函数;另一个构造函数接受宽和高的值,然后将 contents
初始化成给定数量的空白;第三个构造函数接受宽和高的值以及一个字符,该字符作为初始化后屏幕的内容。
解:
#include <string> | |
class Screen { | |
public: | |
using pos = std::string::size_type; | |
Screen() = default; // 1 | |
Screen(pos ht, pos wd):height(ht), width(wd), contents(ht*wd, ' '){ } // 2 | |
Screen(pos ht, pos wd, char c):height(ht), width(wd), contents(ht*wd, c){ } // 3 | |
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; | |
std::string contents; | |
}; |
# Exercise 7.25
Screen
能安全地依赖于拷贝和赋值操作的默认版本吗?
如果能,为什么?如果不能?为什么?
解:
能。 Screen
的成员只有内置类型和 string
,因此能安全地依赖于拷贝和赋值操作的默认版本。
管理动态内存的类则不能依赖于拷贝和赋值操作的默认版本,而且也应该尽量使用 string
和 vector
来避免动态管理内存的复杂性。
# Exercise 7.26
将 Sales_data::avg_price
定义成内联函数。
解:
在头文件中加入:
inline double Sales_data::avg_price() const | |
{ | |
return units_sold ? revenue/units_sold : 0; | |
} |
# Exercise 7.27
给你自己的 Screen
类添加 move
、 set
和 display
函数,通过执行下面的代码检验你的类是否正确。
Screen myScreen(5, 5, 'X'); | |
myScreen.move(4, 0).set('#').display(cout); | |
cout << "\n"; | |
myScreen.display(cout); | |
cout << "\n"; |
解:
增加代码:
#include <string> | |
#include <iostream> | |
class Screen { | |
public: | |
... ... | |
inline Screen& move(pos r, pos c); | |
inline Screen& set(char c); | |
inline Screen& set(pos r, pos c, char ch); | |
const Screen& display(std::ostream &os) const { do_display(os); return *this; } | |
Screen& display(std::ostream &os) { do_display(os); return *this; } | |
private: | |
void do_display(std::ostream &os) const { os << contents; } | |
... ... | |
}; | |
inline Screen& Screen::move(pos r, pos c) | |
{ | |
cursor = r*width + c; | |
return *this; | |
} | |
inline Screen& Screen::set(char c) | |
{ | |
contents[cursor] = c; | |
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(std::cout); | |
std::cout << "\n"; | |
myScreen.display(std::cout); | |
std::cout << "\n"; | |
return 0; | |
} |
# Exercise 7.28
如果 move
、 set
和 display
函数的返回类型不是 Screen&
而是 Screen
,则在上一个练习中将会发生什么?
解:
如果返回类型是 Screen
,那么 move
返回的是 *this
的一个副本,因此 set
函数只能改变临时副本而不能改变 myScreen
的值。
# Exercise 7.29
修改你的 Screen
类,令 move
、 set
和 display
函数返回 Screen
并检查程序的运行结果,在上一个练习中你的推测正确吗?
解:
推测正确。
#with '&'
XXXXXXXXXXXXXXXXXXXX#XXXX
XXXXXXXXXXXXXXXXXXXX#XXXX
^
# without '&'
XXXXXXXXXXXXXXXXXXXX#XXXX
XXXXXXXXXXXXXXXXXXXXXXXXX
^
# Exercise 7.30
通过 this
指针使用成员的做法虽然合法,但是有点多余。讨论显示使用指针访问成员的优缺点。
解:
优点:
程序的意图更明确
函数的参数可以与成员同名,如
void setAddr(const std::string &addr) { this->addr = addr; } |
缺点:
有时候显得有点多余,如
std::string getAddr() const { return this->addr; } |
# Exercise 7.31
定义一对类 X
和 Y
,其中 X
包含一个指向 Y
的指针,而 Y
包含一个类型为 X
的对象。
解:
class Y; | |
class X{ | |
Y* y = nullptr; | |
}; | |
class Y{ | |
X x; | |
}; |
# Exercise 7.32
定义你自己的 Screen
和 Window_mgr
,其中 clear
是 Window_mgr
的成员,是 Screen
的友元。
解:
#include <vector> | |
#include <iostream> | |
#include <string> | |
class Screen; | |
class Window_mgr | |
{ | |
public: | |
using ScreenIndex = std::vector<Screen>::size_type; | |
inline void clear(ScreenIndex); | |
private: | |
std::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) {} | |
char get() const { return contents[cursor]; } | |
char get(pos r, pos c) const { return contents[r*width + c]; } | |
inline Screen& move(pos r, pos c); | |
inline Screen& set(char c); | |
inline Screen& set(pos r, pos c, char ch); | |
const Screen& display(std::ostream& os) const { do_display(os); return *this; } | |
Screen& display(std::ostream& os) { do_display(os); return *this; } | |
private: | |
void do_display(std::ostream &os) const { os << contents; } | |
private: | |
pos cursor = 0; | |
pos width = 0, height = 0; | |
std::string contents; | |
}; | |
inline void Window_mgr::clear(ScreenIndex i) | |
{ | |
Screen& s = screens[i]; | |
s.contents = std::string(s.height*s.width,' '); | |
} | |
inline Screen& Screen::move(pos r, pos c) | |
{ | |
cursor = r*width + c; | |
return *this; | |
} | |
inline Screen& Screen::set(char c) | |
{ | |
contents[cursor] = c; | |
return *this; | |
} | |
inline Screen& Screen::set(pos r, pos c, char ch) | |
{ | |
contents[r*width + c] = ch; | |
return *this; | |
} |
# Class Scope
# 类的作用域
- 每个类都会定义它自己的作用域。在类的作用域之外,普通的数据和函数成员只能由引用、对象、指针使用成员访问运算符来访问。
- 函数的返回类型通常在函数名前面,因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外。
- 如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。
- 类中的类型名定义都要放在一开始。
# Exercise 7.33
如果我们给 Screen
添加一个如下所示的 size
成员将发生什么情况?如果出现了问题,请尝试修改它。
pos Screen::size() const | |
{ | |
return height * width; | |
} |
解:
纠正:错误为 error: extra qualification 'Screen::' on member'size' [-fpermissive]
则应该去掉 Screen::, 改为
pos size() const{ | |
return height * width; | |
} |
# Exercise 7.34
如果我们把第 256 页 Screen
类的 pos
的 typedef
放在类的最后一行会发生什么情况?
解:
在 dummy_fcn (pos height) 函数中会出现 未定义的标识符 pos。
类型名的定义通常出现在类的开始处,这样就能确保所有使用该类型的成员都出现在类名的定义之后。
# Exercise 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; | |
} |
解:
书上 255 页中说:
然而在类中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。
因此重复定义 Type
是错误的行为。
虽然重复定义类型名字是错误的行为,但是编译器并不为此负责。所以我们要人为地遵守一些原则,在这里有一些讨论。
# Constructors Revisited
# 构造函数再探
- 构造函数初始值列表:
- 类似
python
使用赋值的方式有时候不行,比如const
或者引用类型的数据,只能初始化,不能赋值。(注意初始化和赋值的区别) - 最好让构造函数初始值的顺序和成员声明的顺序保持一致。
- 如果一个构造函数为所有参数都提供了默认参数,那么它实际上也定义了默认的构造函数。
- 类似
# 委托构造函数 (delegating constructor, C++11
)
- 委托构造函数将自己的职责委托给了其他构造函数。
Sale_data(): Sale_data("", 0, 0) {}
# 隐式的类型转换
- 如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制。这种构造函数又叫转换构造函数(converting constructor)。
- 编译器只会自动地执行
仅一步
类型转换。 - 抑制构造函数定义的隐式转换:
- 将构造函数声明为
explicit
加以阻止。 explicit
构造函数只能用于直接初始化,不能用于拷贝形式的初始化。
- 将构造函数声明为
# 聚合类 (aggregate class)
- 满足以下所有条件:
- 所有成员都是
public
的。 - 没有定义任何构造函数。
- 没有类内初始值。
- 没有基类,也没有
virtual
函数。
- 所有成员都是
- 可以使用一个花括号括起来的成员初始值列表,初始值的顺序必须和声明的顺序一致。
# 字面值常量类
constexpr
函数的参数和返回值必须是字面值。- 字面值类型:除了算术类型、引用和指针外,某些类也是字面值类型。
- 数据成员都是字面值类型的聚合类是字面值常量类。
- 如果不是聚合类,则必须满足下面所有条件:
- 数据成员都必须是字面值类型。
- 类必须至少含有一个
constexpr
构造函数。 - 如果一个数据成员含有类内部初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的
constexpr
构造函数。 - 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
# Exercise 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; | |
}; |
# Exercise 7.37
使用本节提供的 Sales_data
类,确定初始化下面的变量时分别使用了哪个构造函数,然后罗列出每个对象所有的数据成员的值。
解:
Sales_data first_item(cin); // 使用 Sales_data (std::istream &is) ; 各成员值从输入流中读取 | |
int main() { | |
// 使用默认构造函数 bookNo = "", cnt = 0, revenue = 0.0 | |
Sales_data next; | |
// 使用 Sales_data (std::string s = ""); bookNo ="9-999-99999-9", cnt = 0, revenue = 0.0 | |
Sales_data last("9-999-99999-9"); | |
} |
# Exercise 7.38
有些情况下我们希望提供 cin
作为接受 istream&
参数的构造函数的默认实参,请声明这样的构造函数。
解:
Sales_data(std::istream &is = std::cin) { read(is, *this); } |
# Exercise 7.39
如果接受 string
的构造函数和接受 istream&
的构造函数都使用默认实参,这种行为合法吗?如果不,为什么?
解:
不合法。当你调用 Sales_data()
构造函数时,无法区分是哪个重载。
# Exercise 7.40
从下面的抽象概念中选择一个(或者你自己指定一个),思考这样的类需要哪些数据成员,提供一组合理的构造函数并阐明这样做的原因。
(a) Book
(b) Data
(c) Employee
(d) Vehicle
(e) Object
(f) Tree
解:
(a) Book.
class Book | |
{ | |
public: | |
Book(unsigned isbn, std::string const& name, std::string const& author, std::string const& pubdate) | |
:isbn_(isbn), name_(name), author_(author), pubdate_(pubdate) | |
{ } | |
explicit Book(std::istream &in) | |
{ | |
in >> isbn_ >> name_ >> author_ >> pubdate_; | |
} | |
private: | |
unsigned isbn_; | |
std::string name_; | |
std::string author_; | |
std::string pubdate_; | |
}; |
# Exercise 7.41
使用委托构造函数重新编写你的 Sales_data
类,给每个构造函数体添加一条语句,令其一旦执行就打印一条信息。用各种可能的方式分别创建 Sales_data
对象,认真研究每次输出的信息直到你确实理解了委托构造函数的执行顺序。
解:
- 头文件
- 源文件
- 主函数
总结:使用委托构造函数,调用顺序是:
- 1. 实际的构造函数的函数体。
- 2. 委托构造函数的函数体。
# Exercise 7.42
对于你在练习 7.40 中编写的类,确定哪些构造函数可以使用委托。如果可以的话,编写委托构造函数。如果不可以,从抽象概念列表中重新选择一个你认为可以使用委托构造函数的,为挑选出的这个概念编写类定义。
解:
class Book | |
{ | |
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_; | |
} | |
private: | |
unsigned isbn_; | |
std::string name_; | |
std::string author_; | |
std::string pubdate_; | |
}; |
# Exercise 7.43
假定有一个名为 NoDefault
的类,它有一个接受 int
的构造函数,但是没有默认构造函数。定义类 C
, C
有一个 NoDefault
类型的成员,定义 C
的默认构造函数。
class NoDefault { | |
public: | |
NoDefault(int i) { } | |
}; | |
class C { | |
public: | |
C() : def(0) { } | |
private: | |
NoDefault def; | |
}; |
# Exercise 7.44
下面这条声明合法吗?如果不,为什么?
vector<NoDefault> vec(10);//vec 初始化有 10 个元素 |
解:
不合法。因为 NoDefault
没有默认构造函数。
# Exercise 7.45
如果在上一个练习中定义的 vector 的元素类型是 C,则声明合法吗?为什么?
合法。因为 C
有默认构造函数。
# Exercise 7.46
下面哪些论断是不正确的?为什么?
- (a) 一个类必须至少提供一个构造函数。
- (b) 默认构造函数是参数列表为空的构造函数。
- (c) 如果对于类来说不存在有意义的默认值,则类不应该提供默认构造函数。
- (d) 如果类没有定义默认构造函数,则编译器将为其生成一个并把每个数据成员初始化成相应类型的默认值。
解:
- (a) 不正确。如果我们的类没有显式地定义构造函数,那么编译器就会为我们隐式地定义一个默认构造函数,并称之为合成的默认构造函数。
- (b) 不完全正确。为每个参数都提供了默认值的构造函数也是默认构造函数。
- (c) 不正确。哪怕没有意义的值也需要初始化。
- (d) 不正确。只有当一个类没有定义任何构造函数的时候,编译器才会生成一个默认构造函数。
# Exercise 7.47
说明接受一个 string
参数的 Sales_data
构造函数是否应该是 explicit
的,并解释这样做的优缺点。
解:
是否需要从 string
到 Sales_data
的转换依赖于我们对用户使用该转换的看法。在此例中,这种转换可能是对的。 null_book
中的 string
可能表示了一个不存在的 ISBN
编号。
优点:
可以抑制构造函数定义的隐式转换
缺点:
为了转换要显式地使用构造函数
# Exercise 7.48
假定 Sales_data
的构造函数不是 explicit
的,则下述定义将执行什么样的操作?
解:
string null_isbn("9-999-9999-9"); | |
Sales_data item1(null_isbn); | |
Sales_data item2("9-999-99999-9"); |
这些定义和是不是 explicit
的无关。
# Exercise 7.49
对于 combine
函数的三种不同声明,当我们调用 i.combine(s)
时分别发生什么情况?其中 i
是一个 Sales_data
,而 s
是一个 string
对象。
解:
(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 函数的本意就是要改变对象 |
# Exercise 7.50
确定在你的 Person
类中是否有一些构造函数应该是 explicit
的。
解:
explicit Person(std::istream &is){ read(is, *this); } |
# Exercise 7.51
vector
将其单参数的构造函数定义成 explicit
的,而 string
则不是,你觉得原因何在?
假如我们有一个这样的函数:
int getSize(const std::vector<int>&); |
如果 vector
没有将单参数构造函数定义成 explicit
的,我们就可以这样调用:
getSize(34); |
很明显这样调用会让人困惑,函数实际上会初始化一个拥有 34 个元素的 vecto
r 的临时量,然后返回 34。但是这样没有任何意义。而 string
则不同, string
的单参数构造函数的参数是 const char *
,因此凡是在需要用到 string
的地方都可以用 const char *
来代替(字面值就是 const char *
)。如:
void print(std::string); | |
print("hello world"); |
# Exercise 7.52
使用 2.6.1 节的 Sales_data
类,解释下面的初始化过程。如果存在问题,尝试修改它。
Sales_data item = {"987-0590353403", 25, 15.99}; |
解:
Sales_data
类不是聚合类,应该修改成如下:
struct Sales_data { | |
std::string bookNo; | |
unsigned units_sold; | |
double revenue; | |
}; |
# Exercise 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 | |
}; |
# Exercise 7.54
Debug
中以 set_
开头的成员应该被声明成 constexpr
吗?如果不,为什么?
解:
不能。 constexpr
函数必须包含一个返回语句。
# Exercise 7.55
7.5.5 节的 Data
类是字面值常量类吗?请解释原因。
解:
不是。因为 std::string
不是字面值类型。
# static Class Members
# 类的静态成员
- 非
static
数据成员存在于类类型的每个对象中。 static
数据成员独立于该类的任意对象而存在。- 每个
static
数据成员是与类关联的对象,并不与该类的对象相关联。 - 声明:
- 声明之前加上关键词
static
。
- 声明之前加上关键词
- 使用:
- 使用作用域运算符
::
直接访问静态成员:r = Account::rate();
- 也可以使用对象访问:
r = ac.rate();
- 使用作用域运算符
- 定义:
- 在类外部定义时不用加
static
。
- 在类外部定义时不用加
- 初始化:
- 通常不在类的内部初始化,而是在定义时进行初始化,如
double Account::interestRate = initRate();
- 如果一定要在类内部定义,则要求必须是字面值常量类型的
constexpr
。
- 通常不在类的内部初始化,而是在定义时进行初始化,如
# Exercise 7.56
什么是类的静态成员?它有何优点?静态成员与普通成员有何区别?
解:
与类本身相关,而不是与类的各个对象相关的成员是静态成员。静态成员能用于某些场景,而普通成员不能。
# Exercise 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 = 42.42; | |
static double initRate() { return todayRate; } | |
}; | |
double Account::interestRate = initRate(); |
# Exercise 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
应该是一个常量表达式。而类内只能初始化整型类型的静态常量,所以不能在类内初始化 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); |
# Chapter Summary
🍓:)