目录
条款2:尽量以const、enum、inline替换#define
条款5:C++会默认生成构造函数、拷贝构造函数、拷贝操作符、析构函数
条款8:令operator= 返回一个reference to *this;在operator= 中处理“自我赋值”
条款14:以pass-by-reference-to-const替换pass-by-value
条款16:宁愿用non-member、non-friend替换member函数
条款17:若所有参数都需类型转换,请采用non-member函数
一、让自己习惯C++
条款1:视C++为一个语言联邦
C++是个多重范型编程语言,同时支持过程形式、面向对象形式、函数形式、泛型形式、元编程形式。它主要有四个次语言。
1.C:C++仍以C为基础。区块、语句、预处理、内置数据类型、数组、指针都来自于C。
2.object-oriented C++:这部分是C with classes所追求的。构造与析构、封装、继承、多态、虚函数。
3.template C++:C++的泛型编程,带来了模板元编程(TMP)。
4.STL:是个template程序库。
条款2:尽量以const、enum、inline替换#define
因为#define在编译前就处理完,所以在获得错误信息时,编译器只告诉某个常量有错误而不告诉常量的变量名。这样导致调试bug时比较麻烦。以下几种方法都可以替换宏:
1.用常量替换非指针内置类型:const double pi = 3.14;
2.用指向常量的常量指针替换char*类型:const char* const authorName = "Scott Meyers";
或者用string替换char*类型:const std::string authorName("Scott Meyers");
3.用静态成员常量替换class专属常量:
class GamePlayer {
private :
static const int NumTurns = 5;
int scores[NumTurns]; };
或者用枚举类型:
class GamePlayer {
private :
enum { NumTurns = 5; };
int scores[NumTurns]; };
4.用inline函数替换形似函数的宏(macros)
条款3:尽可能使用const
const允许指定一个语义约束,而编译器会强制实施这项语义约束。
const写在类型前和写在类型后没区别:const Widget* pw和Widget const* pw意义相同。
若是成员函数的功能不会改变成员变量,则在函数结尾加上const(bitwise constness)。但若是想修改某个成员变量,则在该变量前加上mutable(logical constness)。
条款4:确定对象被使用前已先被初始化
C++规定,对象的成员变量初始化动作发生在进入构造函数本体之前,所以在构造函数内部的动作叫做赋值而不是初始化。初始化效率比赋值高。但如果多个初始化函数中,某些成员变量动作相同,那么可以把它们放到某个private函数中,对它们进行赋值动作,从而减少重复操作。
二、构造、析构、赋值运算
条款5:C++会默认生成构造函数、拷贝构造函数、拷贝操作符、析构函数
这些函数都是public和inline的。如果不想使用默认生成的函数,可以在private里声明函数,但不定义它。这样外面成员无法调用,内部成员也无法使用。
条款6:为多态类声明virtual析构函数
条款7:绝不在构造和析构函数过程中调用virtual函数
基类的构造和析构函数要早于继承类。
条款8:令operator= 返回一个reference to *this;在operator= 中处理“自我赋值”
条款9:复制对象时勿忘其每一个成分
三、资源管理
一旦用了资源,将来必须还给系统。C++程序中最常见的资源是动态分配内存。其他还有:文件描述器、互斥锁、图形界面中的字型和笔刷、数据库连接、网络sockets。
条款10:以对象管理资源
管理对象运用析构函数确保资源被释放。
条款11:在资源管理中小心copying行为
比如互斥锁,C API函数处理该对象有lock和unlock两个函数可用:
void lock(Mutex* pm);
void unlock(Mutex* pm);
为确保不忘记一个被锁住的Mutex解锁,可以建立一个class来管理锁:
class Lock
{
public:
explicit Lock(Mutex* pm)
: mutexPtr(pm)
{lock(mutexPtr);}
~Lock(){unlock(mutexPtr);}
private:
Mutex* mutexPtr;
}
但若lock对象有可能被复制,所以要么禁止复制,要么计算资源的被引用次数。
条款12:在资源管理类中提供对原始资源的访问
许多API函数的参数需要提供资源,比如条款11的互斥锁。函数可以提供显示转换和隐式转换。
显示转换:
//定义
class Font
{
public:
...
FontHandle get() const {return f;}
...
private:
FontHandle f;
}
//调用
Font f;
FontHandle f1 = f.get();
隐式转换:
//定义
class Font
{
public:
...
operator FontHandle() const {return f;}
...
private:
FontHandle f;
}
//调用
Font f;
FontHandle f1 = f;
条款13:成对使用new和delete时要采用相同形式
std::string* stringPtr1 = new std::string;
std::string* stringPtr12 = new std::string[100];
delete stringPtr1;
delete[] stringPtr2;
四、设计与声明
条款14:以pass-by-reference-to-const替换pass-by-value
缺省情况下,C++以by value方式传递对象至函数,这是以实际实参的复件为初值,调用对象的copy构造函数产出。by value方式传递一次的成本至少是一次构造函数和析构函数,若是继承类则可能是多次构造函数和继承函数。这使得by value方式是费时操作。
但对于内置类型(如int)和STL,选择pass-by-value则比较好。
条款15:将成员变量声明为private
private变量封装性更好。
条款16:宁愿用non-member、non-friend替换member函数
如果某些东西被封装,它就不再可见。愈多东西被封装,愈少人看到它,就有愈大得到弹性改变它。如果一个member函数和non-member、non-friend提供相同机能,那么较大封装性的是后者。因为它并不增加“能够访问class内private变量”的函数数量。但为了它可以访问某类,可以将它放入命名空间中:
namespace WebBrowserStuff {
class WebBrowser { ... };
void clearBrowser{WebBrowser& wb};
...
}
namepsace和class不同,前者可以跨越多个源码文件而后者不能。这些函数放在多个头文件但属于同一个namespace,意味着用户可以轻松扩展这些函数:通过添加更多non-member、non-friend函数到此命名空间中。这是class定义式对用户而言不能扩展的。
条款17:若所有参数都需类型转换,请采用non-member函数
之前提到class应避免隐式转换,但也有例外,比如建立数值类型。比如原本有理数相乘的成员函数写法如下:
const Rational operator* (const Rational& rhs) const;
那么最正确的写法应该是:
Rational oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf * oneEighth;
result = result * oneEighth;
但若是result = result * 2就会出现错误。因为2不是Rational类型,需要进行隐式转换:
const Rational temp(2);
result = oneHalf * temp;
所以这时构造函数应该是non-explicit类型的。
但若是result = 2 * result又不行了。this对象不能进行隐式转换。这时还需要将operator操作符重载为non-member函数,就可以对两个参数都进行隐式转换:
const Rational operator* (const Ratioanl& lhs, const Rational& rhs)
{
return Ratinal(lhs.numerator() * rhs.numerator(),
lshs.denominator() * rhs.denominator() );
}
五、实现
太快定义变量可能造成效率上的拖延;过度使用转型(casts)可能导致代码变慢;返回对象handles可能会破坏封装并留给客户dangling handles;未考虑异常带来的冲击可能导致资源泄露和数据破坏。过度inlining可能引起代码膨胀,过度耦合可能导致冗长build tmes。
条款18:尽可能拖后变量定义式的出现时间
条款19:尽量少做转型动作
条款20:为异常安全而努力是值得的
对于下面的代码,从“异常安全性”观点来看,这个函数很糟。
void change()
{
lock(&mutex);
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
unlock(&mutex);
}
当异常抛出时,带有异常安全性的函数会:
1.不泄露任何资源:一旦new Iamge导致异常,unlock的调用就不会执行。
2.不允许数据败坏:一旦new Iamge导致异常,imageChanges累加是不应该的。
异常安全函数提供以下三种保证之一:
1.基本承诺:如果异常被抛出,程序内任何事物仍然保持有效状态;
2.强烈保证:如果异常被抛出,程序状态不改变。即函数调用失败,程序会回复到调用函数之前的状态。
3.不抛掷保证:承诺绝不抛出异常。
“强烈保证”可使用copy-swap策略:先对修改的对象做出一份副本,然后在副本上做出修改。如果有异常则抛出,原对象仍保持原有状态。但这种策略耗费时间和空间成本,具体使用视情况而定。
条款21:了解inline函数
调用inline无需承受函数调用所招致的额外开销,但过度使用会造成程序体积太大,导致额外的换页行为,降低指令高速缓存装置的击中率。
inline只是个申请,不是强制命令。申请可以隐式提出,也可以显式提出。隐式方式是将函数定义在class定义式内:
class Person {
public:
...
int age() const { return theAge; }
...
}
显式是使用inline命令。
条款22:将文件间的编译依存关系降至最低
#include <string>
#include "date.h"
#include "address.h"
class Person {
public:
Person(const std::string& name, const Date& birthday,
const Address& addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
private:
std::string theName; //实现细目
Date theBirthDate; //实现细目
Address theAddress; //实现细目
}
若是date和address头文件有任何改变,那么重新编译时person文件也需要重新编译。这时可以用class的声明替换class的定义:
#include <string> //标准程序库组件不该被前置声明
#include <memory>
class PersonImpl; //Person实现类的前置声明,两者拥有相同的成员函数
class Date; //person接口用到的class的前置声明
class Address;
class Person {
public:
Person(const std::string& name, const Date& birthday,
const Address& addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
private:
std::tr1::shared_ptr<PersonImpl> pImpl; //指向实现类的指针,属于pimpl idiom设计
}
这样Person的客户就完全与Person的实现细目分离了,也不可能写出“取决于细目”的代码,这就是接口与实现分离。这种类称为Handle classes。
这种类需要两个头文件,一个声明式另一个定义式。类的实现也需要包含这两个头文件。
#include "Person.h"
#include "PersonImpl.h"
Person::Person(const std::string& name, const Date& birthday,
const Address& addr)
:pImpl(new PersonImpl(name, birthday, addr))
{}
std::string Person::name() const
{
return pImpl->name();
}
如果可以使用对象引用或对象指针完成的操作,就不要直接使用对象。因为只靠一个类型声明就可以定义指向该类型的引用或指针,而定义某类型的对象需要该类型的定义式。
六、继承与面向对象设计
条款23:确定你的public继承是is-a关系
public继承意味着“is-a”,适用于base classes身上的每一件事情一定适用于derived classes。因为每一个derived class对象也是一个base class对象。
条款24:避免遮掩继承而来的名称
如果在derived class里重新声明了base class里的函数,那么会把base class里的函数覆盖掉(即base class里对应函数不可用)。但如果想让base class里的函数可以和derived class里名称相同的函数同时使用(即在derived class里进行函数重载),那么可以在derived class里使用using命令声明base class里的函数。
若是derived class是private继承base class,并只想继承base class里的某个函数,可以使用转交函数:
class Derived class : private Base {
public :
virtual void mf1 () //转交函数(forwarding function)
{ Base::mf1(); } //暗自成为inline函数
...
};
条款25:区分接口继承和实现继承
接口继承相当于(纯虚函数和虚函数),实现继承相当于非虚函数。
条款26:绝不重新定义继承而来的non-virtual函数
条款27:绝不重新定义继承而来的缺省参数值
条款28:明智使用private继承
private继承后,base class中的所有成员在derived class中都会变成private类型。同时,private继承意味着只有实现部分被继承,接口部分应被忽略。
条款29:明智使用多重继承
若是一个类继承多个类,使用基类函数时需要明确指出调用哪个函数。
class BorrowableItem {
public:
void checkOut();
};
class ElectronicGadget {
public:
void checkOut();
};
class MP3Player:
public BorrowableItem,
public ElectronicGadget
{};
MP3Player mp;
mp.BorrowableItem::checkOut();