第5
章:实现
条款26
:尽可能延后变量定义式的出现时间
- 应该延长变量的定义,直到非得使用它时
- 应该延后这份定义,直到能够给它初值实参
对于循环存在的情况:
做法
A
:定义于循环外1
2
3
4
5
6Widget w;
for(int i = 0; i < n; i++)
{
w = 取决于 i 的某个值;
...
}做法
B
:定义于循环内1
2
3
4
5for(int i = 0; i < n; i++)
{
Widget w(取决于 i 的某个值);
...
}这两种写法的成本如下:
做法
A
:1
个构造函数 +1
个析构函数 +n
个赋值操作做法
B
:n
个构造函数 +n
个析构函数做法
A
会造成名称w
的作用域比做法B
更大除非(1)你知道赋值成本比构造 + 析构成本低,(2)你正在处理代码中效率高度敏感的部分,否则你应该使用做法
B
。
条款27
:尽量少做转型操作
旧式C
转型:
T(expression)
(T)expression
新式C++
转型:
const_cast<T>(expression)
用来将对象的常量性(
const
)转除(只有它能办到)。dynamic_cast<T>(expression)
用来执行安全向下转型,也就是用来决定某对象是否归属继承体系中的某个类型。
reinterpret_cast<T>(expression)
用来执行低级转型,如将一个
pointer to in
t 转为一个int
。很少使用(在条款50
中使用过一次)。static_cast<T>(expression)
用来强迫隐式转换,例如将
non-const
对象转为const
对象,将int
转为double
,将void*
指针转为typed
指针,或将pointer to base
转为pointer to derived
。
任何一种类型转换(无论显式转换还是隐式转换)往往都会令编译器编译出运行期间执行的码。如将int
转为double
会产生一些代码,因为int
的底层表述不同于double
的:
1 | int x, y; |
再比如:
1 | class Base {...}; |
这种情况下有时候会有一个偏移量在运行期被施行于Derived*
身上,用来取得正确的Base*
指针值。这个例子表明单一对象可能有一个以上的地址(以Base*
指向它时的地址和以Derived*
指向它时的地址)。
假设我们有个base class Window
和一个derived class Specialwindow
,两者都定义了virtual
函数onResize
。进一步假设Specialwindow
的onResize
函数被要求首先调用Window
的onResize
。下面实际上是错的实现方式:
1 | class Window |
它调用的并不是当前对象上的函数,而是稍早转型动作所建立的一个“*this
对象之base class
成分”的暂时副本身上的onResize
!如果Window::onResize
修改了对象内容,当前对象其实没被改动,改动的是副本。然而SpecialWindow::onResize
内如果也修改对象,当前对象真的会被改动。这使当前对象进入一种“伤残”状态:其base class
成分的更改没有落实,而derived class
成分的更改倒是落实了。
正确的做法是:
1 | class Specialwindow: public Window |
dynamic_cast
的实现版本执行速度相当慢,应该在注重效率的代码中保持对dynamic_cast
的警觉- 优良的
C++
代码很少使用转型,我们应该尽可能隔离转型动作,通过将它隐藏在某个函数内,使得客户可以调用该函数而不需要将转型放进他们自己代码内 - 宁可使用新式转型,也不要使用旧式转型,前者很容易辨识出来
条款28
:避免返回handles
指向对象内部成分
handles
指指针、引用和迭代器- 成员变量的封装性最多只等于“返回其
reference
”的函数的访问级别 - 如果
const
成员函数不得不传出去一个reference
,则最好将返回类型限定为const
- 有可能会导致空悬的
handles
,它的生命期却长于其所指对象
条款29
:为“异常安全”而努力是值得的
当异常被抛出时,带有异常安全性的函数会:
- 不泄露任何资源
- 不允许数据败坏
较少的码就是较好的码,因为出错机会比较少,而且一旦有所改变,被误解的机会也少。异常安全码必须提供以下三个保证之一:
基本承诺
如果异常被抛出,程序内的任何事物仍然保证在有效状态(只要那是个合法状态)下,没有任何对象或数据结构会因此而败坏,所有对象处于一种内部前后一致的状态。
强烈保证
如果异常被抛出,程序状态不改变。
不抛掷保证
承诺绝不抛出异常,因为它们总能完成原先承诺的功能。
可以实现强烈保证的copy and swap
技术:为你打算修改的对象做出copy
一份副本,然后在副本身上做一切修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。带所有改变都成功后,再将修改后的副本和原对象swap
。
1 | // 之所以实现为 struct 是因为其 PrettyMenu 的数据封装性 |
注意,并非所有函数都可实现强烈保证或其具备现实意义。
函数提供的”异常安全保证“通常最高只等于其所调用各个函数提供的”异常安全保证“中的最弱者。
条款30
:透彻了解inlining
的里里外外
inline
函数是指将对此函数的每一个调用都以函数本体替换之。这将导致两种情况:
- 目标码增加(显然)
- 如果
inline
函数的体积很小,编译器对“函数本体”所产出的码可能比“函数调用”所产出的码小
inline
只是对编译器的一个申请,不是强制命令,也就是说编译器可以拒绝将太过复杂(带有递归或循环)的函数进行inline
。通过对函数使用inline
关键字属于明确提出申请,将函数定义于类内属于隐喻申请。
将大多数
inline
限制在小型、被频繁调用的函数身上inline
函数通常一定被置于头文件内因为大多数构建环境在编译过程中进行
inlining
,而为了将一个函数调用替换为被调用函数的本体,编译器必须知道那个函数长什么样子。所有对
virtual
函数的inline
申请都会被拒绝(因为对virtual
函数的调用在运行期才确定)
条款31
:将文件间的编译依存关系降至最低
将接口从实现中分离
1
2
3
4
5
6
7
8
9
10
11
12
13class Person
{
public:
Person(const string& name, const Date& birthday, const Address& addr);
string name() const;
string birthDate() const;
string address() const;
...
private:
string theName; // 实现细目
Date theBirthDate; // 实现细目
Address theAddress; // 实现细目
};这里的
class
无法通过编译,因为编译器没有取得其实现代码所用到的class string
,Date
,Address
的定义式,通常应该在Person
定义文件的最上方存在include
头文件。1
2
3不幸的是,这么一来便是在
Person
定义文件和其含入文件之间形成了一种编译依存关系。如果这些头文件中有任何一个被改变,或这些头文件所倚赖的其他头文件有任何改变,那么每一个含入Person class
的文件就得重新编译,任何使用person class
的文件也必须重新编译。这样的连串编译依存关系(cascading compilation dependencies
)会对许多项目造成难以形容的灾难。Handle classes
:pimpl idiom
(pointer to implenmentation
) + 前向声明把
Person
分割成两个class
,一个只提供接口,另一个负责实现接口。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 使用前向声明而不是包含头文件, 这个很关键
class PersonImpl; // Person 实现类
// Person 接口用到的 class
class Date;
class Address;
class Person
{
public:
Person(const string& name, const Date& birthday, const Address& addr);
string name() const;
string birthDate() const;
string address() const;
...
private:
shared_ptr<PersonImpl> pImpl; // 指针, 指向实现物
// 如果不这样做的话, 编译器在编译 Person 类的时候,必须知道 Person 类的大小
// 就不得不包含这三个实现细目
/*
string theName; // 实现细目
Date theBirthDate; // 实现细目
Address theAddress; // 实现细目
*/
// 然而一旦这样写, 前面的前向声明就没用了, 必须包含头文件
};这样一来,
Person
类的使用者就完全于Date
,Address
以及Person
的实现细目相分离了。这个分离的关键在于以“声明的依存性”替换“定义的依存性”,那正是编译依存性最小化的本质:实现上让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。几个简单的设计策略:
如果使用
object reference
或object pointer
可以完成任务,就不要使用object
你可以只靠一个类型声明式就定义出指向该类型的
reference
和pointer
;但如果定义某类型的object
,就需要用到该类型的定义式。如果能够,尽量以
class
声明式替换class
定义式注意,当你声明一个函数而它用到某个
class
时,你并不需要该class
的定义。为声明式和定义式提供不同的头文件
对于前面代码中的前向声明,最好是提供一个声明式头文件,包含全部的前向声明。
下面式
Person.cpp
文件的部分实现:1
2
3
4
5
6
7
8
9
10
// 注意,Person 和PersonImpl的成员函数完全相同, 两者接口完全相同
Person::Person(const string& name,
const Date& birthday,
const Address& addr) : pImpl(new PersonImpl(name, birthday,addr)) {}
string Person::narne( ) const {return pImpl->name();}
...
Interface classes
:abstract base class
+factory
函数abstract base class
通常没有成员变量,也没有构造函数,只有一个virtual
析构函数和一组pure virtual
函数。但Interface class
的客户必须有办法为这种class
创建新对象。他们通常调用一个特殊函数,此函数创建实际实现的derived class
,这样的函数通常称为factory
(工厂)函数(见条款13
),它们返回智能指针指向动态分配所得对象,而该对象支持Interface class
的接口。这样的函数又往往在Interface class
内被声明为static
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44// 位于 Person.h
// 抽象基类, 只有声明
// 提供给客户使用的头文件
class Person
{
public:
virtual ~Person();
virtual strng name() const = 0;
virtual strng birthDate() const = 0;
virtual string address() const = 0;
// static 的 factory 函数
static shared_ptr<Person> create(const string& name,
const Date& birthday,
const Address& addr);
...
}
// 位于 Person.cpp
class RealPerson: public Person
{
public:
RealPerson(const string& name,
const Date& birthday,
const Address& addr) : theName(name), theBirthDate(birthday), theAddress(addr) {}
virtual ~RealPerson() {}
string name() const;
string birthDate() const;
string address() const;
private:
string theName;
Date theBirthDate;
Address theAddress;
};
... // 虚函数的实现码
shared_ptr<Person> Person::create(const string& name,
const Date& birthday,
const Address& addr)
{
return shared_ptr<Person>(new RealPerson(name, birthday, addr));
}
Handle classes
和Interface classes
解除了接口和实现之间的耦合关系,从而降低了文件间的编译依存性。