Effective C++ 笔记。

一.习惯使用C++

1.C++有不同的特性

C++是个多重泛型语言,支持面向过程,面向对象,函数,泛型,元编程的语言。理解C++最简单的方法是将其视为多个次语言构成的集合。在使用C++时,可能需要从一个次语言迁移到另一个次语言,在这个过程中,守则可能改变。主要的次语言只有四个:

  • C
  • Object-Oriented C++
  • Template C++
  • STL

从这四个次语言相互切换时,切换守则来进行高效编程是必要的。

2.使用enum,const,inline替换#define

define的部分可能不被视为语言的一部分,名称可能被预处理器处理,而编译时没有出现,没有进入符号表,这会导致当该宏定义的量发生错误时,编译错误信息可能提到的不是宏定义的名称,而是名称对应的值。如果宏定义在一个非自己所写的头文件中,遇到这种错误就很难定位到错误的位置。因此,最好用常量来替换宏定义。

1
const int maxN = 1e6+1;

以常量替换宏定义,有两种特殊情况:

  • 定义常量指针,有必要将指针声明为const,例如一个常量的char*-based字符串需要定义为const char* const name = “Y”。
  • 类中的专属常量,为了保证所有对象只有一份该常量,需要声明为static成员。

static const成员可以在类声明中直接给出值并使用,也可以在类的实现文件中单独给出其定义。如果是static成员而不是常量,则需要在实现文件中给出定义。

声明时初始化. 对于静态成员不能在类声明初始化的注解:非常量的静态成员是强符号,如果在类中设定初值,每个对象都包含该成员,多文件可能就会出现强符号重复定义;如果定义为常量,则为弱符号,如果不取地址,可能不会分配空间,直接在汇编指令中使用立即数代替,如果取地址,就又会变成强符号,多文件可能产生重复定义的错误。
1
2
3
4
5
6
7
/*非常量静态成员定义与声明分开*/
//student.h
class student{
static int num;
};
//student.cpp
static int student::num = 0;

对于一些编译器,可能不支持static const成员在类声明中设定初值,可以使用enum代替,enum不可以获取地址,许多代码都使用enum作为类的常量:

1
2
3
class student{
enum {num = 5}; //令num成为5的一个记号名称
};

宏定义的另一个问题是宏定义函数,由于宏定义只是简单的替换,错误传入参数可能带来的结果完全不同,因此最好使用inline内联函数替换函数形式的宏定义。

总结:

  • 使用单纯常量,最好使用const或enums,而不是#defines
  • 形式函数的宏,最好使用inline函数替换

3.尽可能使用const

如果关键字const出现在左侧,表示被指物是常量,如果在\右侧,则指针是常量。

  • 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何对象,函数参数,函数返回类型,函数本题。
  • 编译器强制实施bitwise constness(通过赋值语句判断是否为const函数),但编写程序应该使用概念性的常量性(例如如果成员有指针,那么指针指向的内容也不被修改,才是具有常量性)。
  • 当const和non-const成员函数有着实质等价的实现时,令non-const调用const版本可避免代码重复。
1
2
3
4
5
6
7
8
9
10
class line{
const char& operator[](std::size_t pos) const{
...;
}
char & operator[](std::size_t pos){
return const_cast<char&>(
static_cast<const line&> (*this)[pos]
);
}
};

4.确定对象被使用前已被初始化

  • 为内置型对象进行手工初始化。
  • 构造函数最好使用成员初值列,而不要在构造函数本体内使用赋值操作,且排列顺序应该和声明顺序相同。
  • 使用local static对象替换non-local static对象。

二.构造/析构/赋值

1.了解C++提供了哪些函数

  • C++默认为类提供无参构造函数,拷贝构造函数,赋值运算符,析构函数。

2.如果不想使用编译器自动生成的函数,应明确拒绝

  • 为驳回编译器自动生成函数,可以将相应的成员函数声明为private并不予实现。或者使用一个uncopyable基类,在这个基类将一些成员函数声明为private并不予以实现,然后继承这个类。

3.为多态基类声明virtual析构函数

一般情况下使用多态,总是会使用基类的指针指向一个派生类,然而如果使用基类指针释放空间,可能会导致派生类的成员的空间没有释放,因为派生类的析构函数没有被调用。正确的做法是:

  • 带有多态性质的基类应该声明一个virtual析构函数,如果类有任何virtual函数,就应该有一个virtual析构函数,并且应该提供实现。
  • 类的设计目的如果不是作为基类或不是为了实现多态,就不该声明virtual析构函数。

4.别让异常逃离析构函数

  • 析构函数不应该产生异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该能捕捉任何异常,然后不传播异常或结束程序。
  • 如果客户需要对某个操作函数抛出的异常作出反应,那么类应该提供一个普通函数而不是在析构函数中进行操作。

5.不在构造函数和析构函数中调用virtual函数

  • 在构造和析构期间不要调用virtual函数,因为这类调用不下降到derived class。

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

7.在operator=中处理自我赋值

别名(使用指针,数组下标)可能会导致潜在的自我赋值,必须避免这种情况的发生。

  • 确保当对象自我赋值时有正确的行为,其中的技术包括比较来源对象和目标对象的地址,调整语句顺序(最后删除原对象的内容),copy-and-swap。
  • 确定任何函数操作一个及以上的对象,其中多个对象是同一个对象时,行为正确。

8.复制对象时别忘记任何一个成分

  • 拷贝函数应该确保复制对象内的所有成员变量及所有base class成分。
  • 不要尝试以某个拷贝函数实现另一个拷贝函数。应该将共同的部分放进第三个函数中,共同调用。

三.资源管理

1.以对象管理资源

  • 为防止资源泄露,应该使用资源管理类,他们在构造函数中获得资源并在析构函数中释放资源。
  • 两个常被使用的对象是智能指针shared_ptr和auto_ptr。auto_ptr指向资源的所有权只能有一个指针持有,如果进行赋值,原指针会被设为null,而shared_ptr会进行计数,可以多个指针指向一个资源,最后释放。

2.资源管理类的复制行为

  • 复制资源管理类对象必须一并复制所管理的资源,资源的复制行为决定资源管理类对象的复制行为。

  • 通常对于资源管理类的复制行为是:

    • 禁止复制
    • 使用引用计数法:内含一个shared_ptr指针,并以期望的资源释放函数作为删除器
    • 复制底部资源
    • 转移底部资源的所有权

    引用计数的实现:

    1
    2
    3
    4
    5
    6
    7
    8
    class Lock{
    public:
    explicit Lock(Mutex* pm): mutexPtr(pm, unlock){
    lock(mutexPtr.get());
    }
    private:
    std::shared_ptr<Mutex> mutexPtr;
    };

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

  • APIs往往要求访问原始资源,所以每个资源管理类应该提供一个get方法。
  • 对原始资源的访问可能经显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。

4.成对使用new和delete时要采取相同形式

5.以独立语句将new对象存储于智能指针内

  • 以独立语句将new对象存储于智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的内存泄露。

例如,以下的函数调用,可能先执行new产生对象,然后执行priority()发生异常终止程序,导致资源泄露。

1
2
3
4
5
//会产生异常的做法
processWidget(std::shared_ptr<Widgt>(new Widget), priority()); //参数操作执行顺序不确定
//独立语句存储对象指针
std::shared_ptr<Widgt> pw(new Widget);
processWidget(pw, priority());

四.设计与声明

1.让接口容易被正确使用,不易被误用

  • 好的接口应该容易使用,不易被误用
  • 促进正确使用的方法包括接口的一致性,以及与内置类型的行为兼容
  • 阻止误用的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任
  • shared_ptr支持定制删除器。可以防范DLL问题,可被用来自动解除互斥锁等

2.设计class犹如设计type

3.使用常引用传参

  • 尽量以引用传参替换值传递。引用传参更高效,并可避免切割问题
  • 对于内置类型,STL的迭代器和函数对象,使用值传递更合适

4.必须返回对象时,不要想返回引用

5.将成员变量声明为private

6.以non-member、non-friend替换member函数

7.如果所有参数都需要类型转换,使用non-member函数

8.不抛出异常的swap函数

  • 当std::swap函数对类型效率不高时,提供一个不抛出异常的成员swap函数
  • 提供一个成员swap函数,应该相应的提供一个非成员swap函数调用前者,对于class,特化std:;swap
  • 调用swap应对std::swap使用using声明式
  • 为用户定义类型进行std templates全特化是好的,但不要在std中加入全新的东西

五.实现

1.尽可能延后变量定义式的出现时间

  • 尽可能延后变量定义式的出现,这样做可增加程序的清晰度并改善程序效率
  • 对于循环中使用的变量,如果赋值成本比构造+析构低,或者正在处理代码中效率高度敏感的部分,可以在循环外定义变量,否则在循环内直接构造变量

2.尽量少做转型动作

  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts
  • 如果转型是必要的,尽量隐藏在函数后,使客户不需要将转型放入代码中
  • 尽可能使用新式转型

3.避免返回引用,指针,迭代器

4.异常安全性

  • 异常安全函数即时发生一场也不会泄露资源或允许任何数据结构败坏,这样的函数分为三类:基本型,强烈型,不抛异常型
  • 强烈保证往往能够以copy-and-swap实现,但不是所有函数都可实现强烈保证
  • 函数提供的异常安全保证最高只等于所有调用函数的异常安全保证中的最低级

5.理解inline

  • 将大多数inline限制在小型被频繁调研的函数身上,使潜在的代码膨胀问题最小化
  • 不要只因为function templates出现在头文件,就将其声明为inline

6.将文件间的编译依存关系降低

  • 如果使用对象引用或指针可以完成任务,就不要使用对象本身

  • 支持编译依存最小化的一般构想是:依赖声明式,不要依赖定义式,基于此构想的两个手段是Handle-class和Interface-class(提供一个factory函数,返回指向该类的一个新对象的指针)

  • 头文件应该完全且仅有声明式