导读
Introduction
1.
所谓
declaration
,是用来将一个
object
、
function
、
class
或
template
的类型告诉编译器,它不带细节信息。所谓
definition
,是用来将细节信息提供给编译器。
对
object
而言,其定义式是编译器为它配置内存的地点;对
function
或
function template
而言,其定义式提供函数本体
function body
;
对
class
或
class template
而言,其定义式必须列出该
class
或
template
的所有
members
。
2.
所谓
default constructor
是
指不需任何参数就可被调用的constructor,不是没有任何参数,就是每个参数都有默认值。通常在定义对象数组时,就会需要一个default constructor。如果该类没有提供default constructor,通常的做法是定义一个指针数组,然后利用new将每个指针一一初始化;在该方法无效的情况下,可以使用placement new方法。
3.
所谓
copy constructor
是
以某对象作为另一同种类型对象的初值,或许它最重要的用途就是用来定义何谓以
by value
方式传递和返回对象。事实上,只要编译器决定产生中介的临时性对象,就会需要一些
copy constructor
调用
动作,重点是:
pass-by-value
便意味着调用“
copy constructor
”
。
4.
初始化
initialization
行为发生在对象初次获得一个值的时候。对于带有
constructors
的
classes
或
structs
,初始化总是经由调用某个
constructor
达成。对象的
assignment
动作发生于已初始化的对象被赋予新值的时候。纯粹从操作观点看,
initialization
和
assignment
之间的差异在于前者由
constructor
执行,后者由
operator =
执行。C++ 严格区分此二者,原因是上述两个函数所考虑的事情不同:
constructors
通常必须检验其参数的有效性,而大部份
assignment
运算符不必如此,因为其参数已经构造完成,必然是合法的。另一方面,
assignment
动作的目标对象并非是尚未建构完成的对象,而是可能已经拥有配置得来的资源。在新资源可被赋值过去之前,旧资源通常必须先行释放。
5.
C++
的两个新特征:
bool
类型:其值不是true就是false,语言内建的关系运算符、条件判断式的返回类型都是bool。若编译器尚未实现该类型,有两种选择:
enum bool { false, true };
|
b
ool
与int是不同类型,允许bool与int间的函数重载,但内建关系运算符依然返回int;
|
typedef int bool;
const bool false = 0;
const bool true = 1;
|
bool
与int成为同种类型,兼容于传统的C/C++语意,移植到支持bool的平台上后行为不变,但不允许int与bool间的函数重载;
|
四个转型运算符:static_cast、const_cast、dynamic_cast、reinterpret_cast。它们更容易在程序代码中被识别出来,编译器更容易诊断出错误的运用。
2002-6-23
改变旧有的
C
习惯
Shifting from C to C++
C
基本上只是
C++
的一个子集,其许多技巧在
C++
中已经不合时宜。例如以
reference to pointer
取代
pointer to pinter
。某些
C
习惯用法与
C++
的精神相互矛盾。
条款
1
:尽量以
const
和
liline
取代
#define
(以
compiler
取代
preprocessor
)
理由
1
:
#define
定义的常量名称可能在编译之前就被
preprocessor
移走,因此不会出现于
symbol table
中,从而就没有机会被编译器看见。这样的结果是会给
debug
工作带来不便。不如改用
const
定义常量。
理由
2
:
#define
实现的带有实参的宏,虽然不必付出函数调用所需的成本,但用户使用时极易出错。不如使用
inline function
。
注意
1
.常量指针的定义,如:
const char * const authorName = “Scott Meyers”
。
注意
2
.
class
专属常量,即一个
const static member
,要注意在
implementation
文件中定义它。
注意
3
.不能完全舍弃
preprocessor
,因为
#include
、
#ifdef
、
#ifudef
在编译控制过程中还扮演着重要角色。
条款
2
:尽量以
<iostream>
取代
<stdio.h>
尽管
scanf
和
printf
可移植而且高效率,但是它们家族都还不够完美。尤其是它们都不具备
type-safe
性质,也都不可扩充。而
type safety
和
extensibility
正是
C++
的基石组成部分。再者,
scanf
和
printf
函数家族将变量与控制读写的格式化信息分离开来,读写形式不够简单统一。
注意
1
.
有些
iostream
的实现效率不如相应的
C stream
,
故不同选择可能会给程序带来很大的不同。这只是对一些特殊的实现而言;
注意
2
.在标准化的过程中,
iostream
库在底层做了很多修改,所以对那些要求最大可移植性的应用程序来说,会发现不同版本遵循标准的程度也不同。
注意
3
.
iostream
库的类有构造函数而
<stdio.h>
里的函数没有,在某些涉及到静态对象初始化顺序的时候,如果可以确认不会带来隐患,用标准
C
库会更简单实用。
条款
3
:
尽量用
new
和
delete
而不用
malloc
和
free
malloc
和
free
对
constructors
和
destructors
一无所知
,
由此引发的问题将是对象的初始化难度以及内存泄漏。将
malloc/free
和
new/delete
混合使用其结果未定义
,会带来很多麻烦。
条款
4
:尽量使用
c++
风格的注释形式
理由:
C
的多行注释
/*…*/
不支持嵌套使用。
内存管理
Memory Management
对
C++
程序员而言,把事情作对,意味着正确使用
new
和
delete
。而要让它们更有效率则意味着定制自己的
operator new
和
operator delete
。
条款
5
:使用相同形式的
new
和
delete
使用
new
时会有两件事情发生:内存通过
operator new
被配置,然后一个(或)多个
constructor(s)
针对此内存被调用。使用
delete
时也会有两件事情发生:一个(或)多个的
destructor(s)
针对此内存被调用,然后内存通过
operator delete
被释放。
delete
的最大问题在于不知道即将释放的内存内究竟存在多少对象,必须由程序员指出。故
delete
是否使用
[ ]
一定要与
new
是否使用
[ ]
保持一致,否则结果未定义,最大的可能是
memory leak
。
1
.含有
pointer data member
并供应多个
constructors
的
class
,应在
constructors
中使用相同形式的
new(
包括不使用
new)
将
pointer member
初始化。否则,在
destructor
中就不知道该使用什么形式的
delete
。
2
.最好不要对数组类型使用
typedef
动作!不然,要加倍小心以保证
delete
与
new
的使用形式相同。
条款
6
:记得在
destructor
中以
delete
对付
pointer members
1
.在
class
中每加上一个
pointer member
时,几乎总要相应做以下每件事:
n
在每个
constructors
中将该指针初始化。若没有任何一个
constructor
会为该指针分配内存,那么该指针应初始化为
0
。
n
在
assignment
运算符中将该指针原有的内存删除,重新配置一块。
n
在
destructor
中
delete
这个指针。
2
.
delete
一个
null pointer
是安全的,什么也不做。
delete
一个指向合法内存的
pointer
后,该
pointer
并不为
null pointer
,再次
delete
该
pointer
将是非法操作。
3
.不要以
delete
来对付一个未曾以
new
完成初始化的
pointer
。除了
smart pointer objects
之外,几乎绝对不要
delete
一个传递而来的指针。
条款
7
:为内存不足的状况作准备
1.
operator new
无法配置出所需内存时将丢出一个
std::bad_alloc exception
。使用
”nothrow objects”
形式的
new
则可以像老的编译器一样,直接返回
0
。
2.
operator new
在无法满足需求而丢出
exception
前会先调用
client
专属的错误处理函数,该函数通常称为
new-handler
。
operator new
会
不断重复
调用
new-handler
函数,直至找到足够的内存为止。
client
必须调用
set_new_handler
来指定这个
new-handler
。在
<new>
中大致定义如下:
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw(); //
返回值指向上次指定的
new_handler
可以利用
new-handler
实现一个简单的错误处理策略。
3.
一个设计良好的
new-handler
函数必须实现下面功能中的一种:
n
产生更多的可用内存。一个方法是:程序启动时分配大的内存块,在第一次调用
new-handler
时释放。释放时伴随着一些对用户的警告信息,如内存数量太少,下次请求可能会失败,除非又有更多的可用空间。
n
安装另一个不同的
new-handler
函数。如果当前的
new-handler
函数无法产生更多可用内存,它知道另一个
new-handler
可以提供更多的资源,这样它就可以安装另一个
new-handler
来取代它。另一个变通策略是让
new-handler
可以改变自己的运行行为,使得下次调用时可以做不同的事,方法是使
new-handler
可以修改那些影响它自身行为的
static
或
global
数据。
n
卸除
new-handler
。也就是传递
null
指针给
set_new_handler
。没有安装
new-handler
,
operator new
分配内存不成功时就会抛出一个标准的
std::bad_alloc
类型的异常。
n
抛出
std::bad_alloc
或从
std::bad_alloc
继承的其他类型的
exception
。这样的
exceptions
不会被
operator new
捕捉,所以它们会被送到最初提出内存请求的地方。抛出别的不同类型的
exception
会违反
operator new
的异常规范。规范中的缺省行为是调用
abort
,所以
new-handler
要抛出一个
exception
时,一定要确信它是从
std::bad_alloc
继承来的。
n
不返回,直接调用
abort
或
exit
。
abort
、
exit
可以在标准
c
库和
c++
库中找到。
4
.
要想为
class X
加上“
set_new_handler
支持能力
”
,只需令
X
继承自
NewHandlerSupport class template
。
template<class T> // "mixin-style" base class for class-specific
class NewHandlerSupport {
public: // set_new_handler support
static new_handler set_new_handler(new_handler p);
static void * operator new(size_t size);
private:
static new_handler currentHandler;
};
template<class T>
new_handler NewHandlerSupport<T>::set_new_handler(new_handler p)
{
new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
template<class T>
void * NewHandlerSupport<T>::operator new(size_t size)
{
new_handler globalHandler = std::set_new_handler(currentHandler);
void *memory;
try {
memory = ::operator new(size);
}
catch (std::bad_alloc&) {
std::set_new_handler(globalHandler);
throw;
}
std::set_new_handler(globalHandler);
return memory;
}
// this sets each currentHandler to 0
template<class T>
new_handler NewHandlerSupport<T>::currentHandler;
条款
8
:撰写
operator new
和
operator delete
时应遵循的公约
撰写自己的内存管理函数时,其行为一定要与缺省行为保持一致。撰写
o
perator new
公约:正确的返回值,内存不足时调用错误处理函数
new_handler
,处理好
0
字节内存请求,避免隐藏标准形式的
new
。撰写
o
perator delete
公约:保证删除一个
null
指针永远是安全的。
1.
一个
non-member operator new
的伪码:
void * operator new(size_t size) // your operator new might
{ // take additional params
if (size == 0) { // handle 0-byte requests
size = 1; // by treating them as
} // 1-byte requests
while (1) {
attempt to allocate
size
bytes;
if (the allocation was successful)
return (a pointer to the memory);
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
if (globalHandler) (*globalHandler)();
else throw std::bad_alloc();
}
}
2.
一个
non-member operator delete
的伪码
:
void operator delete(void *rawMemory)
{
if (rawMemory == 0) return; // do nothing if the null pointer is being deleted
deallocate the memory pointed to by
rawMemory;
return;
}
3.
operator new
、
operator delete
可被
subclasses
继承
,
有可能一个
base class
的
operator new
或
operator delete
会被用来配置或释放其
derived class object
,
最好的办法是把错误数量的内存管理请求转给标准
operator new
或
operator delete
来处理
,
像下面这样
:
class Base { // same as before, but now
public: // op. delete is declared
static void * operator new(size_t size);
static void operator delete(void *rawMemory, size_t size);
...
};
void * Base::operator new(size_t size)
{
if (size != sizeof(Base)) // if size is "wrong,"
return ::operator new(size); // have standard operator new handle the request
... // otherwise handle the request here
}
void Base::operator delete(void *rawMemory, size_t size)
{
if (rawMemory == 0) return; // check for null pointer
if (size != sizeof(Base)) { // if size is "wrong,"
::operator delete(rawMemory); // have standard operator
return; // delete handle the request
}
deallocate the memory pointed to by rawMemory;
return;
}
4.
撰写
member function operator new[ ]
唯一要记住的是配置生鲜内存
raw memory
,因为不知道数组中每个对象元素的大小,因此不可对数组尚未存在的对象做任何其它动作。
条款
9
:
避免遮掩了标准形式的
new
问题:内部范围声明的名称会隐藏掉外部范围的相同的名称,所以对于分别在类的内部和全局声明的两个相同名字的函数来说,类的成员函数会隐藏掉全局函数:
class X {
public:
void f();
// operator new allowing specification of a new-handling function
static void * operator new(size_t size, new_handler p);
};
void specialErrorHandler(); // definition is elsewhere
X *px1 = new (specialErrorHandler) X; // calls X::operator new
X *px2 = new X; // error!
方案
1
:
写一个
class
专属的
operator new
,令
它和标准
new
有相同的调用方式。可以用一个高效的
inline
函数封装实现:
class x {
public:
void f();
static void * operator new(size_t size, new_handler p);
static void * operator new(size_t size)
{ return ::operator new(size); }
};
方案
2
:
为
operator new
的每一个额外参数提供缺省值:
class X {
public:
void f();
static void * operator new(size_t size, new_handler p = 0);
};
条款10
:
如果写了
operator new
,
请对应写
operator delete
1.
自己撰写operator new和operator delete
,
通常是为了效率。因为缺省版的operator new是一种通用内存分配器,它须能分配任意大小的内存块,同样,operator delete也要可以释放任意大小的内存块。operator delete想知道释放的内存块有多大,就必须知道当初operator new分配的内存块有多大。一种常用的方法,就是在operator new返回的内存里预先附带一些额外信息,用来指明被分配的内存块的大小。
2.
以下是定制
operator new
和
operator delete
的一段源代码:
class Airplane { // modified class
—
now supports
public: // custom memory management
static void * operator new(size_t size);
static void operator delete(void *deadObject, size_t size);
...
private:
union {
AirplaneRep *rep; // for objects in use
Airplane *next; // for objects on free list
};
static const int BLOCK_SIZE;
static Airplane *headOfFreeList;
};
Airplane *Airplane::headOfFreeList;
const int Airplane::BLOCK_SIZE = 512;
void * Airplane::operator new(size_t size)
{
if (size != sizeof(Airplane))
return ::operator new(size);
Airplane *p = headOfFreeList;
if (p)
headOfFreeList = p->next;
else {
Airplane *newBlock =
static_cast<Airplane*>(::operator new(BLOCK_SIZE *sizeof(Airplane)));
for (int i = 1; i < BLOCK_SIZE-1; ++i)
newBlock[i].next = &newBlock[i+1];
newBlock[BLOCK_SIZE-1].next = 0;
p = newBlock;
headOfFreeList = &newBlock[1];
}
return p;
}
void Airplane::operator delete(void *deadObject, size_t size)
{
if (deadObject == 0) return;
if (size != sizeof(Airplane)){
::operator delete(deadObject);
return;
}
Airplane *carcass = static_cast<Airplane*>(deadObject);
carcass->next = headOfFreeList;
headOfFreeList = carcass;
}
3.
以上代码体现了内存池的概念,并非memory leak。
4.
如果要删除的对象是从一个没有虚析构函数的类继承而来的
,
那传给operator delete的size_t值可能不正确。这就是必须保证基类必须要有虚析构函数的原因之一。
构造函数、析构函数和
Assignment
运算符
条款
11:
为需要动态分配内存的
class
声明一个
copy constructor
和一个
assignment
运算符
1.
只要程序中有pass-by-value的动作,就会调用copy constructor。实际不是这样,关键在编译器。
2.
由memberwise assignment动作产生的指针别名pointer aliasing问题,可能导致 memory leak以及指向同一块内存的指针被delete多次
<
该结果未定义>
。
3.
针对别名问题的解决之道:如果class拥有任何指针,撰写自己的copy constructor和assignment operator,在这些函数中,可以将指针所指之数据结构复制一份,使每个对象拥有属于自己的一份拷贝;也可以实现某种reference-counting策略,追踪记录指向某个数据结构的对象个数。
条款
12:
在
constructor
中尽量以
initialization
取代
assignment
1.
const members
和reference members不能够被赋值,只能够被初始化,必须使用initialization.
2.
对象的构造分两个阶段:初始化data members和执行constructor。对于自定义类型member data,使用member initialization list成本只有一次constructor函数调用,而在constructor中使用operator = 会先调用default constructor,然后再调用assignment运算符。
3.
内建类型的non-const和non-reference对象,initialization和assignment并无操作上的差异。
4.
static class member
绝不应该在class的constructor中被初始化。
条款
13:
初始化列表中成员列出的顺序和它们在类中声明的顺序相同
1.
nonstatic data members
在
class
内的初始化次序与它们的声明次序相同,而与它们在
member initialization list
中出现的次序完全无关;而它们的
destructors
则总是以相反的次序被调用。
2.
base class data members
永远在
derived class data members
之前初始化。如果使用多重继承,
base classes
将以被继承的次序来初始化,同样与在
member initialization list
中出现的次序无关。
条款
14:
确定基类有虚析构函数
1.
当经由一个
base class pointer
删除一个
derived class object
,而此
base class
有一个
nonvirtual destructor
时,其结果未定义,最可能的情况是在执行期未调用
derived class
的
destructor
。
2.
若
class
未含任何虚拟函数,往往意味着它并不是要被当作
base class
使用,此时令其
destructor
为
virtual
通常只是浪费空间
——
容纳不必要的
vptr
以及
vtbl
。
3.
有时希望将一个
class
定义为抽象类,但却没有适当的函数可选为
pure virtual function
,此时可声明一个
pure virtual destructor
。不过,必须为此
destructor
提供定义式!因为
virtual destructor
的运作方式是派生程度最深的
class
,其
destructor
最先被调用,然后是
base class
的
destructor
被调用。这意味着编译器会对该抽象类的
destructor
产生一个调用操作。
4.
inline
函数并不是一个独立存在的函数,如果将一个
virtual destructor
声明为
inline
,虽可以避免函数调用所需的额外负担,但编译器将必须在某个地方产生一个
out-of-line
函数副本。
<vtbl
中相关项正是指向该副本
>
条款
15:
让
operator =
返回
*this
的引用
理由:如果不这样做,就会妨碍
assignments
串链,或是妨碍隐式类型转换,或者兼而有之。
定义:
classname & classname::operator = (const classname & rhs){……return *this;}
条款
16:
在
operator =
中对所有
data members
赋值
1.
class
加入新的
data member
时,
assignment
运算符应同步修改!
2.
derived class
的
assignment
运算符有义务处理其
base class
的
assignment
动作!两种途径:
Base::operator = (rhs); //
如果
assignment
运算符由编译器生成,有些编译器可能不支持;
static_cast<Base&>(*this) = rhs; //
是指向
Base object
的
reference
,不是
Base object
本身!
3.
实现
devived class
的
copy constructors
时一定要确保调用
base class
的
copy constructor
而不是
default constructor
!这只需在
devived class
的成员初始化列表中为
base class
指定初值为
rhs
即可。
条款
17:
在
operator =
中检查是否“自己给自己赋值”
理由:效率、确保正确性。
1.
assignment
运算符在为其左侧对象配置新资源时,通常必须先释放该对象已有的资源。如果不作该项检查,极有可能使用已被自己强行释放的资源。
2.
对象等同
object identity
的判定:两个对象是否有相同的值或相同的地址。
3.
别名
aliasing
问题和对象等同
object identity
问题特别容易发生在
reference
和
pointer
身上。
4.
任何时候,在可能出现别名问题的函数中,必须将该可能性纳入考虑!
类与函数之设计与声明
设计良好的
class
,是一种挑战。关键在于,使得自定义的
class
所导出的类型和语言内建的类型并无区别,这才是优秀的设计。
条款
18:
努力使接口神具而型微
1.
所谓“神具而型微”,是指通过该接口用户可以做他们合理要求的任何事情,但是尽可能让函数个数少,不会任何重复功能的
member functions
。
2.
接口函数愈多,客户愈不容易了解它;大型
class
接口的可维护性较差;长长的
class
定义,会导致长长的头文件,进一步导致长长的编译时间,从而影响整个项目的开发。
3.
对所有实用目的而言,
friend
函数是
class
接口的一部分。这意味着
friend
函数也应该被纳入
class
接口的神具而型微考虑。
条款
19:
分清成员函数,非成员函数和友元函数
1.
区分成员函数,非成员函数和友元函数的规则:
虚拟函数必须是成员函数;
operator >>
和
operator <<
必须是非成员函数;
需要对最左端的参数进行类型转换的函数必须是非成员函数;
非成员函数如需访问
class
的非公有成员,必须成为
class
的友元函数;
其它情况下都声明为成员函数。
2.
比较先进的编译器会在必要的时刻为每个函数的每一个参数身上执行隐式类型转换
implicit type conversion
,但
explicit
constructors
不能作为隐式类型转换使用。
3.
尽量避免
friend
函数。
条款
20:
避免
public
接口出现数据成员
理由:一致性、精确的存取控制、函数抽象性。
条款
21:
尽可能使用
const
1.
必须知道
:
char *p = "Hello"; // non-const pointer,non-const data
const char *p = "Hello"; // non-const pointer,const data
char * const p = "Hello"; // const pointer, non-const data
const char * const p = "Hello"; // const pointer, const data
2.
const
最具威力的用途是作用于函数声明之上,它可以用来修饰函数的返回值、个别参数,以及整个成员函数。
c
onst member function
的目的是为了指明仅可由
const
对象调用。
3.
C++
的一个重要性质
:
member functions
即使是常量性有所不同
,
也可以重载。
4.
mutable
关键词施行于
nonstatic const data member
之上
,
可以有效解放其
bitwise constness
方面的束缚。
条款
22:
尽量用
“
传引用
”
而不用
“
传值
”
1.
C
语言中的每样东西都是
passed by value
。
C++
把
pass-by-value
当作缺省行为
,
除非另行指定
,
否则函数参数都是以实参的副本作为初值
,
而调用端所获得的亦是函数传回值的一个副本。
2.
对象以
by value
的方式传递,其实际意义是由该对象的
copy constructor
决定的,这可能会使
pass-by-value
成为代价很高的动作。同时,对象的
destructor
也必然会被调用。以
by reference
的方式传递参数,不会有任何的
constructor
或
destructor
会被调用,因为没有必要产生任何新对象。它的另一个好处是可避免所谓的“切割
slicing
问题”。
3.
Passing by reference
是一件美妙的事情,但会导致某种复杂性。最知名的问题就是别名
aliasing
问题。
4.
reference
的底层几乎都是以指针完成,因此
passing by reference
通常意味着传递的是指针。对于小尺寸对象,传值可能比传引用的效率更高一些。
条款
23:
当你必须返回一个
object
时,不要试图返回一个
reference
所谓
reference
只是一个符号、一个名称,一个既有对象的名称。任何时候看到一个
reference
,都应该立即问自己,它的另一个名称是什么,那个“既有对象”是否存在,如果不存在,请不要试图返回
reference
。
条款
24:
在函数重载和参数缺省化之间谨慎抉择
一般而言,如果有合理的缺省值,而且只需要一个算法,那么最好是选择参数缺省化,否则使用函数重载。
条款
25:
避免对指针类型和数值类型进行重载
理由
:
C
++
世界中有一种独特的情况,人们认为某个调用动作应该被视为模棱两可,可编译器却不这么认为,因为编译器必须作出抉择。
条款
26:
当心潜在的模棱两可
ambiguity
状态
有很多可能及各种情况,在程序和程序库中隐藏着模棱两可状态。一个好的软件开发这应该时刻保持警惕,将它的出现几率降至最低。
例
1
:
class B; // forward declaration for class B
class A {
public:
A(const B&); // an A can be constructed from a B
};
class B {
public:
operator A() const; // a B can be converted to an A
};
void f(const A&);
B b;
f(b); // error!
—
ambiguous
例
2
:
void f(int);
void f(char);
double d = 6.02;
f(d); // error!
—
ambiguous
f(static_cast<int>(d)); // fine, calls f(int)
f(static_cast<char>(d)); // fine, calls f(char)
例
3
:
class Base1 {
public:
int doIt();
};
class Base2 {
存取限制不能解除“因多继承而来的成员”的模棱两可状态!因此,即使此处
doIt
声明为
private
也不能改变模棱两可的事实。
|
public:
void doIt();
};
class Derived: public Base1, // Derived doesn't declare
public Base2 { // a function called doIt
...
};
Derived d;
d.doIt(); // error!
—
ambiguous
d.Base1::doIt(); // fine, calls Base1::doIt
d.Base2::doIt(); // fine, calls Base2::doIt
条款
27:
如果不想使用编译器暗自产生的
member functions
,
就显式拒绝它
将相应的member functions声明为 private而且不要定义它们。
条款
28:
尝试切割全局名字空间
global namespace
client
可以通过三种方式来访问名字空间里的符号:将名字空间中的所有符号全部引入某一用户空间
scope
;将个别符号引入某一用户空间;每次通过修饰符显式地使用某个符号。
类与函数之实现
条款
29
:避免传回内部数据的
handles
1.
不允许通过一个
const
对象直接或间接调用一个
non-const member function
。
2.
问题常常发生在返回一个指向内部
data
的
pointer
或者
reference
的
const member function
身上,
client
可以通过这个暴露的
handle
来修改一个
const
对象的内部资料。这实质上违反了抽象性
abstraction
。解决问题的方法是要么让
member function
成为
non-const
,要么就不让它传回任何
handle
。
3.
non-const member function
传回
handles
也会导致麻烦,特别是返回暂时对象时。要知道:暂时对象的生命短暂,只能维持到“调用函数“之表达式结束时。此时,很可能出现
dangling handles
现象,因为该
handles
所指向的对象因暂时对象的死亡而消逝!
2002-6-23
条款
30
:避免成员函数返回这样的
non-const pointer
或
reference
,它们指向比该函数的存取级别还要低的
members
1.
目的是拒绝让这些
members
的存取级别获得晋升从而让
clients
随意访问它们而不受存取级别的限制。
2.
特别注意:面对指针,要担心的不只是
data members
,还有
member functions
,因为函数有可能返回一个
pointer to member function
,而访问函数指针是没有存取限制的!
3.
如果一定要返回指向较低存取级别
members
的
pointer
或
reference
,请让它们成为
const
。
条款
31
:千万不要返回函数内局部对象或
new
得指针所指对象的
reference
1.
函数返回时,控制权离开函数,函数内部的局部对象会自动析构。
2.
如果函数返回
new
得指针的所指对象,
client
必须在使用后
delete
通过
operator &
获得的对象指针。然而,有时这种指针是无法获得的,例如在一个链式表达式中,而且也没有理由强制
client
这样做。
3.
返回一个指向
local static
对象的
reference
同样无法正确运作。如果
someFunc
是这样的函数,那么
someFun(a) == someFun(b)
恒为真!
条款
32
:尽量延缓变量定义式的出现
1.
不止应该延缓变量的定义,直到非得使用该变量为止,而且应该直到能给予它一个初值为止。
2.
理由:可以改善效率、增加清晰度,还可以降低变量的说明需求。
2002-6-24
条款
33
:明智地运用
inlining
1.
不仅仅是免除函数调用成本,编译器最佳化机制通常用来浓缩那些不含函数调用动作的程序代码,所以当你
inlining
一个函数时,或许编译器就有能力在函数本体身上执行某种最佳化。
2.
inline
函数背后的整个观念是,将对此函数的调用动作以函数代码来取代,因此也导致了目标代码的增大。
inline
函数的定义几乎总是放在头文件中,这允许多个编译单元
translation units
得以含入相同的头文件并获得其中定义的
inline
函数。
3.
inline
指令就像
register
指令一样,只是对编译器的一种提示,而不是一个强制命令。一个表面上的
inline
函数,是否真是
inline
,必须视编译器而定。如果此函数被拒绝
inline
化,它将被视为一个
non-inline
函数,对此函数的调用动作就像正常函数调用动作一样被处理。在旧规则下,在每一个调用该函数的编译单元中的目标文件中都会产生一个该函数的定义,而且编译器视该函数为
static
从而消除连接时期的问题。此时,若该函数内还定义有
local static
变量,则该函数的每一个副本中都将拥有该变量的一份副本!在新规则下,不论牵扯的编译单元有几个,只有一个
out-of-line
的函数副本被产生出来。
4.
有时,编译器也会为一个已经成功
inline
的函数产生一个函数实体,比如在编译器或者程序员需要取该函数的地址时。
5.
inline
函数无法随着程序库的升级而升级,其改变将会导致所有用到它的程序全部重新编译。此外,
inline
函数中的
static
对象常会展现反直观的行为,因此如果函数含有
static
对象,通常避免将它声明为
inline
。还有,大部分除错器
debugger
面对
inline
函数都无能为力。
6.
只要明智地运用,
inline
函数会是每一个
C++
程序员的无价之宝,作为软件开发者,其目标便是要识别出可以有效增进程序效率的百分之二十的程序代码然后将它
inline
,或是尽量拉扯直到每块代码都高效运作。
2002-6-27
条款
34
:将文件之间的编译依赖关系
compilation dependencies
降至最低
问题
由于在接口与实现分离上,
C++
表现的很不好:
class
定义式中内含的不只是接口规格,还包括某些实现细节。之所以这样,是因为编译器看到一个
class object
定义式时,它必须知道要配置多少空间,唯一办法是询问
class
定义式。这样便在
class
的定义文件与其含入文件之间形成一种编译依赖,结果只要某个
class
或者其所倚赖的其它
classes
有任何一个改变了实现代码,那么含有或者使用该
class
的所有文件就必须重新编译。
1.
将编译依赖性最小化的关键技术在于以对
class
声明的依存性来取代对
class
定义的依存性,即让头文件尽可能自我满足,或至少让它依赖于
class
的声明而不要依赖于
class
定义。三项具体建议:
ü
如果
object references
或者
object pointers
可以完成任务,就不要使用
objects
。可以只靠一个类型声明就定义出指向该类型的
reference
和
pointer
,但定义出某个类型的
object
,就要使用该类型的定义。
ü
尽量以
class
的声明取代
class
的定义。声明一个会用到某个
class
的函数,纵使它使用
by value
方式传递该
class
的参数或返回值,也是这样。此时,要提供该
class
定义的是
clients
。
ü
不要在头文件中再
include
其它头文件,除非不这样做就无法编译,把这个责任留给
clients
。尽可能手动声明所需要的
classes
。
2.
Handle classes
或
Envelope classes
是将接口与实现分离的一项有效技术,它们将所有的函数调用转交给相应的
Body classes
或
Letter classes
,由后者完成真正的工作。
3.
另一种不同于
Handle classes
的做法是
Protocol class
。根据定义,
Protocol class
没有任何实现代码,其作用是为
derived classes
指定一个接口,它往往没有
data members
,也没有
constructors
,只有一个
virtual destructor
和一组用来表示接口的纯虚拟函数。
Protocol class
的
clients
必须用
pointers
和
references
来写程序,因为
Protocol classes
不可能被实体化。
Factory functions
或称为
virtual constructors
通常扮演
constructor
的角色,它们传回一个指针,指向动态配置而来的对象,该对象是真正被实体化的
derived classes
对象。最好让
Factory functions
成为
Protocol classes
的
static
函数。
4.
实现
Protocol class
有两种最平常的机制:一种从
Protocol classes
继承接口规格,然后实现出接口中的所有函数;第二种则涉及到多重继承。
5.
不论
Handle classes
和
Protocol classes
都无法获得
inline
函数的好处,所有实用性的
inline
函数都要求处理实现细节,而
Handle classes
和
Protocol classes
的设计目的正是用来避免实现细节曝光。
2002-6-29
继承关系与面向对象设计
Inheritance and Object-Oriented Design
条款
35
:确定你的
public inheritance
模塑出
isa
关系
1.
以
C++
完成面向对象程序设计,最重要的一个规则就是
public inheritance
意味着
isa
关系。如果令
class D
以
public
形式继承了
class B
,便是告诉编译器:每一个类型为
D
的对象同时也是一个类型为
B
的对象,但反之并不成立。但请注意,这并不意味着
D
数组是一种
B
数组!
2.
可适用于所有软件的完美设计是不存在的。所谓最佳设计,取决于这个系统现在与将来希望做什么事。为继承体系添加多余的
classes
,就像在
classes
之间构造不正确的继承关系一样,都是不良的设计。
2002-6-30
条款
36
:区分接口继承
interface inheritance
和实现继承
implementation inheritance
1
.
有关接口继承
interface inheritance
和实现继承
implementation inheritance
的三条:
ü
声明一个纯虚拟函数的目的是为了让
derived classes
只继承其接口;
ü
声明一个非纯虚拟函数的目的是为了让
derived classes
继承该函数的接口和缺省行为;
ü
声明非虚拟函数的目的是为了让
derived classes
继承函数的接口及其实现。
2
.
可以为纯虚拟函数提供定义,也就是说可以未它提供一份实现代码。
C++
并不认为这是错误的,不过只能静态地调用它,即必须指定其完整的
class
名称。它可以用来实现一种安全机制,为一般非纯虚拟函数提供更安全的缺省行为。
3.
非虚拟函数代表的意义是为不变性凌驾于变异性之上,所以不应该在
subclass
中重新定义它。
条款
37
:绝对不要重新定义继承而来的非虚拟函数
从实务的角度来看,非虚拟函数是静态绑定的,如果撰写
class D
并重新定义继承自
class B
的非虚拟函数,那么
D
对象很有可能展现出精神分裂的行径:当该函数被调用时,任何一个
D
对象都可能表现出
B
或
D
的行为,决定因素不在对象本身,而在于指向该对象指指针当初的声明类型。
从理论的角度来看,所谓
public inheritance
意味着
isa
的关系,任何
D
对象都是一个
B
对象,
B
的
subclasses
一定会继承
mf
的接口与实现,如果
D
重新定义
mf
,设计就出现矛盾。
2002-7-1
条款
38
:绝对不要重新定义继承而来的缺省参数值
首先,重新定义一个继承而来的非虚拟函数永远是错误的行为;其次,虚拟函数是动态绑定的,而缺省参数值是静态绑定的,就是说可能会在调用一个定义于
derived class
内的虚拟函数时,却使用
base class
为它指定的缺省参数值!
C++
这样做完全是为了执行期的效率。
对象的静态类型,是程序声明它时所采用的类型;对象的动态类型,是对象目前所代表的类型,即动态类型可以表现出一个对象的行为模式,它可以在程序执行过程中改变。
2002-7-2
条款
39
:避免在继承体系中做向下转型
cast down
动作
1.
为了摆脱
downcasts
,不论花多少努力都是值得的,因为
downcasts
既丑陋又容易出错,而且还会导致程序代码难以理解、难以强化、难以维护。
2.
解决
downcast
的最佳办法是将转型动作以虚拟函数的调用取代,并让每一个虚拟函数有一个“无任何动作”的缺省实现代码,以便应用在并不想要施行该函数的任何
classes
身上;第二个办法是让类型更明确一些,使得声明式中的指针类型就是真正的指针类型。
3.
万不得已,请使用由
dynamic_cast
运算符提供的安全向下转型动作
sefe downcasting
,在转型成功时会传回一个隶属新类型的指针,如果失败则传回
null
指针。此时
downcasting
必然导致的
if-then-else
程序风格,比起使用虚拟函数,实在是拙劣之至,所以万非得已不要出此下策。
4.
任何时候不要写出根据对象类型判断的不同结果而做不同事情的代码!不要在程序中到处放条件判断式或
switch
语句,让编译器来做这件事吧。
2002-7-7
条款
40
:通过
layering
技术来模塑
has-a
或
is-implemented-in-terms-of
关系
所谓
layering
,是以一个
class
为本,建立另一个
class
,并令所谓
layering class
(外层)内含所谓
layered class
(内层)对象作为
data member
。某些时候,两个
classes
不是
has-a
的关系,此时
public inheritance
并不适合它们,而
layering
技术则是实现
is-implemented-in-trms-of
关系的最佳选择。不过同时也会在这些
classes
之间产生了一个编译依存关系,利用条款
34
的技术可以很好的解决这个问题。
条款
41
:区分
inheritance
和
templates
template
用来产生一群
classes
,其对象类型不会影响
class
的函数行为;
inheritance
用于一群
classes
身上,其中,对象类型会影响
class
的函数行为。
条款
42
:明智地运用
private inheritance
1.
如果
classes
之间的继承关系是
private
,编译器通常不会自动将一个
derived class object
转换为一个
base class object
。由
private base class
继承而来的所有
members
,在
derived class
中都会变成
private
属性。
2.
private inheritance
意味着
implemented-in-terms-of
,使用这项技术的原因往往是想采用已经撰写于
base class
的某些程序代码,而不是因为
derived class
和
base class
之间有任何概念关系存在,即
private inheritance
意味着继承实现部分,接口部分略去。
3.
对于
is-implemented-in-terms-of
,应该尽可能使用
layering
,只有在涉及到
protected member
或虚拟函数时才使用
private inheritance
,因为唯有通过继承,才得以取用
protected members
;唯有通过继承,才允许虚拟函数被重新定义。
4.
base class
舍弃
template
技术而使用泛型指针
void *
来有效地遏制代码膨胀现象,并通过将
constructors
、
destructors
以及所有接口声明为
protected
,而将
data members
声明为
private
来阻止
clients
误用这个类;通过
private inheritance
,
derived class
继承实现部分代码,再使用
template
技术来建立类型安全
type-safe
的接口,同时将所有接口声明为
inline
来减少执行期成本。这样的设计带来的是最高的效率和最高的类型安全性,是一项巧妙的技术!
2002-7-5
条款
43
明智地运用多继承
multiple inheritance
,
MI
1.
MI
的根本问题是产生了一大堆但继承中不存在的复杂性。最根本的复杂性是模棱两可
ambiguity
,其次是继承时是否应该使用
virtual inheritance
。
2.
考虑一个
class
的两个
public base classes
,如果它们均有一个相同虚拟函数,那么在
derived class
使用这个函数时,必须明确指定这个函数属于哪个
base class
,同时还不能在
derived class
中改写这个函数,不过此时可以通过添加两个
classes
来将这个
class
继承体系中单一而模棱两可的函数名称一分为二为两个明确的、操作性质等同的名称。
3.
在钻石形继承体系中,通常要令顶层
class
为
virtual base class
,这样底层
derived class object
中就不会内含多份顶层
class
的
subobjects
,但也意味着增加了程序执行时间和空间的额外成本,因为
virtual base classes
常常是以对象指针来实现,而不是以对象本身来完成。由此可见,要在多继承的情况下完成有效的
class
设计,程序员似乎得拥有优秀的洞察力才行。然而,决定一个
base class
是否应该成为
virtual
缺乏一种完好的高阶定义,该决定通常只取决于“整个”继承体系的结构。在该结构尚未明朗之前,无法做出决定。
4.
非虚拟继承时,
base class constructor
的实参是在下一层的
derived class
的成员初始表中指定,然而对于虚拟继承,实参是在派生程度最深
most derived
的
classes
的成员初始表中指定。因此,对
virtual base
执行初始化动作的那个
class
,在继承体系中可能距离其
base
相当远,而一旦有新的
classes
加入此体系,初始化动作可能就要由别的
class
来担当。解决此问题的好办法是消除传递
constructor
实参至
virtual bases
的必要性。最简单的做法是避免
virtual base classes
拥有
data members
。
5.
考虑如下钻石形继承体系:
class A{ virtual void mf(){}; }
class B : virtual public A{} //
继承默认的虚拟函数
mf
class C : virtual public A{virtual mf(){};}//
改写虚拟函数
mf
class D : public B, public C{}
代码
D *pd = new D; pb->mf();
中,
D
对象调用的
mf
会被编译器明确决议为
C::mf
。
6
.
造成
MI
这么难用是因为,要让所有细节以某种合理的方式共同运作,必然会伴随某种复杂性。其实这些复杂都源于
virtual base classes
,所以尽量避免使用
virtual bases
即钻石形继承体系。
2002-7-13
条款
44
:说出你的意思并了解你所说的每一句话
继承关系与面向对象关系中最重要的一些观念:
ü
共同的
base class
意味着共同的特征;
ü
public inheritance
意味着
isa
;
ü
private inheritance
意味着
is-implemented-in-terms-of
;
ü
layering
意味着
has-a
或
is-implemented-in-terms-of
;
在牵涉到
public inheritance
时,以下几点才成立:
ü
纯虚拟函数意味着只有函数接口会被继承;
ü
一般虚拟函数意味着函数的接口及缺省实现代码会被继承;
ü
非虚拟函数意味着函数的接口和实现代码都会被继承。
2002-7-7
条款
45
:清楚知道
C++
编译器默默为我们完成那些函数
1.
一个空的
class
在编译器处理过它之后就不再为空,如果你写:
class Empty{};
其意义相当于:
class Empty{
public:
Empty();
Empty(const Empty& rhs);
~Empty();
Empty& operator = (const Empty& rhs);
Empty* operator& ();
const Empty* operator& () const;
}
2.
这些函数只有在需要时,编译器才会定义它们。
default constructor
和
destructor
不做任何事情,只是让你得以产生和摧毁这个
class
的对象,并提供一个便利场所让编译器放置一些用来完成幕后动作的代码;产生出来的
destructor
并非虚拟函数,除非这个
class
继承自一个拥有
virtual destructor
的
base class
;缺省的
address-of
运算符只负责传回对象地址;对于缺省
copy constructor
或
assignment
运算符,官方规则是对该
class
的
nonstatic data members
执行
memberwise copy construction
或
assignment
动作。
3.
编译器为
class
默默产生的
assignment
运算符只有在其函数代码不但合法而且合理的情况下才会有正确行为,否则编译器会拒绝产生一个
operator =
,并报告编译错误。因此,如果打算让内含
reference member
或
const member
的
class
支持
assignment
动作,必须自行定义
assignment
运算符。如果
base classes
将标准的
assignment
运算符声明为
private
,编译器也会拒绝为其
derived class
产生
assignment
运算符。
2002-7-8
杂项讨论
Miscellany
条款
46
:宁愿编译和连接时出错,也不要执行时才错
不做执行期检验工作,程序会更小更快;尽可能将错误侦测工作交给连接器来做,或最好是交给编译器来做,这样做的好处不仅是程序大小的降低和数度的增加,好包括可信度的提升。相反,在执行期侦测错误,比起在编译期或连接期捕捉它们要麻烦许多。通常,只需稍稍改变设计,就可以在编译期捕捉除可能的执行期错误,这往往要加入新的类型。
条款
47
:使用
non-local static objects
之前先确定它已有初值
所谓
non-local static objects
,是指定义于
global
、
namespace scope
、
class
或者
file scope
内的
static objects
。每当在不同的编译单元内定义有
non-local static objects
时,想要决定它们以适当的次序初始化是极度困难甚至无法办到的事情。最常见的形式是,在多个编译单元中,
non-local static objects
被隐式的
template
具现化行为产生出来,这样不但不可能决定正确的初始化次序,甚至不值得我们寻找有可能决定正确次序的特殊情况。
解决问题的方案是不再取用
non-local static object
,而改用行为类似
non-local static objects
的对象。这项被称为
Singleton pattern
的技术很简单:首先,将每个
non-local static object
移到一个它专属的函数中,在那里将它声明为
static
;然后,令这个函数传回一个
reference
,指向这个
static object
。这项技术的关键是以函数内的
static objects
取代
non-local static objects
,其依据是
C++
虽然对于何时初始化一个
non-local static object
没有任何明确表示,但它却非常明白的指出函数中的
static object
的初始化时机。使用这项技术可以保证所获得的
references
一定指向已经初始化妥当的对象,同时,如果从未调用这样的函数还可以避免对象的构造成本和析构成本。
2002-7-10
条款
48
:不要对编译器的警告信息视而不见
警告信息天生就和编译器相依相靠,所以轻率地依赖编译期为你找出程序错误,决不是什么好主意。编译器的用途基本上是将
C++
代码转换为可执行代码,而不是一张安全网。
条款
49
:尽量让自己熟悉
C++
标准程序库
1.
为了避免程序员使用的
class
名称或函数名称与标准程序库所提供的名称相冲突,标准程序库的每一样东西几乎都驻在
namespase std
之中。由此而来的问题是,世上有无可数计的
C++
代码依赖那些已使用多年的准标准程序库,那些软件并不知道什么是
namespace
。以下是
C++
表头文件的组织状态:
ü
旧有的C++头文件如<iostream.h>尽管不是官方标准,但有可能继续存在。这些头文件的内容不在namespace std中;
ü
新的C++头文件如<iostream>包含的基本功能和对应的旧头文件相同,但其内容在namespace std中。(在标准化的过程中,程序库中有些组件细节稍有修改,所以新旧头文件中的实体不一定完全对应。)
ü
标准C头文件如<stdio.h>继续被支持。这类头文件的内容不在std中。
ü
具有C库功能的新C++头文件具有如<cstdio>这样的名字。它们提供的内容和相应的旧C头文件相同,但全部放在std中。
2.
关于标准程序库,必须知道的第二件事是,几乎每一样东西都是template。string、complex、vector都不是class而是class template;cin的真正类型是basic_istream<char>,string的真正类型是basic_string<char>。
3.
C++
标准程序库内的主要组件:
ü
C
标准程序库,它有些小小的改变,但整体而言改变不大。
ü
iostreams
。和传统的iostream相比,它已被template化了。它的继承体系已被修改,内容被扩充以便可以抛出异常信息,同时它可以支持strings和多国文化。它依旧支持stream buffers、formatters、manipulators、files以及cin、cout、cerr、clog等对象。
ü
strings
。
ü
containers
。C++ 标准库为vectors、lists、queues、stacks、deques、maps、sets和bitsets提供了高效的实现。同时,strings也是containers!
ü
algorithms
。用以轻松操作标准containers,它们大部分都适用于所有containers以及语言内建的数组身上。
ü
国际化internationalization支持。其主要组件是facets和locales。前者描述某一文化的特殊字符集应该如何处理,包括校对规则、日期和时间表示法、信息代码与自然语言之间的映射关系等等。后者含有一整组facets。C++允许多个locales同时在程序库中起作用,所以同一个程序的不同部分可能会采用不同的规则。
ü
数值处理。
ü
诊断功能。标准程序库提供三种方法来记录错误:经由C的assertions、经由错误代码、经由异常信息。
2002-7-11
条款
50
:加强自己对
C++
的了解
C++
最主要的几个设计目标:与C兼容、高效率、与传统工具和开发环境兼容、解决问题的真正能力,它的目的是成为专业程序员在各种不同领域中解决真正问题的一个威力强大的工具。这些目标可以解释C++语言的许多来龙去脉。
2002-7-13