《C+编程规范 101条规则、准则与最佳实践》笔记

《C+编程规范 101条规则、准则与最佳实践》

0、不要拘泥于小节(了解哪些东西不应该标准化)
* 与组织内现有编码规范一致即可
* 包括但不限于:
- 缩进
- 行长度
- 命名规范
- 注释形式
- 空格/制表符

1、在高警告级别干净利落地进行编译
* 使用编译器的最高警告级别,构建完应用程序后应该是0警告。 理解所有的警告,通过修改代码而不是降低警告级别来消除警告。
* VS警告级别:
- /W0 关闭所有警告
- /W1 显示严重警告
- /W2 显示等级1以及某些不太严重的警告
- /W3 显示等级2以及某些不太严重的警告
- /W4 显示所有等级3警告以及信息性警告 (默认)

2、使用自动构建系统

3、使用版本控制系统
* 使用git / svn 或其他工具作为版本控制工具协同工作

4、做代码审查
* code review 有助于提高质量,亮出自己的代码,阅读别人的代码,互相学习。无需形式主义,最好通过书面形式进行,比如电子邮件。

5、一个实体应该只有一个紧凑的职责
* 一次只解决一个问题:一个实体(变量、类、函数、名字空间、模块、库)赋予一个定义良好的职责。

6、正确、简单和清晰第一
* 质量 > 速度,简单 > 复杂, 清晰 > 机巧, 安全 > 不安全。
* 代码优先给人看,其次才是给计算机理解的。

7、编程中应该知道何时、如何考虑可伸缩性
* 最好不要假定在实际中不会处理大量数据,即使很大的可能性不会有大量数据需要处理,算法的时间复杂度也不要高于O(N)。
* 代码中使用灵活的、动态的分配数组,不要使用固定大小的数组;
* 优先使用尽可能快的算法( 对数 < 线性 < Nlog(N) < O(N^2 < 指数);

8、不要进行不成熟的优化
* 定义:以性能提升为目的,使代码更加复杂、可读性更差, 同时没有对修改前后做过性能对比, 这本质上是对程序没有好处的。
清晰、易读永远是第一位的。

9、不要进行不成熟的劣化
* 比如:可以使用引用传递参数的时候使用了值传递;使用前缀++ / -- 操作符很合适的场合使用了后缀版本;在构造函数中使用赋值操作而不是初始化列表。

10、尽量减少全局和共享数据
* 避免共享数据,尤其是全局变量。共享数据会增加耦合度,从而降低可维护性,通常还会降低性能。
* 在不同的编译单元中(c++源文件),全局变量或者静态类对象的初始化顺序是未定义的,在必须使用全局变量的场合注意其初始化的问题。

11、隐藏信息
* 减少操作抽象的调用代码和抽象的实现之间的依赖性,必须隐藏内部的数据;
* 绝对不要公开类的数据成员,或者公开数据成员的句柄或指针;
* 信息暴露越多,其受影响的范围也越大,也约不安全,反之亦然。

12、懂得何时、如何进行并发性编程
* 在多线程处理逻辑中,应该减少数据的共享; 如果必须要对数据进行共享,则要注意线程安全;
* 注意加锁的粒度:粒度过大容易造成等待锁的线程做无谓的等待,粒度过小容易导致频繁进行内核态和用户态代码的切换(WINDOWS内核线程同步对象的获取);
* 多线程只读的场合无需加锁

13、确保资源为对象所拥有。 使用显式的RAII和智能指针
* RAII:c++ 的 “资源获取即初始化”
* 最好使用智能指针(比如 shared_ptr ) 代替原始指针
* 分配原始资源的时候,应该立即将其传递给属主对象, 永远不要在一条语句中分配一个以上的资源

14、宁要编译时和链接时错误,也不要运行时错误
* C++ 最强大的静态检查工具,就是其自身的静态类型检查
* C++11 static_assert(expression && "what happened here!")

15、积极使用const
* 用const代替宏定义
* 不要强制进行const转换
* 函数参数优先采用const类型(根据实际情况来定,比如不要用const修复值传递的函数参数)
* const为常量,也是“不变的变量”,编译器可在编译时做一些检查

16、避免使用宏
* 使用const或枚举enum代替宏
* 不要使用宏,除非迫不得已
* 例外:c/c++ 头文件的防止重复包含模型
#ifndef PROJ_PATH_HEADER_FILE_H_
#define PROJ_PATH_HEADER_FILE_H_
// codes
#endif
可以用宏(可能是必须用宏,windows平台#pragma once)实现
* 例外: 条件编译指令等

17、避免使用魔数(Magic number)
* 使用枚举、const变量、普通变量、甚至宏来代替魔数

18、尽可能局部地声明变量
* 变量的生存期越短越好
* 例外:有时候把变量提取到for / while / for_each 循环之外是有好处的

19、总是初始化变量
* 定义的时候立即初始化变量是个很好的编码习惯

20、避免函数过长,避免嵌套过深
* 函数职责应该单一,代码应尽量紧凑
* 在条件判断中优先使用短路与 &&
* 优先使用标准算法

21、避免跨编译单元的初始化依赖
* 由于C++中全局变量的初始化顺序是未定义的,因此不要使用其他编译单元(c++源文件)中的全局变量给本编译单元中的全局变量赋值

22、尽量减少定义性依赖,避免循环依赖
* 优先使用类的前向生命,代替使用 #include 类头文件的引入
* 不要循环依赖(互相依赖)
* 不要让高层模块依赖低层模块
* 应该依赖接口,而非依赖细节(实现)

23、头文件应该自给自足
* 确保编写的每个头文件都能够独自进行编译,为此需要包含其内容所依赖的所有头文件
* 不要包含不需要的头文件
* 涉及模版的场合更能体现该条款:没有实例化时可以通过编译的模版(编译器不用知道其中某个类型T的大小),此时不用引入T声明所在的头文件就可以通过编译

24、总是编写内部 #include 保护符, 绝不要编写外部 #include 保护符
* 在头文件中使用
#ifndef xx
#define xx
..
#endif

xx应该唯一,google编程规范规定了xx的形式:Project_path_header_file_name_H_

* 不要在保护符前后写代码

25、正确地选择通过值、(智能)指针或者引用传递参数
* 分清输入参数、输出参数、输入/输出参数、值传递参数/引用传递参数
* 对于输出参数(只读):
- 用const修饰
- 优先使用值传递简单类型参数

* 对于输出参数 或 输入/输出参数:
- 如果参数是可选的,则优先使用(智能)指针传递
- 如果参数是必须的,则优先使用引用传递

26、保持重载操作符的自然语义
* 重载操作符时不要改变操作符本身的使用习惯和语义

27、优先使用算法操作符和赋值操作符的标准形式
* 如果要定义 a + b, 那也应该定义a += b
* 使 a @ b 与 a @= b 有相同的语义
* 用 += 实现 +
* 用 -= 实现 -
* 用 *= 实现 *
* 用 /= 实现 /
* 将重载的操作符的非成员函数版本与其操作的类型放在同一个命名空间下

28、优先使用 ++ 和 -- 的标准形式, 优先使用前缀形式
* 如果要定义 ++a,也要定义a++
* 后缀式通常会创建一个临时对象,因此优先使用前缀式

29、考虑重载以避免隐含类型转换
* 提供签名与常见类型精确匹配的重载函数可以避免不必要的隐含类型转换
* 比如:bool operator==(const std::string&, const std::string& ), std::string有一个隐含类型转换 std::string(const char*)
- 提供更精确的 bool operator==(const char* pBuffer, const std::string&)
- 提供更精确的 bool operator==(const std::string&, const char* pBuffer)
- 提供更精确的 bool operator==(const char* p1, const char* p2)

30、避免重载 && || ,
* 无法在三种情况下实现内置操作符的完整语义
* 无法保证求值顺序

31、不要编写依赖于函数参数求值顺序的代码
* 函数参数的求值顺序与调用约定有关,调用约定影响参数入栈(如果是用栈传递参数的话)顺序,因此不要依赖于具体的顺序

32、弄清所要编写的是哪种类
* 不同的类适用于不同的功能
* 值类
* 基类:
- 有一个 public && virtual 或者 protected && non-virtual 的虚函数
- 接口使用虚函数
- 总是动态的在堆中实例化为具体的派生类,并通过(智能)指针来用

* traits类:
- 只包含typedef和静态函数,没有可以修改的状态或者虚函数
- 通常不实例化

* 策略类:
- 可能有,也可能没有状态或虚函数
- 通常不独立实例化,只作为基类或者成员

* 异常类

33、用小类代替巨类
* 类的功能应该单一
* 一个类含有的状态越少,与其耦合的可能性越低
* 相反,如果一个类拥有太多的状态(成员变量),那么这个类一定很臃肿

34、用组合代替继承
* 友元关系是C++中第一紧密的耦合关系
* 继承 是C++中第二紧密的耦合关系
* 优先使用继承代替继承

35、避免从并非要设计成基类的类中继承
* 基类往往需要一些特殊的设计,应该避免将普通类当作基类
* 要添加行为,应该添加非成员函数,新添加的非成员函数应该与该类放在同一个命名空间下
* 要添加状态,应该使用组合而不是继承

36、优先提供抽象接口
* 不在接口中添加状态
* 抽象接口完全由(纯)虚函数组成
* 通常也没有成员函数实现
* 遵循依赖倒转原则:地层依赖高层、细节依赖抽象

37、公用继承即可替换性。 继承 不是为了重用,而是为了被重用
* 公用继承的目的不是未来派生类重用基类的代码

38、实施安全的改写
* 在派生类中重写一个virtual函数时:
- 要保持基类中的函数的前后条件
- 不要改变virtual函数的默认参数(virtual函数的默认参数使用其静态类型的默认参数)
- 要显式地使用 virtual 关键字修饰重写的虚函数

39、考虑将virtual函数声明为non-public的, 将public函数声明为non-virtual的
* 此条款不适用于析构函数
* 非虚拟接口模拟(MVI):public函数为non-virtual的, virtual函数为private(或protected的)
* 优点:
- 每个接口自然形成
- 基类拥有控制权
- 基类能够健壮地适应变化
* e.g.
class CBase
{
public:
void Func()
{
DoFunc();
}

private:
virtual DoFunc()
{
// do something
}
};

class CDerived : public CBase
{
private:
virtual DoFunc()
{
// do as i want to do
}
};

CBase* p = new Derived();
p->Func(); // 执行到了CDerived::DoFunc()函数

40、要避免提供隐式转换
* 隐式转换带来的影响经常是弊大于利
* 应该用 explicit 关键字修饰 只有一个非默认参数的构造函数

41、将数据成员设为私有的,无行为的聚集(c语言结构体)除外
* 数据成员应该尽量有低的访问属性
* 如果只是纯数据的集合,而并没有对这些字段进行操作的内部行为,则为public更加合适

42、不要公开内部数据
* 不要公开内部数据
* 不要公开内部数据的指针
* 不要公开内部数据的句柄

43、明智地使用Pimpl
* 对某个类的细节隐藏:
- 现对该类前置声明:class CObj
- shared_ptr<CObj> m_pOBj;
- 到这,编译器无需看到CObj类的实现细节
* 只有当增加访问层次确实有好处的情况下才这样做

44、优先编写非成员、非友元函数
* 非成员、非友元函数意味着更低的访问权限,更少的影响状态(出错)的概率

45、总是一起提供new 和delete
* 每个类重载 void* operator new(param) 时都必须同时重载 void operator delete(void*, param)
* 数组形式的new[] delete[]同理

46、如果提供类专用的new,应该提供所有标准形式(普通、就地、不抛出)
* 普通 void* operator new(std::size_t)
* 就地 void* operator new(std::size_t, void*)
* 不抛出 void* operator new(std::size_t, std::nothrow_t) throw()

47、以同样的顺序定义和初始化成员变量
* 成员变量的初始化顺序要与在类定义中的声明顺序一致
* 其初始化顺序与其在初始化列表中的书信无关

48、在构造函数中用初始化代替赋值
* 用初始化代替赋值减少临时对象的创建
* 应该总是在构造函数体内而不是初始化列表中执行非托管资源获取,比如并不立即将结果传递给智能指针构造函数的new表达式

49、避免在构造函数和析构函数中调用virtual函数
* 因为在构造和析构期间,对象的状态处于未知或不完整状态,因此代用的 virtual 未必是期望的运行时版本的

50、将基类析构函数设置为 public && virtual 的, 或者protected && non-virtual 的
* 含有动态删除的基类(delete pBase):public virtual ~CBase();
* 不含有动态删除的基类: protected ~CBase();

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

52、一致地进行复制和销毁
* 如果定义了拷贝构造函数、operator=或者析构函数中的任何一个,大概率需要定义另一个或另外两个
* 例外,如果声明这三个函数中的某(几)个是为了将其设置为private的则无需遵循此条款

53、显式地启用或禁止复制
* 三种行为:
- 使用编译器默认版本
- 自定义版本
- 禁用

54、避免切片,在积累中考虑用克隆代替复制
* 如果在基类中要进行多态(深度)复制的话,考虑禁止copy constrcutor 和 operator=,则改为提供虚拟的Clone()成员函数

55、使用赋值的标准形式
* 标准赋值操作符的两种声明形式
- T& operator=(const T& t)
- T& operator=(T t)

56、只要可行,就提供不会失败的swap(而且要正确地提供)

57、将类型及其非成员函数接口置于同一名字空间中
* 如果非成员函数foo是类X的接口的一部分,那么就必须将foo和X放在同一个名字空间中

58、应该将类型和函数分别置于不同的名字空间中,除非有意想让它们一起工作
* 通过将类型置于单独的名字空间中,可以将类型与无意的ADL(参数依赖查找)隔离开来,促进有意的ADL
* 避免将类型和模板化函数或操作符放在相同的名字空间中

59、不要在头文件中或者 #include 之前写名字空间using
* 不要 在头文件任何位置写 using namespace xx, 应该使用 xx::type
* 不要 在c++实现文件(cpp)的最后一行 #include 之前使用 using namespace xx, 应该使用xx::type
* 在c++实现文件(cpp)的最后一个 #include 之后可以随意使用 using namespace xx 或 x::type

60、要避免在不同模块中分配和释放内存
* 在哪个模块分配内存,就在哪个模块释放内存
* 不同的模块可能使用不同的编译器 / 编译选项 / 编程语言 编写
* 不同的模块可能使用了不同堆

61、不要在头文件中定义具有链接的实体
* 包括: 全局变量、函数定义,可能会引起链接错误
* 例外:inline函数、函数模版、类模板的static数据成员

62、不要允许异常跨越模块边界传播

63、在模块的接口中使用具有良好移植性的类型
* 模块A与模块B之间的接口尽量采用简单类型
* 模块A B 内部可将简单类型转换为复合类型使用

64、理智地结合静态多态性和动态多态性

65、有意地进行显式自定义

66、不要特化函数模板

67、不要无意地编写不通用的代码

68、广泛的使用断言记录内部假设和不变式
* 断言表达式不要包含运行时代码
* 避免使用assert(false), 应该使用assert(!"assert information")
* assert(expression && "information message")
* c++11 静态断言static_assert(expression && "information message"),在编译阶段的断言

69、建立合理的错误处理策略,并严格遵守

70、区别错误和非错误
* 违反或不满足前条件
* 无法满足后条件
* 无法重新建立不变式

71、设计和编写错误安全代码
* 基本保证:程序出错时仍处于有效状态
* 最强保证:如果有错误则可以回滚到最初的状态
* 不会失败保证:永远不会失败

72、优先使用异常报告错误
* 异常 > 错误码

73、通过值抛出异常,通过引用捕获异常

74、正确地报告、处理和转换错误

75、避免使用异常规范
* 尽量不要使用异常规范
* 派生类的异常规范应该使其限制不少于基类版本

76、默认时使用vector,否则选择其他合适的容器
* 编程时正确、简单和清晰是第一位的
* 编程时只在必要时才考虑效率,对于简单类型而言,容器大小超过几千个元素之前,vector与hash、set、map相比差异不会太大
* 尽可能编写事务性(一个操作要么成功完成、要么保持原始状态)、强错误安全的代码
* 当元素数量较小时,vector的(插入操作时间复杂度O(N))优势仍然总是优于list(即使后者的插入操作时间复杂度是O(1))
* list的算法复杂性上的优势只有在数据量更大时才能发挥作用

77、用vector和string代替数组
* 用vector和string代替C语言风格的数组
* vector和string提供的接口易操作,支持的功能丰富并且并未牺牲太多效率

78、使用vector(和string::c_str)与非C++ API 交换数据
* vector和string::c_str 是与非C++ API 通信的通道
* 获取vector<T>::iterator指向的元素地址:&(*iter)
* 获取vector<T> v的元素首元素地址:&(*v.begin()) &v[0] &(v.front())
* string的实现不一定是一片连续的内存
* string::c_str 返回一个空字符结束的C风格字符串

79、在容器中只存储值和智能指针
* vector<int> set<string> list<shared_ptr<Widget> > list<Widget*> list< vector<Widget>::iterator >
* 不要把 auto_ptr 对象放入容器
* 类似 TCP连接 不可复制的对象,也应该直接通过智能指针 container< shared_ptr<TcpConnection> > 存放

80、用 push_back 代替其他扩展序列的方式
* 如果不是要在特定位置插入,就应该直接使用push_back, 其他方法可能极慢并且不简明
* push_back 是按指数级扩大容量的,而不是按固定容量扩大的,因此重新分配和复制的次数将随大小的增长而迅速减少
* push_back 添加元素平均每个元素只会复制一次
* 可以显式调用 reserve 预先为容器分配空间,减少元素移动复制的次数

81、多用范围操作,少用单元素操作
* 应该多用 insert(pos, Iterator begin, Iterator end) 传入两个迭代器指定范围的版本,而不要使用连续调用操作单个元素的形式
* 给函数传入的信息越多,函数正确完成的可能性就越大
* 传入两个迭代器insert的版本可以让函数有更大的优化的空间

82、使用公认的惯用法真正地压缩容量,真正的删除元素
* 要真正的压缩容器的多余容量,应该使用“swap”惯用法
- container<T>(c).swap(c) 去除多余容量, size() == capacity()
- container<T>().swap(c) 删除所有元素,size() == capacity() == 0

* 要真正的删除容器中的元素,应该使用 erase-remove 惯用法
* remove 操作并非真正将元素从容器中删除,而是将所有不删除的元素放到容器前面,把要删除的元素放到容器后面,并不影响容器的size和capacity
- vector<T> c
auto iter = std::remove(c.begin(), c.end(), value) // remove掉从being到end范围内的value元素
c.erase(iter) // 删除所有value

* 对于有 remove 和 remove_if 的容器,应该尽量使用其成员函数版本

83、使用带检查的STL实现
* 迭代器很容易出错,而且不会出现编译错误,甚至不会崩溃(最糟的情况)
* 大多数带检查的STL都会通过在容器和迭代器中添加附加的调试信息和支持信息来自动检查这些错误

84、用算法调用代替手工编写的循环
* STL算法使用了较高层次的、定义更佳的抽象操作

85、使用正确的STL查找算法
* find find_if lower_bound upper_bound equal_range binary_search
* 对无序范围:find find_if 线性时间
* 对有效范围:binary_search lower_bound upper_bound equal_range 对数时间
* equal_range 同时用到了 lower_bound upper_bound,但是 T(equal_range) < ( T(lower_bound) + T(upper_bound) )

86、使用正确的STL排序算法
* 时间复杂度 partition < stable_partition < nth_element < partial_sort( partial_sort_copy ) < sort < stable_sort
* partition stable_partition nth_element 线性时间
* nth_element
- nth_element(Iterator begin, Iterator nth, Iterator end, Compare comp)
- 该算法是部分排序算法,它重排[begin, end)中的元素,使得:nth所指向的元素被改为假如[begin, end)已排序后该元素会出现的元素;
- 这个新的nth元素前的所有元素<=nth元素后的所有元素
- comp 是一个比较器:std::greater<T>() 或 [](const T& t1, const T& t2) { return (t1 > t2);}

87、使谓词成为纯函数
* 不要让谓词保存或访问对其 operator() 结果有影响的状态,包括成员状态 和 全局状态
* 应使 operator() 成为谓词的 const 成员函数

88、算法和比较器的参数应多用函数对象,而少用函数
* 应该向算法传递函数对象而非函数
* 关联容器的比较器必须是函数对象,函数对象的适配性好,它们产生的代码一般比函数要快
* unary_function 一元函数对象的基类
template <class Arg, calss Result>
struct unary_function
{
typedef Arg argument_type;
typedef Result result_type;
};

其中 Arg 是参数类型, Result是返回值类型

struct IsOdd : public std::unary_function<int, bool>
{
bool operator()(int number)
{
return ( number % 2 == 0);
}
};

* binary_function 二元函数对象的基类
template <class Arg1, class Arg2, class Result>
struct binary_function
{
typedef Arg1 first_argument_type;
typedef Arg2 secdon_argument_type;
typedef Result result_type;
};

其中 Arg1 是第一个参数的类型, Arg2是第二个参数的类型, Result是返回值类型

struct Comparer : public std::binary_function<int, int, bool>
{
bool operator()(int a, int b) { return ( a == b ); }
};

89、正确编写函数对象
* 成本要低,而且要可适配
* 将函数对象设计为复制成本很低的值类型,尽量从 unary_function binary_function 继承
* 要避免提供多个 operator() 函数

90、避免使用类型分支,多使用多态
* 避免通过对象的类型分支来定制行为
* 使用模板、虚函数让类型自己决定行为

91、依赖类型,而非其表示方法
* 不要对对象在内存中的布局做任何假设
* 让对象的类型自己决定其内存布局

92、避免使用 reinterpret_cast
* 不要尝试使用 reinterpret_cast 强制编译器将某个类型对象的内存重新解释为另一种类型的对象,这违反了类型安全性的原则

93、避免对指针使用 static_cast
* 不要对动态对象的指针使用 static_cast, 应该使用 dynamic_cast甚至重构货或重新设计
* dynamic_cast 把基类指针(引用)转为派生类指针(引用)需要基类中有至少一个 virtual 函数
* dynamic_cast 可在多重继承结构体中由一个基类转为另一个基类

94、避免强制转换 const
* 强制转换 const 有代码的坏味道

95、不要使用C风格的强制转换
* C风格的强制转换不会对类型做任何检查

96、不要对非POD类型进行 memcpy / memcmp 操作
* 编译器经常会在动态对象中嵌入一些隐藏数据来支持多态,在多重继承情况下对象在不同的偏移位置会有多个vptr存在
* memcpy / memcmp 破坏了对象的封装性和隐藏性
* 只有对POD类型进行 memcpy / memcmp 才是安全的

97、不要使用联合体重新解释表示方法
* 读取联合体中的字段时,要读最后一次被赋值的字段, 不要对union的内存布局做假设

98、不要使用可变长参数(...)
* 不安全

99、不要使用失效对象,不要使用不安全函数
* 失效对象
- 已销毁对象
- 语义失效对象(e.g. 野指针)
- 从来都有效的对象(越界数组)
* 不要尝试调用 object.~T(), placement new 的形式

100、不要多态的处理数组

上一篇:菜鸟教程之工具使用(十二)——Eclipse突出显示选中的相同变量


下一篇:3D空间坐标系转换复习