第4章:设计与声明
条款18:让接口容易被正确使用,不容易被误用
- 如果客户企图使用某个接口却没有获得所预期的行为,这个代码不应该通过编译;如果代码通过了编译,则它的行为就应该是客户想要的
- 欲达“让接口容易被正确使用,不容易被误用”的目的,必须考虑客户调用接口时可能做出什么样的错误
- “促进正确使用”的办法包括接口的一致性,以及与内置类型兼容
- “阻止误用”的办法包括建立新类型、限制在类型上的操作,束缚对象值以及消除客户的资源管理责任
条款19:设计class犹如设计type
你应该带着“语言设计者当初设计语言内置类型时”一样的谨慎来研讨每一个class的设计。因为,重载函数和操作符、控制内存的分配和归还、定义对象的初始化和终结…全都在你手上。
新的
type的对象应该如何创建和销毁?设计函数为类的构造函数和析构函数以及内存分配函数和释放函数(见条款
49-52)。对象的初始化和对象的赋值该有什么样的差别?
决定了构造函数和赋值操作符的行为以及差异。
新
type的对象被passed by value,意味着什么?取决于
copy构造函数的实现。什么新
type的合法值?你的新
type需要配合某个继承图系吗?见条款
7、34、36。你的新
type需要什么样的转换?explicit和operator关键字的使用。见条款15。什么样的操作符和函数对此新
type而言时合理的?见条款
23、24、46。什么样的标准函数应该驳回?
见条款
6。谁该取用新
type的成员?决定了数据成员是
public、private还是protected,以及friend关键的使用。什么是新
type的“未声明接口”?见条款
29。你的新
type有多么一般化?决定
class template的使用。你真的需要一个新
type吗?
条款20:宁以pass by reference to const替换pass by value
- 当把具有继承关系的类作为参数传递时,如果
pass by value可能会出现“截断”问题。
条款21:必须返回对象时,别妄想返回其reference
- 绝不要返回指向一个
local stack对象的pointer或reference - 绝不要返回指向一个
heap allocated对象的reference - 除非有单例模式的设计要求,否则绝对不要返回指向一个
local static对象的pointer或reference
条款22:将成员变量声明为private
客户访问数据的一致性
public里都是函数。可以对成员变量有更精确的控制
可以实现成员变量的不可访问,只读、只写、读写访问。
封装性
将成员变量隐藏在函数接口背后,可以为所有可能的实现提供弹性。
从封装的角度看,只有两种权限:
private(提供封装)和其它protected并不比private更具封装性。
条款23:宁以non-member、non-friend替换member函数
考虑一个用来表示网页浏览器的class,这个class提供的众多成员函数中,有用来清除下载元素高速缓冲区的,有用来清除访问过的历史记录的,有用来移除系统中所有cookies的。
1 | class WebBrowser |
后者比较好。因为:
增加封装性
non-member non-friend将提供较大的封装性,因为它并不增加能够访问class内的private成分的函数数量。friend函数对class private成员的访问权力和member函数相同,两者对封装的冲击力度是一样的。增加扩充机能性
在
C++中,正确且自然的做法是:1
2
3
4
5
6namespace WebBrowserStuff
{
class WebBrowser {...};
void clearBrowser(WebBrowser& wb); // 为 WebBrowser “提供便利”的函数
...
}namespace和class的不同是,前者可跨越多个源码文件而后者不能。像
WebBrowser这样的类可能有大量的“提供便利”函数,某些与书签有关,与打印有关,与cookie有关… 分离它们最直接的做法就是:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 头文件 WebBrowser.h 内
namespace WebBrowserStuff
{
class WebBrowser {...};
... // 核心机能, 例如所有客户都想要的 non-member 函数
}
// 头文件 WebBrowserBookmarks.h 内
namespace WebBrowserStuff
{
... // 与书签相关的 non-member 函数
}
// 头文件 WebBrowserCookies.h 内
namespace WebBrowserStuff
{
... // 与 cookie 相关的 non-member 函数
}将所有“便利函数”放在多个头文件内但隶属于同一个命名空间,意味客户可以轻松扩展这一组“便利函数”。
条款24:若所有参数皆需类型转换,请为此采用non-member函数
有理数类
Rational的实现重点关注用来将两个有理数相乘的
operator*操作符重载函数的实现方式。
条款25:考虑写出一个不抛异常的swap函数
标准程序库提供的swap算法的典型实现为:
1 | namespace std |
只要T类型支持copying函数(copy构造函数和copy assignment操作符)。对于用户自定义类型,效率低下(需要三次复制)。
现在考虑所谓的pimpl(pointer to implementation)实现手法:
1 | // 注意这是个模板类 |
一旦需要置换两个Widget对象值,我们实际唯一需要的是置换两个指针即可。但缺省的swap函数不知道这一点!
下面是正确的实现步骤:
令
Widget实现一个名为swap的public成员函数做真正的置换工作1
2
3
4
5
6
7
8
9
10
11
12template<typename T>
class Widget
{
public:
...
void swap(Widget<T>& other)
{
using std::swap; // 必须的
swap(pImpl, other.pImpl); // 直接置换指针即可
}
...
}将
std::swap特化,令它调用该成员函数(当Widget是类时。这里的例子中,Widget是个模板类则不要这一步)因为,
C++只允许对class template偏特化,不允许对function template进行偏特化1
2
3
4
5
6
7
8namespace std
{
template<>
void swap<Widget>(Widget& a, Widget& b)
{
a.swap();
}
}
声明一个
non-member swap,令它调用member swap1
2
3
4
5
6// 注意, 要将它放在和 Widget<T> 同一个空间内(全局或者自定义的 namespace)
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}编程时,在调用
swap置换对象的语句之前加上using std::swap声明1
2
3
4
5
6
7
8
9// 客户代码
template<typename T>
void doSomething(T& obj1. T& obj2)
{
using std::swap; // 令 std::swap 在此函数内可用
...
swap(obj1, obj2); // 这样一来, 编译器将为 T 类型对象调用最佳版本 swap
...
}编译器首先在全局作用域或
T所在命名空间内寻找T(在这里,T就是例子中的Widget哦)专属的swap(也就是步骤3中实现的)。如果没有实现这些,则调用std内的swap,如果步骤2还实现了特化版本,将会选中特化版本。
劝告,
member swap绝不可抛出异常具体参考条款29。