Effective C++ 笔记一

导读部分有这么一句话”这本书的目的是告诉你如何高效的运用 C++”.然而,我个人觉得这本书其实也在回答另一个问题: “如何高效的学习 C++.”

这是关于《effective C++》这本书籍的第一篇文章,在记录书中的前三个章节内容的同时,也加入了一些自己的思考,同时还用 STL 源码为例,来”印证”条款的正确性.

effective C++ 算是 关于 C++ 的经典书籍了,这本书要告诉你的是写出高效代码时应当遵循的准则,而不是带着你钻研各种技术细
节,看看那种实现方式更为高效. 要写出高效的代码,显然需要对于 C++ 的源代码有一定的理解.有趣的是通过这篇文章,通过阅读 STL 的源代码你会发现 STL 在一定程度上也遵循着书中的条款.

一. 让自己习惯 C++

第一章的标题是:让自己习惯C++. 什么叫做“习惯”呢?
说到 C++大部分人都会联想到其他的语言,将 C++ 跟其他语言进行比较,看看谁优谁劣。
大部分的程序员心中都有这样的一个语言次序 → 先是机器语言、汇编语言、C语言、然后是面向对象语言JAVA,C++,PYTHON….

1.视 C++ 为一个语言联邦

C++ 在 C 的基础上加入了一些面向对象的特性( c with classes),并不断的扩展,加入了模板,异常等。
最简单理解 C++ 的方式是将 C++ 视为一个相关语言的组成的联邦,而不是将其理解为单一的语言.

C++ 可以理解为 由 C 语言、Object-Oriented C++、Template C++、STL 的集合.

  • C
    C++ 支持 C 的语法,例如你可以用 C语言的 printf 函数打印东西。
    很多 C++ 的底层其实是用C 语言实现的,例如STL中的迭代器常常借助指针来实现.
  • Object-Oriented C++
    C++ 在 C 的基础上引入了面向对象的思想,这使得 C++ 拥有与其他面向对象语言的部分特性.
    例如,构造函数、析构函数、封装、继承、多态、虚函数….等等
  • Template C++
    这是 C++ 的泛型编程部分。
  • STL
    STL 是个 template 程序库,包含容器、迭代器、算法、迭代器、函数对象、适配器。
    值得一提的是 STL 并不是按照面向对象的思想设计出来的…..

2.尽量用const,enum,inline替代 #define

  • 对于单纯常数,最好用const 或者enums替代#define
  • 对于形如函数的宏,最好用inline函数替代#define

    在STL中仍有很多使用 #define的地方

1
2
3
4
5
6
//algorithm
#include <stl_algobase.h>
#include <stl_construct.h>
#include <stl_tempbuf.h>
#include <stl_algo.h>
#endif /* __SGI_STL_ALGORITHM */

3.尽可能使用const

const能够指定一个语言约束,而编译器会强制执行这种约束。它允许你告诉编译器和其他程序员某些值不应该被改变。

const能够被用来修饰全局、namespace作用域内的变量,或者修饰文件、函数、或区块作用域中被声明为 static 的对象。也能以用来修饰classes内部的static 和 non-static 成员变量。
值得一提的是,const 的以下用法

1
2
3
char greeing[] = "Hello";
const char* const p = greeting;
//星号右边的const关键字,表示指向的物体是常量,而星号左边的const表示指针自身是常量

在STL源代码中,我们也常常能够看见const的身影

1
2
3
4
// vector 容器中的size函数,借助cosnt声明这个函数不应该尝试改变任何值
size_type size() const { return size_type(end() - begin()); }
// const迭代器
typedef const value_type* const_iterator;

然而 使用const 时,我们也不能高枕无忧了,编译器遵循的是”bitwise const”.
下面的程序片段能够很轻松的通过编译器的检测,const说明不应该有值被改变,而实际情况也是这样的.

1
2
3
4
5
6
7
8
9
class CTextBlock{

//重载[],返回字符串某一个位置的引用
char& operator[](size_t pos) const{
return s[pos];
}
private:
cahr* s;
}

然而,程序返回了一个引用,这使得能够很简单的改变 s 其中某一位处的值,

1
2
3
const CTextBlock c("hello");
char* pc = &[0];
*pc = 'J';

我们创建了一个const对象,调用了一个const函数,但最终我们还是允许客户端改变它的值.

将operator[]函数的类型声明为const char& 就能避免出现上面的错误。

一个良好的程序应该在编译阶段,尽可能的避免出现错误。

1
2
3
4
5
6
7
class Rational{...}
const Rational operator* (const Rational* lhs, const Rational* rhs);
//返回类型是const使得下面的程序在编译阶段就通不过
Rational a, b, c;
if(a * b = c)
....
//条款28指出,一个良好的用户自定义的类型应该避免无端与内置类型不兼容

4.确定对象使用前,应该被初始化

这一点不仅仅应该在 C++ 中才应该被强调,在任何一门语言中使用未初始化的对象,常常会带来意想不到的“效果”.

内置类型以外的其他东西,初始化的责任落在了构造函数上.

并且,你还应当能区分赋值(assignment) 和 初始化(initialization)

1
2
3
4
5
6
7
8
class A{
public:
A(const string& name) {
data = name; //这是赋值,而不是初始化
}
private:
string data;
}

C++ 规定:对象的成员变量的初始化动作发生在进入构造器之前,事实上甚至发生在默认的构造器之前。

C++ 提供了所谓的 成员初值列(member initialization list)来替换赋值动作.

1
2
A::A(const string& name):data(name)//初始化
{} //

第一个版本首先会调用默认构造函数为data设初值,然后在进行赋值。
第二版本中初值列中对应的参数被作为了各成员变量构造函数的实参.(进行了copy构造)
使用成员初值列通常会带来更高的效率。

二、构造、析构、赋值运算

第一章可以被理解为 学习C++的一些“规范性”的概念.这些规范性的概念使得你能够将“C思维”转变成“C++思维”.(并不是面向过程到面向对象). 而第二章则是帮助我们理解,类的“入口”和“出口”.

5.C++ 会帮你调用哪些函数

如果你没有声明,C++ 会为你声明(编译器版本)一个cop构造函数、copy assignment 操作符、和一个析构函数.
如果你没有声明任何的构造函数,编译器也会为你声明一个 default 构造函数。

1
2
3
4
5
6
7
8
9
class Empty{}
//等价于:
class Empty{
pubblic:
Empty(){} //default构造函数
Empty(const Empty& rhs){...}//copy构造函数
Empty(){...}//析构函数
Empty& operator=(const Empty& rhs){...} //copy assignment 操作符
}

值得注意的是编译器会在程序需要时,才会创造出上述成员函数。

1
2
3
Empty e1;//default 构造函数、析构函数
Empty e2(e1);//copy 函数
e2 = e1; //copy assignment 操作符

6.禁止编译器生成默认函数

当我们不想编译器为我们生成默写成员函数时,我们可将相应的成员函数声明为private,并且不予实现。

1
2
3
4
5
6
class HomeforSale{
private:
//只声明,不实现
HomforSale(const HomeForWSale&);
HomeForSale& operator=(const HomeForWSale&);
}

但 member 函数 和 friend 函数仍能调用这些模板(没有实现、连接器会报错)
一种可选的替代方法是:定义一个uncopyable base 类

1
2
3
4
5
6
7
8
9
10
class Uncopyable{
protected:
Uncopyable(){} //能构造和析构
~Uncopyable(){}

private:
Uncopyable(const Uncopyable&); //不实现,但阻止 copying
Uncopyable& operator=(const Uncopyable&);
}
class Derived : public Uncopyable{} //编译器不在默认构造 copy函数和 copy assign 分配符

7.为基类声明 virtual 析构函数

析构函数的运行方式是: 最深层派生的析构函数最先被调用,然后依次是上一层 base 类的析构函数.

”virtual 析构函数,应该在带有多态性质的基类中提供“
“如果一个类带有任何的 virtual 函数,那么他就应该virtual函数”
不难看出,第二条是第一条的特例,如果一个类带有 virtual 函数,那么很显然这个类一开始就是被设计成基类。

这条条款,基于以下的事实(以避免出现这样的问题):
当 派生类对象经由基类删除时,派生部分可能并没有被”删除“(调用的是基类的析构函数,派生类的析构函数未能被调用)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Base{
public:
Base(){}
~Base(){
cout << "Base"<<endl;
}
};
class Derived : public Base{

public:
Derived(){
}
~Derived() {
cout << "Derived" <<endl;
}
};
int main() {
Base* b = new Derived();
delete b; //输出结果是Base
getchar();
return 0;
}

要避免”局部销毁“的现象,只需要将基类的析构函数声明为 virtual 即可.

STL容器类的析构函数,并不是 virtual 的,所以继承这些类是个”坏主意”。
在特殊情况下,可以将析构函数声明为 pure virtual 函数,以使得该基类不能被实现.(但要提供实现)
盲目的使用 virtual 也是是错误的,因为这并不是没有代价的,系统要为此付出维护一个指针的代价才能实现 virtual

8.别让析构函数抛出异常

析构函数中如果出现异常,可能会出现内存泄露的问题.

1
2
3
4
5
6
class Widget{
~Wif=dget(){...}//可能抛出异常
void doSomthing(){
std::vector<Widget> v;
}//退出作用域 v 销毁、析构过程中如果抛出异常,之后元素的“析构”是不可预测的
}

如果析构函数中的操作,一定要抛出异常,应该用 try {} catch语句捕获异常.

1
2
3
4
5
6
7
8
9
10
11
class Widget{
~Wif=dget(){
try{...
}catch(...) {
//报告或者隐藏消息
}
}
void doSomthing(){
std::vector<Widget> v;
}//退出作用域 v 销毁、析构过程中如果抛出异常,之后元素的“析构”是不可预测的
}

一个更佳的策略是:重新设计出口,让用户负责处理将可能抛出异常。其他函数抛出异常比析构函数抛出异常更好,因为析构函数抛出异常总是会带来“过早结束程序、或发生不确定行为”的风险

9.不要在构造函数和析构过程中调用 virtual 函数

派生类对象内的基类成分会在派生类被构造之前被构造完成。In other words,基类的构造函数会先于派生类被调用.

然而,如果在基类构造函数中调用 virtual 函数,那么实际上是调用的基类内的版本,而不是派生类实现的版本.

10.令 operator= 返回一个 reference to *this

为实现”连锁赋值”,赋值操作符必须返回一个 reference 指向,操作符的左侧参数.(这是一个必须协议)

1
2
3
4
5
6
7
8
class A{
public:
...
A& operator(const A& rhs) {
...
return *this;
}
}

其他赋值运算(例如,*=、+=也适用)
这只是一个协议,并不一定要严格遵守,但”定制类型应该与内置类型行为一致“

STL中的list这部分的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    list<T, Alloc>& list<T, Alloc>::operator=(const list<T, Alloc>& x) {
if (this != &x) {
iterator first1 = begin();
iterator last1 = end();
const_iterator first2 = x.begin();
const_iterator last2 = x.end();
while (first1 != last1 && first2 != last2) *first1++ = *first2++;
if (first2 == last2)
erase(first1, last1);
else
insert(last1, first2, last2);
}
return *this;
}

11.在 operator= 中处理”自我赋值”

这条建议的目的在于避免出现”自我赋值的“情况.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Class Widget{}
Widget w;
w = w; // 显示 自我赋值

class Base{}
class Derived : public Base{}
void doSomething(const Base& rb, Derived* pd); //rb 和*pd可能其实是同一个对象
//STL中list源代码
list<T, Alloc>& list<T, Alloc>::operator=(const list<T, Alloc>& x) {
if (this != &x) { //进行”认同测试“
iterator first1 = begin();
iterator last1 = end();
const_iterator first2 = x.begin();
const_iterator last2 = x.end();
while (first1 != last1 && first2 != last2) *first1++ = *first2++;
if (first2 == last2)
erase(first1, last1);
else
insert(last1, first2, last2);
}
return *this;
}

12.复制对象时,要复制每一个成分

只要你承担起”为派生类撰写copying函数”的重任时,你必须小心的复制其基类中的成员.(这些成员往往是private的),此时,你应该让derived class的copying函数调用对应的基类中的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Base{
public:
Base(const Base& rhs);
Base& operator=(const Base&rhs);
private:
string name;
};
Base::Base(const Base& rhs):name(rhs.name){}
Base& Base::operator=(const Base& rhs){
name = rhs.name;
return *this;
}
Class Derived : public Base{

public:
Derived(const Derived& rhs);
Derived& operator&(const Derived& rhs);
private:
string pwd;
}
//错误版本
Derived::Derived(const Derived& rhs):
pwd(rhs.pwd){}
Derived& operator&(const Derived& rhs){
pwd= rhs.pwd;
return *this;
}
//正确版本
Derived::Derived(const Derived& rhs):
pwd(rhs.pwd),Base(rhs){}
Derived& operator&(const Derived& rhs){
Base::operator=(rhs);
pwd= rhs.pwd;
return *this;
}

三、资源管理

13.以对象管理资源

把资源放进对象内,我们便可依赖 C++ 的“析构函数自动调用机制”确保资源被释放

合理的使用 auto_ptr 、shared_ptr 进行资源的管理.

不过用智能指针析构函数中使用的是 delete ,而非delete[],因此用其管理动态分配的array(string[10])是一个坏主意

14.在资源管理类中小心copying行为

shared_ptr允许指定所谓的“删除器”(一个函数或是一个函数对象),当引用为 0 时,便调用。auto_ptr则不提供该能力,当引用为 0 时便直接删除其指针.
使用方式形如:

1
shared_ptr<int> p(raw指针, 删除器);

15.在资源管理类中提供对原始资源的访问

条框13指出,借助智能指针对资源进行管理.但我们有时候需要将 RAII 对象,转变成原 对象.

  • 智能指针都提供一个 get成员函数,来执行显式转换,也就是返回raw pointer 的副本.
  • 智能指针也重载了 operator-> 和 operator* ,允许隐式转换至raw pointer.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Investment {
    public:
    bool isTaxFree() cosnt;
    ...
    };
    Investment* createInvestment();//工厂方法
    shared_ptr<Investment> pill(createInvestment()); //用智能指针管理一笔资源
    bool taxablel = !(pil->isTaxFree());
    bool taxable2 = !((*pil).isTaxFree());

16.new 和delete要采用相同的形式

会导致内存泄露的问题

17.以独立语句将 newed 对象置入智能指针

shared_ptr的构造函数需要一个原始指针,但shared_ptr 的构造函数被声明为 explict, 所以无法进行隐形转换.

1
2
3
4
5
6
7
8
9
10
11
int priority();
void processWidget(shared_ptr<Widget> pw, int priority);
//通不过编译
processWidget(new Widget, priority());//调用
//能通过编译,但容易导致资源泄漏(prioruty调用异常,new 指针遗失)(可能按以下的顺序执行:
//“new Widget”、调用priority、调用shared_ptr构造函数)
processWidget(shared_ptr<Widget>(new Widget
), priority());
//安全方式
shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());