0%

Item 9: Never call virtual functions during construction or destruction.

在构造和析构期间不要调用 virtual 函数,因为这类调用不会下降至 derived class
(比起当前执行构造函数和析构函数的那层)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Transaction {                               // base class for all
public: // transactions
Transaction(){ // base class ctor
logTransaction(); // as final action, log this
}
virtual void logTransaction() const = 0; // make type-dependent
};

class BuyTransaction: public Transaction { // derived class
public:
virtual void logTransaction() const; // how to log trans-
};
...
BuyTransaction b;

b 在构造时,调用到父类Transaction的构造函数,其中对 logTransaction 的调用会被解析到 Transaction 类。 那是一个纯虚函数,因此程序会非正常退出。

derived class 对象的 base class 构造期间,对象的类型是 base class 而不是 derived classo 不只 virtual 函数会被编译器解析至(resolve to) base class ,若使用运行期类型信息 RTTI(runtime type information, 例如 dynamic_cast typeid) ,也会把对象视为 base class 类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Transaction{
public:
Transaction(){
cout<<typeid(this).name()<<endl;
}
};
class BuyTransaction: public Transaction{
public:
BuyTransaction(){
cout<<typeid(this).name()<<endl;
}
};
void main(){
BuyTransaction b;
}

输出

1
2
P11Transaction
P14BuyTransaction

相同道理也适用于析构函数.

Item 8: Prevent exceptions from leaving destructors.

C++ 本身不阻止在析构函数抛出异常,但在析构函数中抛出的异常往往会难以捕获,引发程序非正常退出或未定义行为。例如:

1
2
3
4
5
6
7
8
9
class Widget {
public:
...
~Widget() { ... } //假设这里可能抛出异常
};

void doSomething(){
std::vector<Widget> v; // v 这里被自动析构
}

当v被调用析构函数,它包含的所有Widget对象也都会被调用析构函数。又因为v是一个容器,如果在释放第一个元素时触发了异常,它也只能继续释放别的元素,否则会导致其它元素的资源泄露。如果在释放第二个元素的时候又触发了异常,那么程序同样会导致崩溃。

不仅仅是std::vector,所有STL容器的类甚至包括数组也都会像这样因为析构函数抛出异常而崩溃程序,所以在 C++ 中,不要让析构函数抛出异常!

但是如果析构函数所使用的代码可能无法避免抛出异常呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class DBConnection{                   //某用来建立数据库连接的类
public:
...
static DBConnection create(); //建立一个连接
void close(); //关闭一个连接,假设可以抛出异常
};

class DBConn{ //创建一个资源管理类来提供更好的用户接口
public:
....
~DBConn{ db.close(); } //终止时自动调用关闭连接的方法
private:
DBConnection db;
};


...{
DBConn dbc(DBConnection::create()); //创建一个DBConn类的对象
... //使用这个对象
} //对象dbc被释放资源

析构函数所调用的 close() 方法可能会抛出异常,那么有什么方法来解决呢?

吞掉异常

1
2
3
4
5
6
7
8
DBConn::~DBConn(){
try{
db.close();
}catch(...){
//记录访问历史
}
}

主动关闭程序

1
2
3
4
5
6
7
8
DBConn::~DBConn(){
try{
db.close();
}catch(...){
//记录访问历史
std::abort();
}
}

把可能抛出异常的代码移出析构函数

客户在需要关闭的时候主动调用 close() 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class DBConn{
public:
...
~DBConn();
void close(); //当要关闭连接时,手动调用此函数
private:
...
closed = false; //显示连接是否被手动关闭
};

void DBConn::close(){ //当需要关闭连接,手动调用此函数
db.close();
closed = true;
}

DBConn::~DBcon(){
if(!closed) //析构函数还是要留有备用,但不用每次都承担风险了
try{
db.close();
}catch(...){
//记录访问历史
//消化异常或者主动关闭
}
}
  • 析构函数绝对不要抛出异常。如果一个被析构函数调用的函数可能抛出异常,析
    构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提
    供一个普通函数(而非在析构函数中)执行该操作。

Item 7: Declare destructors virtual in polymorphic base classes.

析构函数声明为虚函数目的在于以基类指针调用析构函数时能够正确地析构子类部分的内存。 否则子类部分的内存将会泄漏,正确的用法如下:

1
2
3
4
5
6
7
8
9
// 基类
class TimeKeeper{
public:
virtual ~TimeKeeper();
...
};
TimeKeeper *ptk = getTimeKeeper(): // 可能返回任何一种子类
...
delete ptk;
  • polymorphic (带多态性质的) base classes 应该声明一个 virtual 析构函数。如果
    class 带有任何 virtual 函数,它就应该拥有一个 virtual 析构函数。
  • Classes 的设计目的如果不是作为 base classes 使用,或不是为了具备多态性
    (polymorphically) ,就不该声明 virtual 析构函数。

Item 6: Explicitly disallow the use of compiler-generated functions you do not want.

在C++中,编译器会自动生成一些你没有显式定义的函数。可以参考:了解c++默默编写并调用哪些函数
然而有时候我们希望禁用掉这些函数,可以通过把自动生成的函数设为 private 来禁用它或者在 c++11 中使用 delete 关键字。

比如我们禁用拷贝的功能:

1
2
3
4
5
6
7
8
9
10
class HomeForSale
{
public:
...

private:
HomeForSale(const HomeForSale &); // 只有声明
HomeForSale& operator=(const HomeForSale&) = delete// c++11

};

我们可以专门设计一个阻止copying 的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace noncopyable_ {
class noncopyable {
protected:
noncopyable() {}
~noncopyable(){}
/** C++11
noncopyable() = default;
~noncopyable() = default;
*/
private:
noncopyable(const noncopyable&);
noncopyable& operator=( const noncopyable& );
/** C++11
noncopyable( const noncopyable& ) = delete;
noncopyable& operator=( const noncopyable& ) = delete;
*/
};
}
1
2
3
4
5
6
7
8
9
10
class HomeForSale : private noncopyable_::noncopyable
{
};

HomeForSale p1, p2;
p1 = p2;

error: object of type 'HomeForSale' cannot be assigned because its copy assignment operator is implicitly deleted
p1 = p2;
^

Item 5: Know what functions C++ silently writes and calls

默认函数

C++ 中,一个类有八个默认函数:

1
2
3
4
5
6
7
8
9
10
class Empty {
Empty () {} //默认构造函数
Empty (const Empty &) {} // 默认拷贝构造函数
Empty (const Empty &&) {} // 默认移动构造函数(`C++11`)
~Empty() {} // 默认析构函数
Empty& operator=(const Empty&) {} // 默认重载赋值运算符函数
Empty& operator=(const Empty&&){} // 默认重载移动赋值操作符函数函数
Empty* operator &() {} // 默认重载取址运算符函数
const Empty* operator &() const {} // 默认重载取址运算符 `const` 函数
};

调用时机

只有你需要用到这些函数并且你又没有显示的声明这些函数的时候,编译器才会贴心的自动声明相应的函数。

引用成员

如果你打算在一个“内含引用成员”或者“内含const成员”的类内支持赋值操作,就必须定义自己的默认拷贝赋值操作符。因为 C++ 本身不允许引用改指不同的对象,也不允许更改 const 成员。

1
2
3
4
5
6
7
8
9
class Person {
public:
string & name;
Person(string &str):name(str) {}
};

string s1 = "hello", s2 = "world";
Person p1(s1), p2(s2);
p1 = p2;
1
error: object of type 'Person' cannot be assigned because its copy assignment operator is implicitly deleted

Item 4: Make sure that objects are initialized before they’re used.

手工初始化内置对象

为内置对象进行手工初始化,因为C++不保证初始化他们。

1
2
3
4
5
int x = 0;                                  //对 int 进行手工初始化
const char *text = "A C-style string"; //对指针进行手工初始化

double d;
std::cin >> d; //以读取 input stream 的方式完成初始化

构造函数最好使用成员初值列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class PhoneNumber { ... }

class ABEntry {
public:
ABEntry(const std::string &name, const std::string &address, const std::list<PhoneNumber> &phones);
private:
std::string theName;
std::string theAddress;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
}

ABEntry::ABEntry(const std::string &name, const std::string &address, const std::list<PhoneNumber> &phones) {
theName = name; //这些都是赋值
theAddress = address; //而非初始化
thePhones = phones;
numTimesConsulted = 0;
}

构造函数最好使用成员初值列,而不要在构造函数本体内使用赋值操作。初值列列出的成员变量,其排列次序应该和他们在class中的声明次序相同。

1
ABEntry::ABEntry(const std::string &name, const std::string &address, const std::list<PhoneNumber> &phones) : theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0) {}

local static 对象替换 non-local static 对象。

为免除”跨单元之初始化次序“问题,请以 local static 对象替换 non-local static 对象。

1
2
3
4
5
6
7
class FileSystem {
public:
...
std::size_t numDisks() const;
...
}
extern FileSystem tfs;
1
2
3
4
5
6
7
8
9
10
11
12
class Directory {
public:
Directory( params );
...
}

Directory::Directory( params)
{
...
std::size_t disks = tfs.numDisks();
...
}

客户使用使用:

1
Directory tempDir( params );

现在初始化次序的重要性体现出来了,除非 tfstempDir 之前先被初始化,否则tempDir的构造函数会用到尚未初始化的tfs。但tfstempDir是不同的人在不同的时间于不同的源文件建立起来的,它们是定义于不同编译单元内的 non-local static 对象。它们初始化相对次序并无明确定义。但我们可以将 local static 对象替换non-local static 对象来解决。这也是Singleton模式的常见实现手法。

这个手法的基础在于:C++保证,函数内的 local static 对象会在调用该函数时首次遇上该对象的定义式时被初始化。

1
2
3
4
5
6
7
class FileSystem { ... }

FileSystem& tfs()
{
static FileSystem fs;
return fs;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Directory { ... }

Directory::Directory( params)
{
...
std::size_t disks = tfs().numDisks();
...
}

Directory& tempDir()
{
static Directory td;
return td;
}

Item3: Use const whenever possible.

常量的声明

指针的常量声明:

1
2
3
4
5
char greeting[] = "Hello";
char* p = greeting; //non-const pointer, non-const data
const char* p = greeting; //non-const pointer, const data
char* const p = greeting; //const pointer, non-const data
const char* const p = greeting; //const pointer, const data

如果 const 出现在*左边,表示被指物为常量; 如果出现在*右边,表示指针自身为常量;如果出现在*两边,表示被指物和指针两者都是常量。

如果被指物是常量,const 放在类型之前和放在类型之后*之前表示的意义一样:

1
2
void f1(const Widget* p); //f1 获得一个指针,指向一个常量Widget对象
void f2(widget const *p); //f2 也是

STL的iterator 系以指针塑模出来,所以iterator的作用像个T*指针。如果希望指针是常量,可以声明为 const iterator,如果希望被指物为常量,需使用 const_iterator

1
2
3
4
5
6
7
8
std::vector<int> vec;
...
const std::vector<int>::iterator iter = vec.begin(); //iter的作用像个T* const
*iter = 10; //没问题,改变iter所指物
++iter;                            //错误,iter是const
std::vector<int>::const_iterator cIter = vec.begin(); //cIter的作用像个const T*
*cIter = 10; //错误,*cIter是const
++cIter; //没问题, 改变cIter

返回值声明为常量,可以降低代码被错误使用:

1
2
class Rational {...};
const Rational operator*{const Rational& lhs, const Rational& rhs};

当我们本来想做个比较,错误地输入=

1
if (a * b = c) ...

编译器就会报错误:不可给常量赋值。

const 成员函数

声明const 成员函数,是为了确认该成员函数可以作用与const对象,也使class接口比较容易理解,可以得知哪些函数可以改动对象内容,哪些不可以。

成员函数只是常量性不同是可以被重载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class TextBlock {
public:
...
const char& operator[](std::size_t position) const // operator[] for
{ return text[position]; } // const objects

char& operator[](std::size_t position) // operator[] for
{ return text[position]; } // non-const objects

private:
std::string text;
};

TextBlock tb("Hello");
const TextBlock ctb("World");
tb[0] = 'x'; // fine — writing a non-const TextBlock
ctb[0] = 'x'; // error! — writing a const TextBlock

bitsise constness 和 logical constness

bitsise constness: 成员函数只有在不改变对象的任何非静态成员变量时才可以被称为常量函数。也是C++对常量性的定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
class TextBlock{

public:
char& operator[](std::size_t position) const{
return pText[position];
}
private:
char* pText;
};

const TextBlock tb;
char *p = &tb[1];
*p = 'a';

在const和non-const成员函数中避免重复

constnon-const成员函数有着实质等价的实现时,令non-const函数调用const函数可以避免代码重复。不可以反着来。

1
2
3
4
5
6
7
8
9
10
const char& operator[](std::size_t position) const {
...
return text[position]
}
char& operator[](std::size_t position) {
return const_cast<char&>(
static_cast<const TextBlock&>(*this)
[position]
)
}
  1. *this 的类型是 TextBlock,先把它强制隐式转换为 const TextBlock,这样我们才能调用那个常量方法。
  2. 调用 operator[](std::size_t) const,得到的返回值类型为 const char&
  3. 把返回值去掉 const 属性,得到类型为 char& 的返回值。

Item 2: Prefer consts, enums, and inlines to #defines

我们先看看#deifne 有哪些的问题:

不利于调试

1
#define ASPECT_RATION 1.653

在预处理时候 ASPECT_RATION 可能就被移走了,ASPECT_RATION 没有进入 符号表, 运行此常量获得编译错误信息时, 可能会疑惑。因为这个错误信息总是提到 1.653,而不是ASPECT_RATION , 如果 ASPECT_RATION 定义不是自己写的头文件中,可能对 1.653 的来源毫无概念,将因追踪它浪费时间,解决之道是以一个常量替换上述宏 。

1
2
const double AspectRatio = 1.653 //大写名称通常用于宏
//因此这里改变名称写法

作为一个语言常量,ASPECT_RATION 肯定会被编译器看到,当然会进入记号表内。此外对于浮点常量(floating point constant)而言,使用常量可能比使用#define 导致较少量的码。

不重视scope

无法利用 #define 创建class专属常量。一旦宏定义,它就在其后的编译过程中有效(除非在某处 #undef )。而 const 可以。

1
2
3
4
5
6
class GamePlayer {
private:
static const int NumTurns; //常量声明式
int scores[NumTurns]; //使用该常量

}

enum 比 const 更好用

旧式编译器也许不支持上述语法, 它们不允许static在声明式上获得初值,此外所谓的“in-classs 初值设定”也只运行对整数常量进行, 如果编译器不支持上述语法,可以将初值放在定义式

1
2
3
4
5
class CostEstimate {
public:
static const double FudgeFactor; //staitc class 常量声明位于头文件内
}
const double CostEstimate::FudgeFactor = 1.35; //staitc class 常量定义位于实现文件内

如果使用emnu就很简单:

1
2
3
4
5
6
class GamePlayer {
private:
enum { NumTurns = 5 };

int scores[NumTurns]; //the enum hack
}

 

不易理解

1
2
3
4
5
#define CALL_WITH_MAX(a, b)  f((a) > (b) ? (a) : (b))

int a = 5, b =0;
CALL_WITH_MAX(++a, b);  //a被累加二次
CALL_WITH_MAX(++a, b + 10); //a被累加一次
  • 必须记住为宏的所有实参加上小括号
  • 在这里调用f之前,a的递增次取决与“它被拿来与谁比较”

更好的做法是使用 template inline 函数。

1
2
3
4
5
6
template <typename T>
inline void callWithMax(const T &a, const T &b)
{
return (a > b ? a : b);
}

Item 1: View C++ as a federation of languages

一开始,C++ 只是 加上一些面向对象特性,C++ 最初的名称 C with Classes 也反映了这个血缘关系。现在这个语言逐渐成熟,已经是一个多重泛型编程语言(multiparadigm programming language)。同时支持过程形式(procedural)、面向对象形式(object-oriented)、函数形式(functional)、泛型形式(generic)、元编程形式(metaprogramming)

C++ 视为一个由相关语言组成的联邦而非单一的语言。

C++ 主要4个子语言:

  • C。说到底C++仍是以C为基础。许多时候C++对问题的解法其实不过就是较高级的C的解法如item2item13。当只使用C++C的那部分语法, 会发现C语言的缺陷:没有模板、没有异常、没有重载。
  • Object-Oriented。面向对象程序设计也是C++的设计初衷:构造与析构、封装与继承、多态、动态绑定的虚函数。
  • Template C++。这是C++的泛型编程部分,大多数程序员经验最少的部分。TMP模板元编程template metaprogramming)也是一个新兴的程序设计范式。
  • STLSTL是一个特殊的模板库,它将容器、迭代器和算法优雅地结合在一起。

C++ 程序设计的惯例并非一成不变,而是取决于你使用 C++ 语言的哪一部分。例如, 在基于C语言的程序设计中,基本类型传参时传值比传引用更有效率。 然而当你接触 Object-Oriented C++ 时会发现,传常量指针是更好的选择。运用Template C++时尤其如此,因为彼时你甚至不知道所处理的对象的类型。 但是你如果又碰到了STL,其中的迭代器和函数对象都是基于C语言的指针而设计的, 这时又回到了原来的规则:传值比传引用更好。