Effective Cpp 学习笔记

学习程序语言根本大法是一回事,学习如何以某种语言设计并实现高效程序则是另一回事。这种说法对C++尤其适用。

Chapter 0 术语

声明式:告诉编译器某个东西的名称和类型,但略去细节。

1
2
3
4
5
6
extern int x; // 对象声明
std::size_t numDigits(int number); // 函数声明
class Widget; // 类声明

template<typename T>
class GraphNode; // 模板声明

每个函数的声明揭示其签名式(signature),也是参数返回类型

定义式:提供给编译器声明式遗漏的细节;

  • for object:编译器为此对象分配内存的位置;
  • for function | template:提供代码本体;
  • for class | template:列出成员;

初始化:“给予对象初值”的过程。

关键字explicit可以被用在自定义类型的构造函数前,可以阻止构造函数被用来执行隐式类型转换(但仍可进行显示类型转换)

1
2
3
4
5
6
7
8
9
Class B{
public:
explicit B(int x = 0, bool b = true);
}
// --------
void doSomething(B object);

doSomething(28); // ❌ 不可以进行隐式类型转换
doSomething(B(28)); // ✔ 可以显式类型转换

Chapter 1 View C++ as a federation of languages

一开始,c++只是c加上一些面向对象的特征,所以最初叫”C with Classes”

今天的C++已经是个多重泛型编程语言(multiparadigm programming language)

最简单的方法是将C++视为一个由相关语言组成的Federation而非单一语言,在每个次语言中,各种守则都倾向于简单、直观易懂。

一共四个:

  1. C:说到底C++仍是以C为基础的语言,但也反映出C语言的局限性:没有模板、异常、重载等,面向过程
  2. Object-Oriented C++:这部分是C with Class追求的。引入了类的概念、封装、继承、多态、虚函数(动态绑定)等。
  3. Template C++:这部分是C++的泛型编程部分。由于templates威力强大,他们带来崭新的编程范型“模板元编程”
  4. STL:template程序库

Chapter 2 Prefer consts, enums, and inline to #defines

原则:以编译器替换预处理器,因为#define不被视为语言的一部分,因此它定义的内容不会进入符号表,因此错误信息中也不会有记录。

const/enums替换define中两种值得提及的情况:

  1. 定义常量指针

    const char* const authorName = "Scott Meyers";

    因为常量定义通常被放在头文件里,因此可能会被多个文件访问到,所以有必要将指针(而不是指向的内容)声明为const;若还要将指向的内容也定义为const,那么需要写两次const

  2. 定义class专属常量

    为了将常量作用域限制于class内部,必须让他作为一个成员;

    而为了保证此常量最多只有一份实体,必须让成为一个static成员

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

​ 在这里NumTurns 是一个声明而非定义式。有可能不被旧编译器支持,他们不允许static成员在其声明式上获得初值,in-class 初值设定只允许对整数常量进行(包括int、char、bool),那么可以:

1
2
3
4
5
6
7
class CostEstimate{
private:
static const double FudgeFactor; // 声明位于头文件内
...
};
const double
CostEstimate::FudgeFator = 1.35; // 定义位于实现文件内

enum hack

如果你在class编译期间一定需要一个class常量值, 例如GamePlayer中编译器一定坚持必须在编译期间就知道数组的大小,可以采用enum hack方法:

1
2
3
4
5
6
class GamePlayer{
private:
enum{ NumTurns = 5};
int scores[NumTurns]; // 使用常量
...
};
  1. enum hack的行为某些方面比较像#define而不是const,例如取一个const的地址是合法的,但是取一个enum/#define的地址通常不合法
  2. enum hack是模板元编程的基础技术(见C48)

inline替换define:

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 = ?

改成具有可预测行为和类型安全的template inline函数:

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

Chapter 3 Use const whenever possible

const 出现在星号*左边时,表示被指向的对象是常量,如果出现在*右边,则说明指针自身是常量,如果出现在两边,则表示被指对象和指针都是常量;

所以下面两种写法是一样的:

1
2
void f1(const Widget* pw);
void f2(Widget const * pw);

const成员函数,让函数返回一个常量值,可以有效避免诸如if( (a*b) = c ) 这种错误(编译器报错)

non-const成员调用const成员函数并转型可以避免代码重复

Chapter 4 Make sure that objects are initialized before they’re used

  • 内置类型:手动完成:
1
2
3
4
5
int x = 0;
const char* text = "A C-style string";

double d;
cin >> d;
  • 非内置类型:构造函数——确保每一个构造函数都将对象的每一个成员初始化。注意,C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前,即初始化列表中;
  • 父类总是先于子类被初始化;class成员变量以其声明次序被初始化(即使他们在初始化列表中的位置可能不同)
  • non-local static对象的初始化:

函数内的static对象称为local static对象,其他(包括global、定义在namespace中、classes中、file作用域内被声明为static的对象)称为non-local static对象。

如果一个编译文件里的某个non-local static对象初始化动作使用了另一个文件中的non-local static对象,此时他用到的这个对象可能尚未被初始化。

解决方法:将non-local static对象放进它的专属函数中,也就是将non-local static替换为local static对象,因为C++保证:函数内的local static对象会在“该函数被调用期间,首次遇上该对象的定义式”时被初始化。这也是单例模式(Singleton)的一个常见实现手法。

Chapter 5 Know what functions C++ silently writes and call

C++中一个空类默认包含:

  • 空的构造函数
  • copy构造函数
  • non-virtual析构函数
  • copy assignment操作符(=)

只有当这些函数被调用时,他们才会被编译器创建出来

1
2
3
4
Empty el; // 默认构造函数
Empty e2(e1); // copy构造函数
e2 = e1; // assignment操作符
// 析构函数

默认的copy行为是将对象的所有非static变量拷贝到目标对象。

Chapter 6 Explicitly disallow the use of compiler-generated functions u do not want

  • 阻止copy行为:声明copy函数为private但不实现
  • 进一步:定义一个Uncopyable基类,并将有“阻止copy行为”需求的类作为它的private派生类
1
2
3
4
5
6
7
8
9
10
11
12
class Uncopyale{
protected:
Uncopyable(){}
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&);
Uncopyable& opeartor= (const Uncopyable&);
};

class HomeForSale: private Uncopyalbe{
///...
};

Chapter 7 Declare destructors virtual in polymorphic base classes

  • 通过为基类声明virtual析构函数,来让base class指针控制的derived对象正确销毁。
  • 当class不被用来作为base class时,不要用virtual析构函数。因为virtual导致在class中增加vptr指针指向vtbl,导致占用空间增大
  • 只有当class内至少含有一个virtual函数,才为他添加virtual析构函数
  • 任何带多态性质的base class,应该为其声明一个virtual析构函数

Chapter 8 Prevent exception from leaving destructors

  • 不要让异常跑出析构函数
  • 可能会引发两个未被处理的异常同时存在,例如:
1
2
3
4
5
void doSomething(){
vector<Widget> v;
...
// 假设Widget析构会抛出异常,那么此时会有多个异常被抛出,导致不确定行为
}
  • 两个方法处理该问题:
    • 在析构函数中处理异常,遇到异常就在catch中调用abort()结束程序,“抢先制不确定行为于死地”;
    • “吞下”异常;在catch中记录该次调用失败。后续可以交由用户在普通函数中进行处理。

Chapter 9 Never call virtual functions during construction or destruction

  • derived class对象的base class构造期间调用的virtual函数是base class中的virtual函数,因为此时derived class的成员变量还没有被构造;此时virtual被调用的实际行为并不符合virtual函数被期望的行为(实现多态),但是编译器不会报错。

  • 相同的道理适用于析构函数,一旦derived class的析构函数开始执行,其对象内所有的成员变量都应视为未定义值

  • 解决方法:

    • 将要调用的函数声明为non-virtual;
    • 从derived class的构造列表中传值给base class的构造函数
    • 使用private static函数生成要传的值,比较可读,也保证了使用的变量在使用时已经被初始化完毕(因为仅能使用static变量)

Chapter 10 Have operator= return a reference to *this

  • 字面意思,令赋值操作符返回一个指向当前对象的引用
1
2
3
4
Widget& operator=(const Widget& ths){
...
return *this;
}

以实现类似于x = y = z = 15 的操作

Chapter 11 Handle assignment to self in operator=

  • 当出现a[i] = a[j]i == j,即自我赋值时,可能会出现不安全现象:
1
2
3
4
5
Widget& Widget::Operator= (const Widget& rhs){
delete pb;
pb = new Bitmap(*rhs.pb); // 这里,&rhs可能是this,此时pb已经被释放了
return *this;
}
  • 解决方法:

    • identity test 证同测试:if(this == &rhs) return *this; 缺点:引入了新的控制流分支,降低执行速度;
    • 另外一个思路是,通过“消除异常安全性”同时保证“自我赋值安全性”——在编写代码时就考虑到&rhs是否可能是this这个问题,从而正确安排代码顺序;
    • 采用Chapter 29提到的copy and swap技术:
    1
    2
    3
    4
    Widget& Widget::Operator= (const Widget rhs){ // 传入副本
    swap(rhs); // 将副本与当前对象进行数据交换
    return *this;
    }

Chapter 12 Copy all part of an object

如果你声明自己的copying函数,意思就是告诉编译器你并不喜欢默认实现中的某些行为。编译器仿佛被冒犯似的,会以一种奇怪的方式回敬:当你的实现代码几乎必然出错时却不告诉你:)

  • copying函数 == copy构造函数 + copy assignment操作符
  • 当自定义copying函数时,如果缺少成员变量,编译器不会报错;因此注意要复制所有local成员变量
  • 尤其是当编写derived class的copying函数时,必须也复制其base class的成分(通过调回base class的copying函数)
  • 如果copy构造函数和copy assignment操作符有相近的代码,那么就可以从中抽出一个函数给二者调用。

资源管理

所谓资源,就是那些使用之后必须还给系统的东西。如果不这样,糟糕的事情就会发生。

C++中最常见的资源就是动态分配内存,包括文件描述符、互斥锁、图形界面中的字型和笔刷、数据库连接、网络sockets。无论哪种资源,重要的是,当你不再使用他,必须将他还给系统。

Chapter 13 Use Object to manage resources

  • 获得资源后立即放入对应的管理对象内(Resource Acquisition Is Initialization RAII
  • 管理对象运用析构函数确保资源被正确释放
  • 常用的RAII classestr1::shared_ptrauto_ptr
  • 智能指针被销毁时会自动删除它指向的对象,所以不要让多个智能指针指向同一个对象
  • 为了解决上述问题,智能指针有如下特性:若通过copying函数复制它,它本身会变成null,而复制得到的指针将得到原资源的唯一拥有权:
1
2
3
4
auto_ptr<Investment>
p1(createInvestment()); // p1指向工厂函数createInvestment返回的对象
auto_ptr p2(p1); // p2指向该对象,p1设置为null
p1 = p2; // p1指向该对象,p2设置为null
  • STL容器要求元素发挥“正常的”复制行为,因此不允许容器类型为auto_ptr

  • shared_ptr采用引用计数追踪对象被引用的情况,当无人指向该对象时自动删除该资源

  • auto_ptrshared_ptr都是在析构函数中做delete而不是delete [],因此不能在动态申请的数组身上使用二者

Chapter 14 Think carefully about copying behavior in resource-managing classes

  • 当一个RAII对象被复制时,几种常见的行为:
    • 禁止复制
    • 引用计数(使用shared_ptr
    • 复制底层资源
    • 转移资源拥有权

Chapter 15 Provide access to raw resources in resource-managing classes

  • 在资源管理类中封装并提供对原始资源的访问
  • 例如shared_ptrauto_ptr都提供一个get成员函数,返回一个指向指针内部的原始指针(显示转换):
1
2
auto_ptr<Investment> p1(createInvestment());
Investment* ptr = p1.get();

当然也可以通过指针取值操作符(-> / *)隐式转换至底部指针

1
2
3
4
5
6
7
class Investment{
public:
bool isTaxFree() const;
...
}
bool taxable = !(p1->isTaxFree());
bool taxable2 = (*p1).isTaxFree();

自定义隐式转换函数:

1
2
3
4
5
6
7
8
9
10
class Font{
public:
...
operator FontHandle () const{
return f;
}
...
private:
FontHandle f;
}

Chapter 16 Use the same form in corresponding uses of new and delete

  • 因为对象数组和单一对象的内存空间结构不同,导致了delete [] 和delete的行为不同
  • 因此new一个数组时需要用delete[ ],单个对象使用delete;交错使用的行为都是不可预测的
  • 警惕typedef:尽量不要对数组形式做typedef,否则容易引发用户不正确的delete;

Chapter 17 Store newed objects in smart pointers in standalone statements

  • 使用智能指针时,需要用独立的语句将对象置入:
1
2
3
4
5
6
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);

processWidget(new Widget, priority()); // 错误 不能通过编译 因为Widget的构造函数声明为了explicit,导致返回的Widget不能通过隐式转换转换成std::tr1::shared_ptr<Widget>类型
processWidget(
std::tr1::shared_ptr<Widget>(new Widget), priority()); // 可以通过编译,但仍不推荐,会导致难以察觉的泄漏

上述情况下,第一实参由两部分组成:

  • 执行new Widget表达式
  • 调用shared_ptr 构造函数

那么processWidget之前,编译器执行了如下三件事:

  • 计算第一参数
    • 执行new Widget表达式
    • 调用shared_ptr 构造函数
  • 计算第二参数
    • 调用priority

编译器只保证参数内部的计算顺序,不能保证计算各个参数的顺序,因此实际执行时可能会变成:

  • 执行new Widget表达式
  • 调用priority
  • 调用shared_ptr 构造函数

此时,如果调用priority时抛出异常,那么之前申请的空间就会发生泄漏。为了避免上述状况的出现:

1
2
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());

这样才是最安全的。因为编译器对跨越语句的操作没有重新排列的自由

# c++
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×