条款01:视C++语言为一个语言联邦
将C++
视为一个由多种语言组成的联邦。在某个次语言中,各种守则都简单易懂;从一个次语言转换为另一个次语言时守则可能改变。有以下四种次语言:
1.C
:区块、语句、预处理器、内置数据类型、数组、指针等。
2.C with Class:classes
:封装、继承、多态、虚函数。
3.Template C++
:泛型编程。
4.STL
:容器、迭代器、算法、函数对象。
条款02:尽量以
const,enum,inline
替换#define
使用#define
的好处:
1. 用宏定义一个函数的时候不会导致因函数调用导致的额外开销,会有一定的效率提升
2. 不会导致非必要的内存分配。
使用#define
的坏处:
1.调试:当使用#define
时记号名称在编译之前就被预处理器替换掉了,可能并未进入记号表中,发生错误时很难追踪。
因此用
const double AspectRatio = 1.653;
替换
#define ASPECT_RATIO 1.653;
2.作用域问题:我们是无法用#define
来创建一个class
的专属常量,一旦宏被定义,其后的编译过程都会有效,没有任何的封装性。
3. 导致函数不安全
#define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b))
int a = 5,b = 0;
CALL_WITH_MAX(++a,b); //a被累加2次
CALL_WITH_MAX(++a,b+10); //a被累加1次
用常量替换#define
时注意事项
1.定义常量指针时有必要将指针(而不是指针所指之物)声明为const
,如
const char* const authorName = “Scott Meyers”;
使用string
更好些:
const std::string authorName(“Scott Meyers”);
2.使用class
专属常量时,让常量成为class
成员以限制其作用域。
class GamePlayer {
private:
static const int NumTurns = 5; //常量声明式 static确保常量只有一个实体
int scores[NumTurns]; //使用该常量
...
};
以上NumTurns
是声明式而非定义式,C++
还要求提供定义式:
Const int GamePlayer::NumTurns;//声明时已获得初值定义时不可以再赋值
旧式编译器也许不支持static
成员在声明式上获得初值,只能将初值放在定义式。然而在上述GamePlayer
的数组声明式中,编译器必须在编译期间知道数组的大小,而又不允许在声明式上获得初值,可以使用enum
:
class GamePlayer {
private:
enum { NumTurns = 5 };
int scores[NumTurns];
...
};
enum
无法取到地址,这样做可以避免被别人取到指向这个整型变量的指针。enum
和#define
一样不会导致非必要的内存分配。
条款03:尽可能使用const
条款05:了解C++默默编写并调用哪些函数
编译器可以自动为class
创建一个default
构造函数、copy构造函数、copy assignment
操作符以及析构函数,所有这些函数都是public
且inline
。
因此你写下:
class Empty { };
就等同于你写下这样的代码:
class Empty {
public:
Empty() { ... }
Empty(const Empty& rhs) { ... }
~Empty() { ... }
Empty& operator=(const Empty& rhs) { ... }
};
default
构造函数和析构函数主要是编译器用来放置调用base class
和non-static
成员变量的构造函数和析构函数。注意,编译器产出的析构函数是个non-virtual
,除非这个class
的base class
自身声明有virtual
析构函数。
copy构造函数和copy assignment
操作符,编译器只是将来源对象的每一个non-static
成员变量拷贝到目标对象。考虑这样一个类:
template
class NamedObject {
public:
NamedObject(const char* name, const T& value);
NamedObject(const std::& name, const T& value);
...
private:
std::string nameValue;
T objectValue;
}
NamedObject<int> no1("hello world", 2);
NamedObject<int> no2(no1);
编译器生成的copy构造函数必须以no1.nameValue
和no1.objectValue
为初值设定no2.nameValue
和no2.objectValue
。其中nameValue
的类型是string
,而标准的string
有个copy构造函数,所以no2.nameValue
的初始化方式是调用string
的copy构造函数并以no1.nameValue
为实参,另一个成员NameObject::objectValue
的类型是int
,是内置类型,所以会拷贝no1.objectValue
内的每个bits完成初始化。
编译器所生的copy assignment
操作符其行为和copy构造函数一致,但如果生成的代码是不合法的,编译器会拒绝为class
生成operator=
。例如成员变量是reference
或const
,或者base class
将copy assignment
操作符声明为private
。
条款06:若不想使用编译器自动生成的函数,就该明确拒绝
通常如果不希望class
支持某一项特定的功能,只要不声明对应的函数即可,但这个策略对copy构造函数和copy assignment
操作符却不起作用,因为编译器会默认声明。那么如何达到目的呢?
关键是所有的编译器产出的函数都是public
,为阻止这些函数被创建出来,需要自行声明它们,因此可以将copy构造函数和copy assignment
操作符声明为private
。这样既阻止了编译器创建它,又阻止了人们调用它。
这被用在C++的iostream
程序库中阻止拷贝行为。
一般而言这种做法并不是绝对安全,因为member
函数和friend
函数还是可以调用private
函数。当然你可以不去定义它们(即不实现他们),如果不慎调用了任何一个,就会获得一个连接错误(linkage error)。
将连接期错误移到编译期是可能的(而且是好事,越早侦测出错误越好),设计一个专门阻止copying动作的base class
类:
class Uncopyable {
protected:
Uncopyable() {}
~Uncopyable() {}
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};
//为阻止HomeForSale 对象被拷贝,使其继承Uncopyable:
class HomeForSale : private Uncopyable {
};
这样当member
函数和friend
函数尝试拷贝 HomeForSale
对象,编译器便试着生成一个copy构造函数和一个copy assignment
操作符,编译器会尝试调用base class
,由于其base class
的拷贝函数是private
所以会被拒绝。这样就将连接期错误移到了编译期。
Uncopyable class
的实现和运用颇为微妙,不一定以public
继承它,Uncopyable
的析构也不一定得是virtual
。
条款07:为多态基类声明virtual析构函数
当基类被派生(即带有virtual
函数,因为通常一个class
被意图用作基类时含有virtual
函数)且基类的析构函数不为虚析构函数时,如果用一个基类指针指向派生类对象,该对象经由基类指针删除时,会出现对象的派生成分没被销毁,基类成分却被销毁的“局部销毁”对象,这将造成资源泄露等后果。
因此,当基类被派生时,请务必将它的析构函数设置为虚析构函数。
当需要抽象类但并没有纯虚函数时,可以将析构函数声明为纯虚析构函数。但此时必须为纯虚析构函数提供一份定义,不然连接器会报错。
class AWOV {
public:
virtual ~AWOV() = 0; //声明纯虚析构函数
};
AWOV::~AWOV() { } //纯虚析构函数的定义
class
如果不是作为基类使用,或不是为了具备多态性,就不该声明虚析构函数。例如标准string
和STL
容器。
条款08:别让异常逃离析构函数
避免析构函数抛出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常然后吞下它们或者结束程序。
如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class
应该提供一个普通函数(而非在析构函数中)执行该操作。
class DBConn {
public:
…
void close( ) //供客户使用的新函数
{
db.close( );
closed = true;
}
~DBConn( )
{
if(!closed) {
try { //关闭连接
db.close( );
}
catch {…} { //如果close调用失败,记录下来并结束程序或吞下异常
制作运转记录,记录对close的调用失败;
…
}
}
}
private:
DBConnection db;
bool closed;
};
条款09:绝不在构造和析构过程中调用virtual函数
当基类的构造函数中调用了一个虚函数时,此时创建一个派生类对象,无疑基类构造函数将先于派生类构造函数执行,由于基类构造期间虚函数不会下降到派生类阶层,也就是说基类构造函数中的虚函数尚未被初始化,“要求使用对象内部尚未初始化的成分”是危险和不被允许的。
相同道理也适用于析构函数。
因此不要在构造和析构过程中调用virtual
函数。
并且构造和析构函数调用的所有函数也须遵守这一点。
条款10:令operator= 返回一个reference to *this
赋值可以写出连锁的形式:
x = y = z = 15;
因为赋值是采用右结合律的,所以赋值操作符必须返回一个引用指向操作符的左侧实参。这是为classes
实现赋值操作符时应该遵循的协议:
class Widget {
public:
...
Widget& operator=(const Widget* rhs) { //返回类型是引用,指向当前对象
...
return *this; //返回左侧对象
}
};
这个协议不仅适用于以上的标准赋值形式,也适用于所有赋值相关运算,如+=,-=,*=
等。
条款11:在operator= 中处理“自我赋值”
自我赋值是指对象赋值给自己。在有别名(即有一个以上的方法指向某对象)的情况下很容易出现自我赋值。
假设建立一个class用来保存一个指针指向一块动态分配的位图:
class Bitmap { ... };
class Widget {
...
private:
Bitmap* pb; //指针,指向一个从heap分配而得的对象
};
Widget& Widget::operator=(const Widget& rhs) { //一份不安全的operator=实现版本
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
这里的自我赋值问题是如果*this
和rhs
是同一个对象,delete
删除的就是rhs
的bitmap
,Widget
将持有一个指针指向一个已被删除的对象。想要阻止这种错误,传统的做法是借由operator=
最前面的一个“认同测试”达到“自我赋值”的检验目的:
Widget* Widget::operator=(const Widget& rhs) {
if (this == &rhs) return *this; //证同测试_如果是自我赋值就不做任何事
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
这样做并不具备异常安全性。如果new Bitmap
抛出异常(分配时内存不足或bitmap
的copy构造函数抛出异常),Widget
最终会持有一个指针指向一块被删除的Bitmap
。以下代码可以解决这个问题,只要注意在复制pb
所指东西之前别删除pb
:
Widget& Widget::operator=(const Widget& rhs) {
Bitmap* pOrig = pb; //记住原先的pb
pb = new Bitmap(*rhs.pb) //令pb指向*pb的一个副本
delete pOrig; //删除原先的pb
return *this;
}
现在,如果new Bitmap
抛出异常,pb
及其栖身的Widget
保持原状。同时也能处理自我赋值。
另一种替代方案是使用copy and swap
技术。
class Widget {
...
void swap(Widget& rhs); //交换*this和rhs的数据
...
};
Widget& Widget::operator=(const Widget& rhs) {
Widget temp(rhs); //为rhs数据制作一份副本
swap(temp); //将*this数据和上述复件的数据交换
return *this;
}
这个方法基于的事实是:
(1)某class的copy assignment
操作符可能被声明为“以by value
方式接受实参”;
(2)以by value
方式传递东西会造成一份副本。
Widget& Widget::operator=(Widget rhs) {
swap(rhs):
return *this;
}
这种做法为了伶俐巧妙的修补而牺牲了清晰性。然而将copying的动作从函数的本体移至函数参数构造阶段,可以令编译器生成更加高效的代码。
条款12:复制对象时勿忘其每一个成分(又名:编译器的复仇)
如果你手动地声明了copying
函数,编译器仿佛被冒犯似的,会以一种奇怪的方式回敬:当实现代码几乎必然出错时却不告诉你。
例如有新的私有成员变量加入,就必须同时修改copying
函数,如果忘记修改,编译器也不会提示。
当基类被继承,写copying
函数时不能遗忘复制其基类成分。这些成分一般是private
,无法直接访问,所以应该调用基类的构造函数。
当编写一个copying
函数时请确保:
(1)复制所有的local
成员变量;
(2)调用所有的base classes
内的适当的copying
函数。
如果发现copy
构造函数和copy assignment
操作符有相近的实现体,消除重复代码的做法是,建立一个新的成员函数给两者调用。这个函数通常是private
且被命名为init
。
条款13:以对象管理资源
把资源放进对象内,可以倚赖C++的“析构函数自动调用机制”确保资源被释放。
标准程序库提供了类指针对象auto_ptr
(智能指针):
class Investment {…};
Investment* createInvestment(); //工厂函数返回指针,指向Investment继承体系内的动态分配对象
void f()
{
std::auto_ptr pInv(createInvestment());
...
}
1.获得资源后立刻放进管理对象内。
2.管理对象运用析构函数确保资源被释放。
由于auto_ptr
被销毁的时候会自动销毁所指的对象,所以要注意别让多个auto_ptr
同时指向同一个对象。为了预防这个问题,auto_ptrs
有一个不寻常的性质:若通过copy
构造函数或copy assignment
操作符复制它们,它们就会变成null,而复制所得的指针将获得资源的唯一拥有权。
std::auto_ptr pInv1(createInvestment());
std::auto_ptr pInv2(pInv1); // 现在pInv2指向对象,pIn1被设为null
pInv1 = pInv2; // 现在pInv1指向对象,pInv2被设为null
auto_ptr
替代方案是“引用计数型智能指针”RCSP
,记录共有多少对象指向某笔资源。
void f()
{
...
std::tr1::shared_ptr pInv1(createInvestment());
std::tr1::shared_ptr pInv2(pInv1); // pInv1和pInv2指向同一个对象
pInv1 = pInv2; // 同上,没有改变
...
}
auto_ptr
和tr1::shared_ptr
两者都在其析构函数内做delete
而不是delete[]
动作,意味着在动态分配而得的array
上使用auto_ptr
和tr1::shared_ptr
是馊主意。
条款14:在资源管理中小心coping行为
coping
函数包括copy
构造函数与copy assignment
操作符。
条款13中提出了RAII
的概念:资源获得时机便是初始化时机。当一个RAII
对象被复制时,应当选择以下两种行为:
1.禁止复制。具体做法见条款06:将copying
操作声明为private
。
2.对底层资源采用“引用计数法”。
tr1::shared_ptr
允许指定“删除器”,“删除器”是一个函数或函数对象,当引用次数为0时被调用。此机能可以用于封装希望对资源采取的操作,例如当处理互斥器对象mutex
时,当引用计数为0时利用删除器函数解除对mutex
的锁定,如果没有删除器,则会直接删掉mutex
。
条款15:在资源管理类中提供对原始资源的访问
在条款13中,使用智能指针保存工厂函数:
std::tr1::shared_ptr pInv1(createInvestment());
假如用某个函数处理Investment
对象,如:
int daysHeld(const Investment* pi);
int days = daysHeld(pInv); //这样调用是错误的!
因为daysHeld
需要的是Investment*
指针,而不是类型为tr1::shared_ptr
的对象。
这时候需要一个函数将RAII类对象(tr1::shared_ptr
)转换为其所内含之原始资源(底部Investment*
),有显式转换和隐式转换两种做法。
显式转换:get
成员函数。
int days = daysHeld(pInv.get());
隐式转换:大多智能指针都重载了指针取值操作符(operator->
operator*
)。
class Investment {
public:
bool isTaxFree() const;
…
};
Investment* createInvestment();
std::tr1::shared_ptr pi1(createInvestment());
bool taxable = !(pi1->isTaxFree()); //用operator->访问资源
std::auto_ptr pi2(createInvestment());
bool taxable2 = !((*pil).isTaxFree()); //用operator*访问资源
条款16:成对使用new和delete时要采取相同形式
string* stringArray = new string[100];
…
delete stringArray; //错误 没有调用所有的析构函数
delete [] stringArray; //正确
string* stringArray2 = new string;
…
delete stringArray2; //正确
delete [] stringArray2; //错误 delete会读取若干内存解读为“数组大小”,然后开始多次调用析构函数
//注意对数组使用typedef时也要匹配使用delete []
条款17:以独立语句将
newed
对象置入智能指针
int priority(); //函数
void processWidget(tr1::shared_ptr pw, int priority); //在动态分配的Widget上进行带有优先权的处理
processWidget(new Widget, priority()); //错误无法进行将得自new Widget的原始指针转换为processWidget所要求的tr1::shared_ptr
processWidget(tr1::shared_ptr (new Widget), priority()); //正确
上述代码中,调用processWidget
之前,编译器要做三件事:执行new Widget
、调用priority
、调用tr1::shared_ptr
构造函数。由于调用priority
发生的顺序是任意的,当发生在另外两个操作之间时(也就是按照上述顺序),调用一旦发生异常,则导致new Widget返回的指针未被置入tr1::shared_ptr而遗失,就引发了资源泄露。解决办法是将new操作分离出来,这样编译器对于跨越语句的操作没有重新排列的权利,不会导致上述问题:
tr1::shared_ptr pw(new Widget);
processWidget(pw,priority());