《C++ Coding Standards》读书笔记

Table of Contents

  • 1 书籍信息
  • 2 Rule 0: 不要因为小事纠结
  • 3 Rule 1: 使用最高警报级别编译
  • 4 Rule 16: 尽量不使用宏
  • 5 Rule 19: 总是初始化变量
  • 6 Rule 25: 传参规则
  • 7 Rule 29: 重载拷贝构造函数避免隐式类型转换
  • 8 Rule 32: 明确类的类型
    • 8.1 值类
    • 8.2 基类
    • 8.3 traits类
    • 8.4 异常类
  • 9 Rule 34: 使用组合代替继承
  • 10 Rule 35: 避免从并非设计成基类的类中继承
  • 11 Rule 37: 公用继承即可替代性。继承,不是为了重用,而是为了被重用
  • 12 Rule 38: 实施安全的覆盖
  • 13 Rule 39: NVI(Non-Virtual Interface)机制
  • 14 Rule 40: 避免提供隐式类型转换
  • 15 Rule 43: Pimpl惯用法
  • 16 Rule 44: 优先编写非成员非友元函数
  • 17 Rule 46: 如果提供专门的new,应该提供所有标准形式
    • 17.1 new的3种类型
      • 17.1.1 普通new
      • 17.1.2 in-place new
      • 17.1.3 nothrow new
  • 18 Rule 47: 以同样的顺序定义和初始化成员变量
  • 19 Rule 48: 在构造函数中用初始化代替赋值
  • 20 Rule 49: 避免在构造函数和析构函数中调用virtual函数
  • 21 Rule 50: 总是为类编写析构函数,因为模式的是public且非virtual
  • 22 Rule 51: 析构函数、释放和交换绝不能失败
  • 23 Rule 52: 如果定义了拷贝构造函数、赋值操作符或者析构函数中的任一个,那么也应该定义另外两个
  • 24 Rule 54: 为了避免切片,显示提供nvi风格的Clone函数,并且禁止拷贝
  • 25 Rule 55: 使用赋值的标准形式
  • 26 Rule 59: 不要在头文件中或者#include之前编写名字空间using
  • 27 Rule 60: 不要把申请和释放内容放到不同模块
  • 28 Rule 61: 不要在头文件中定义链接实体
  • 29 Rule 62: 不要允许异常跨模块传播
  • 30 Rule 68: 正确使用断言
  • 31 Rule 73: 异常通过值抛出,通过引用捕获
  • 32 Rule 75: 避免使用异常规范
  • 33 Rule 76: 模式时使用vector。否则,选择其它合适容器
  • 34 Rule 78: 使用vector(和string::c_str)与非C++ API交换数据
  • 35 Rule 79: 容器中只存值和智能指针
  • 36 Rule 82: 小技巧:释放容器多余空间、删除容器内容
  • 37 Rule 95: 不要使用C风格的强制类型转换
  • 38 Rule 96: 不要对类对象使用memcpy或者memcmp

1 书籍信息

./cpp_coding_standards_101_rules_guidelines_best_practise-cover.jpg

  • 书名:C++ Coding Standards, 101 Rules, Guidelines, and Best Practices
  • 作者:Herb Sutter / Andrei Alexandrescu

2 Rule 0 : 不要因为小事纠结

Don't sweat the small stuff

我们建立代码规范的目的是提高代码质量,而不是为了把工程师约束成代码机器。所以,需要避免将一些旁枝末节的东西纳入到规范中来。下面是一些常见的例子:

  1. 不必规定缩进的风格 - 保证一个项目中使用统一的缩进风格即可
  2. 不必规定行的长度 - 只要代码可读性好就成
  3. 不必过度渴求命名 - 通俗易懂即可
  4. 不必规定注释风格 - 除了要使用doxygen这类工具生成文档的情况

3 Rule 1 : 使用最高警报级别编译

例如:-Wall -Werror

4 Rule 16 : 尽量不使用宏

在C++中,宏可以用如下语言特性替换:

  • const或者enum定义常量
  • inline避免函数调用的开销
  • template指定函数系列和类型系列
  • namespace避免名字冲突

5 Rule 19 : 总是初始化变量

6 Rule 25 : 传参规则

对于只输入参数:

  1. 使用const
  2. 对于复制开销低的类型,优先通过值传递
  3. 如果函数需要获得参数的副本,使用值传递

对于输入输出参数:

  1. 如果需要保存指针副本,使用(智能)指针
  2. 如果无需保存指针副本,使用引用

7 Rule 29 : 重载拷贝构造函数避免隐式类型转换

8 Rule 32 : 明确类的类型

8.1 值类

  • 有一个public的析构函数、拷贝构造函数、赋值操作符
  • 没有virtual函数(包括析构函数)
  • 是用作具体类,不是基类(Rule 35)
  • 总在栈中实例化,或者作为一个类的成员实例化

8.2 基类

  • 有一个public且virtual,或者是protected且非virtual的析构函数(Rule 50)
  • 有一个非public的拷贝构造函数和赋值操作符(Rule 53)

8.3 traits类

  • 只包含typedef和静态函数,没有可修改的状态或者virtual函数
  • 通常不实例化(构造函数被禁止)

8.4 异常类

  • 有一个public的析构函数和不会失败的构造函数(特别是一个不会失败的拷贝构造函数。从异常的拷贝构造函数抛出将使程序中止)
  • 有virtual函数,经常实现克隆(Rule 54)和访问(visitation)
  • 从std::exception虚拟派生更好

9 Rule 34 : 使用组合代替继承

继承是仅次于友元的第二紧密的的耦合关系。如仅仅为了代码复用,使用组合即可。和继承相比,组合有如下好处:

  • 不影响调用代码的情况下具有更大的灵活性 。(Rule 37)
  • 更好的编译时隔离,更短的编译时间 。使用组合能减少对头文件的依赖,因为在定义中使用某个类的指针,不需要完整的类定义。但是继承需要引入完整的头文件。
  • 奇异现象减少 。(Rule 58)。
  • 更广的适用性 。有些类一开始并不是想设计成基类(Rule 35)。但是,大多数类都能充当一个成员的角色。
  • 复杂性和脆弱性降低 。继承会带来更多额外的复杂情况,比如名字隐藏。

在下面的情况中,使用继承会更适合:

  • 使用公用继承模拟可替代性 。(Rule 37)
  • 如果需要改写virtual函数
  • 如果需要访问protected成员
  • 如果需要在基类之前构造已使用过的对象,或者在基类之后销毁此对象
  • 如果需要控制多态

10 Rule 35 : 避免从并非设计成基类的类中继承

一个被设计成基类的类需要做一些额外的工作的(Rule 32、Rule 50、Rule 54)。 以std::string为例,若想扩充它的功能(做一个super_string),定义自由函数(非成员)要比继承它好得多,因为:

  • 自由函数对于已经使用了string的遗留代码工作良好,super_string则需要重构整块代码,将类型和函数签名改为super_string
  • 以string为参数的接口需要做改动(3选1):a)避开super_string提供的功能;b)复制参数为super_string;c)将string的引用强制转换成super_string。都很搓
  • super_string的新增成员函数并不会比自由函数有更大的访问权限。因为string也许并没有被设计成可继承,那么成员可能是private
  • super_string若隐藏(重新定义了一个非virtual成员函数)了一个string的成员,会导致功能异常

使用自由函数来扩展,需要注意:

  • 要将这些函数放在被扩展类同一个名字空间中(Rule 57)

11 Rule 37 : 公用继承即可替代性。继承,不是为了重用,而是为了被重用

12 Rule 38 : 实施安全的覆盖

  • 覆盖应该保持被覆盖函数的约定,如:是否一定成功、保持参数的默认值等
  • 谨防不小心隐藏了重载函数
class Base {
public:
  virtual void Foo(int a) {printf("%d\n", a);}
  virtual void Foo(int a, int b) {printf("%d%d\n", a, b);}
  virtual void Foo(int a, int b, int c) {printf("%d%d%d\n", a, b,c);}
};

class Derived : public Base {
public:
  virtual void Foo(int a) {printf("%d\n", a);}
};

int main(int argc, char *argv[]) {
  Derived d;
  d.Foo(1);        // 正确
  d.Foo(1, 2);     // 错误
  d.Foo(1, 2, 3);  // 错误
  return 0;
}

上面的代码里面,由于派生类隐藏了基类的2个Foo函数,导致这2个函数的调用是非法的,不能编译。可以使用using来暴露函数:

class Derived : public Base {
public:
  virtual void Foo(int a) {printf("%d\n", a);} // 覆盖Base::Foo(int)
  using Base::Foo;              // 将其它Base::Foo重载函数引入作用域
};

13 Rule 39 : NVI(Non-Virtual Interface)机制

考虑将virtual函数声明为非public,将public函数生命为非virtual

公用的virtual函数有2个相互矛盾的职责:

  1. 指定接口
  2. 提供实现细节

NVI的目的是将接口控制权收回给基类。下面是一个NVI的例子:

class Base {
public:
  Base() {};
  int Work(void) {
    printf("in base\n");
    return DoWork();
  }
private:
  virtual int DoWork(void) = 0;
};

class Derived : public Base {
public:
  Derived() {};
private:
  virtual int DoWork(void) {
    return 2;
  }
};

NVI将控制权收回到了基类,这样能在编写基类的时候,编写一些前置或后置的代码,而不用担心会被子类给覆盖掉。

14 Rule 40 : 避免提供隐式类型转换

对于只有一个参数的构造函数,加上 explicit 关键字。

15 Rule 43 : Pimpl惯用法

代码举例如下:

class Map {
private:
  struct Impl;
  shared_ptr<Impl> pimpl_;
};

使用pimpl存储所有的私有成员,包括数据成员和成员函数。这样就能 随意改变类的私有实现细节,而不用重新编译调用代码

16 Rule 44 : 优先编写非成员非友元函数

将不依赖于类私有成员的函数从类里面独立出来。

17 Rule 46 : 如果提供专门的new,应该提供所有标准形式

17.1 new的3种类型

17.1.1 普通new
void *operator new(std::size_t);
17.1.2 in-place new
void *operator new(std::size_t, void*);
17.1.3 nothrow new
void *operator new(std::size_t, std::nothrow_t) throw();

18 Rule 47 : 以同样的顺序定义和初始化成员变量

C++是按照定义的顺序初始化成员变量的,和构造函数初始化列表的顺序无关。如下面代码:

class A {
private:
  string a_, b_, c_;
public:
  A(const char *a, const char *b, const char *c) : b_(b), c_(c), a_(b_ + c_) {}
};

这段代码能通过编译,但是运行时候会报错。

19 Rule 48 : 在构造函数中用初始化代替赋值

class A {
  string s1_, s2_;
public:
  A() {s1_ = "hello"; s2_ = "world";}
};

这代码实际上是:

class A {
  string s1_, s2_;
public:
  A() : s1_(), s2_() {s1_ = "hello"; s2_ = "world";}

那么最优的写法:

class A {
  string s1_, s2_;
public:
  A() : s1_("hello"), s2_("world") {}
};

20 Rule 49 : 避免在构造函数和析构函数中调用virtual函数

下面代码,能编译通过,但是运行时会报错:

class Base {
public:
  Base() {DoWork();};
  virtual void DoWork() = 0;
};

class Derived : public Base {
public:
  Derived() {}
  virtual void DoWork() {printf("DoWork\n");}
};

但是有的情况就需要后构造,可以采取如下方法:

  1. 一个bool的成员变量表示类的初始化状态,显示提供一个init的函数,执行后构造。在文档里面规定,调用者必须在类构造之后,调用init函数。(比较搓)
  2. 工厂方法,示例代码如下:
class Base {
protected:
  Base() {}
  virtual Init() {} // 构造之后立即调用
public:
  template<class T>
  static shared_ptr<T> Create() {
    shared_ptr<T> p(new T);
    p->Init();
    return p;
  }
};

class Derived : public Base { /*****/ }

int main(int argc, char *argv[]) {
  shared_ptr<D> p = D::Create<D>();
}

21 Rule 50 : 总是为类编写析构函数,因为模式的是public且非virtual

22 Rule 51 : 析构函数、释放和交换绝不能失败

这些函数应该是总能捕获异常的,不让异常传播到函数以外。

23 Rule 52 : 如果定义了拷贝构造函数、赋值操作符或者析构函数中的任一个,那么也应该定义另外两个

24 Rule 54 : 为了避免切片,显示提供nvi风格的Clone函数,并且禁止拷贝

class Base {
public:
Base* Clone() const {
  B *p = DoClone();
  assert(typeid(*p) == typeid(*this) && "DoClone incorrectly overridden");
  return p;
protected:
  B(const B&);
private:
  virtual B *DoClone() const = 0;
}
};

25 Rule 55 : 使用赋值的标准形式

T& operator=(const T&);
T& operator=(T);
  • 不要把返回值定义为const T&,否则将不能和标准库一起使用
  • 赋值函数需要考虑参数就是对象本身的情况
  • 注意区分 成员赋值函数 和 独立赋值函数

26 Rule 59 : 不要在头文件中或者#include之前编写名字空间using

27 Rule 60 : 不要把申请和释放内容放到不同模块

28 Rule 61 : 不要在头文件中定义链接实体

链接实体,放到头文件中会链接错误,诸如以下:

int age;
void Foo() { /*****/ }

但以下例外:

  1. 内联函数
  2. 函数模板
  3. 类static数据成员

此外, Schwarz计数器 或 灵巧计数器 等全局数据初始化技术要求将静态(或匿名名字空间中的)数据。cin、cout、cerr、clog中使用了此类技术。

29 Rule 62 : 不要允许异常跨模块传播

30 Rule 68 : 正确使用断言

  • 通过定义NDEBUG屏蔽assert
  • 要避免使用
assert(false);

使用:

assert(!"Info Message");

31 Rule 73 : 异常通过值抛出,通过引用捕获

32 Rule 75 : 避免使用异常规范

33 Rule 76 : 模式时使用vector。否则,选择其它合适容器

vector具有一下特性:

  1. 保证具有所有容器中最低的空间开销
  2. 保证具有所有容器中对所存元素进行存取的速度最快
  3. 保证具有与身俱来的引用局部性。容器中相邻对象保证在内存中也相邻
  4. 保证具有与C语言兼容的内存布局。vector和string::c_str都可以传递给C语言的API。Rule 78
  5. 保证具有所有容器中最灵活的迭代器(随机访问迭代器)
  6. 几乎肯定具有最快的迭代器

34 Rule 78 : 使用vector(和string::c_str)与非C++ API交换数据

35 Rule 79 : 容器中只存值和智能指针

36 Rule 82 : 小技巧:释放容器多余空间、删除容器内容

container<T>(c).swap(c);  // 去除多余容量的shrink-to-fit(压缩到合适)惯用法
container<T>().swap(c);   // 去除全部内容和容量的惯用法

remove并不真正从容器中删除元素。需要remove之后再erase才能删除。

37 Rule 95 : 不要使用C风格的强制类型转换

反面例子:

extern void Fun(Derived*);
void Gun(Base *pb) {
  Derived *pd = (Derived*)pb;
  Fun(pd);
}

而应该:

extern void Fun(Derived*);
void Gun(Base *pb) {
  Derived *pd = static_cast<Derived*>(pb);
  // 或者     = dynamic_cast<Derived*>(pb);
  Fun(pd);
}

38 Rule 96 : 不要对类对象使用memcpy或者memcmp

memcpy和memcmp会破坏类型系统

Date: Thu Jun 14 19:23:07 2012

Author: Tan Menglong

Org version 7.8.11 with Emacs version 24

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值