第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 swap
1
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。