C++编码规范 读书笔记

      项目组一直没有做代码审查,最近有启动这项计划的打算,因此提前复习一下《C++编程规范》,并做一些笔记。我们做任何事通常都先从简单的入手,循序渐进,持续改进,那么做代码审查也不例外,《C++编程规范》又很多,如果一下子突然引入,会对代码编写提出过高的要求,对开发人员的打击比较大,从而可能会影响团队的整个士气,所以我想我们应该从最简单(即容易遵循做到)、最重要的几个规范开始,即追求 【有效性/复杂性】 最大化。

     联想到日程安排的十字表格,如法炮制了如下表格,以便分门别类:

代码审查
B(简单&很重要)C(复杂&很重要)
A(简单&较重要)D(复杂&较重要)

    当然,规范本没有重要与不重要之分,这里这样给其画上这样的标签,只是一个相对的概念,是给我们推进“代码审查”这项工作一个简单的指引,例如先实施A区的规则,一段时间后,当团队成员都习惯了这些规则,再实施B区的规则,基本上按照先易后难的顺序,依次推进。

    由于笔者的学识水平,以及经验所致,对一些规则的认识肯定存在偏差与不妥,还请同学批评赐教。

  《C++编程规范——101条规则、准则与最佳实践》(C++ Coding Standards——101 Rules, Guidelines and Best Practices)

组织和策略问题

 第0条(D): 不要拘泥于小节(又名:了解哪些东西不应该标准化)

      是的,有些东西不应该规定过死。但是我们认为,在一些个人风格和喜好方面,保持团队内部的一致性是有好处的。一致性的重要性似乎怎么强调都不过分。

第1条(B):在高警告级别下干净利落地进行编译

      将编译器的警告级别调到最高,高度重视警告。编译器是我们的好朋友,如果它对某个构建发出警告,就说明这个构建可能存在潜在的问题。这个问题可能是良性的,也可能是恶性的。我们应该通过修改代码而不是降低警告级别来消除警告。

第2条(C):使用自动构建系统

      “一键构建”,甚至使构建服务器根据代码的提交自动进行构建,持续集成,不断交付软件产品,在迭代中完善。尽管现在有了很多开源的自动化构建系统,但是搭建并维护这样一个自动化构建系统,需要团队中有一位经验丰富的高手。

第3条(B):使用版本控制系统(VCS)

       VSS, SVN, GIT。

第4条(*):做代码审查

      审查代码:更多的关注有助于提高质量。亮出自己的代码,阅读别人的代码。互相学习,彼此都会受益。代码审查无需太形式主义,但一定要做,团队可根据自己的实际情况尝试着去做,在做的过程中,慢慢改进,找到一个符合团队实际需要的方式。CppCheck

设计风格

第5条(C):一个实体应该只有一个紧凑的职责

      一次只解决一个问题:只给一个实体(变量、类、函数、名字空间、模块和库)赋予一个定义良好的职责。随着实体变大,其职责范围自然也会扩大,但是职责不应该发散。

第6条(C):正确、简单和清晰第一

      软件简单为美(KISS原则:Keep It Simple Software):质量优于速度;简单优于复杂;清晰优于机巧;安全第一。可读性:代码必须是为人编写的,其次才是计算机。

第7条(C):编程中应该知道何时和如何考虑可伸缩性

    面对数据的爆炸性增长,应该集中精力改善算法的O(N)复杂度。在这种情况下,小型的优化(例如节约一个赋值、加法或乘法运算)通常无济于事。

第8条(A):不要进行不成熟的优化

第9条(A):不要进行不成熟的劣化

     构造既清晰又有效的程序有两种方式:使用抽象(DIP)和库(STL)。

第10条(B):尽量减少全局和共享数据

第11条(C):隐藏信息

第12条(D):懂得何时和如何进行并发性编程

      线程安全?并发编程?加锁解锁死锁?这些对我来说还只是属于概念......,鄙视一下自己!

第13条(B):确保资源为对象所拥有。使用显式的RAII和智能指针

     利器在手,不要再徒手为之。当然,也要防止智能指针的过度使用。如果只对有限的代码可见(例如函数内部,类内部),原始指针就够用了。

编程风格

第14条(C):宁要编译时和连接时错误,也不要运行时错误

第15条(A):积极的使用const

     const是我们的朋友,不变的值更易于理解,跟踪和分析。定义值的时候,应该将const作为默认选项。用mutable成员变量实现逻辑上的不变性,在给某些数据做缓存处理的时候经常使用这一特性。即在类的const成员函数中可以合法的修改类的mutable成员变量。

     当然,对于那种通过值传递的函数参数声明为const 纯属多此一举,反而还会引起误解。

第16条(D):避免使用宏

     这似乎是C++中一条人人皆知的编程规范。可真正严格遵守的团队并不多(纯属猜想,哈哈)。

第17条(D):避免使用“魔数”

    同第16条。

    应该用符号常量替换直接写死的字符串(或宏)。将字符串与代码分开(比如将字符串放入一个独立的CPP文件中),这样有利于审查和更新,而且有助于国家化。

第18条(A):尽可能局部地声明变量

    避免作用域膨胀。变量的生存期越短越好。因此,尽可能只在首次使用变量之前声明之(通常这时你也有足够的数据对它初始化了)。

第19条(B):总是初始化变量。

    这里有两段很好的示例代码:

[cpp]  view plain copy
  1. // 虽然正确但不可取的方式:定义变量时没有初始化  
  2. int speedupFactor;  
  3. if (condition)  
  4.     speedupFactor = 2;  
  5. else  
  6.     speedupFactor = -1;  


以下两种方式更好一些:

[cpp]  view plain copy
  1. // 可取的方式一:定义变量时即初始化  
  2. int speedupFactor = -1;  
  3. if (condition)  
  4.     speedupFactor = 2;    
  5.   
  6. // 较好且简练的方式二:定义变量时即初始化  
  7. int speedupFactor = condition ? 2 : -1;  

 

第20条(D):避免函数过长,避免嵌套过深

第21条(C):避免跨编译单元的初始化依赖

第22条(A):尽量减少定义性依赖。避免循环依赖。

    尽可能的使用前置声明(forward declaration). Pimpl惯用法对遵循这一规范有实际性的帮助。DIP(依赖倒置原则)。

第23条(A):头文件应该自给自足

   各司其责:应该确保每个头文件都能够单独编译。

第24条(A):总是编写内部的#include保护符,决不要编写外部的#include保护符

函数与操作符

第25条(B):正确地选择通过值、(智能)指针或者引用传递参数

第26条(C):保持重载操作符的自然语义

第27条(C):优先使用算术操作符和赋值操作符的标准形式

     1)如果要定义 a+b,也应该定义 a+=b。一般利用后者实现前者,即赋值形式的操作符完成实际的工作,非赋值形式的操作符调用赋值形式的操作符。2)如果可能,优先选择将这些操作符函数定义为非成员函数,并将其和要类型T放入同一个名字空间中。3)非成员函数返回值或引用,成员函数返回引用。

帖几段示例代码:

[cpp]  view plain copy
  1. // 成员函数 @=   
  2. T& operator@=(const T& rhs)  
  3. {  
  4.      //.....具体的实现代码.....  
  5.      return *this;  
  6. }  
  7.   
  8. // 非成员函数 @   
  9. T  operator@(const T& lhs, const T& rhs)  
  10. {  
  11. T temp = lhs;  
  12. return temp @= rhs;  
  13. }  
  14.   
  15. // 非成员函数 @= 返回输入参数的引用  
  16. T& operator@=(T& lhs, const T& rhs)  
  17. {  
  18.     // ......具体的实现代码......  
  19.     return lhs; // 返回输入参数的引用  
  20. }  
  21.   
  22. // 非成员函数 @   
  23. T operator@(T lhs, const T& rhs)  
  24. {  
  25.     return lhs @= rhs;  
  26. }  

 

第28条(A):优先使用++和--的标准形式。优先使用前缀形式
      如果定义了++C,也应该定义 C++,而且应该用前缀形式实现后缀形式。

标准形式,示例代码:

[cpp]  view plain copy
  1. //  前缀形式  
  2. T& operator++()  
  3. {  
  4.     // ......执行递增的实现代码  
  5.    return *this// 返回递增后的新值  
  6. }   
  7.   
  8. // 后缀形式  
  9. T operator++(int// 返回值  
  10. {  
  11.      T oldT(*this); // 先保存原值  
  12.       ++(*this);  // 调用前缀形式执行递增  
  13.       return oldT; //  返回原值  
  14. }  

 

第29条(D):考虑重载以避免隐含类型转换

第30条(A):避免重载操作符&&, || 和,(逗号)

第31条(A):不要编写依赖于函数参数求值顺序的代码

     调用函数时,参数的求值顺序是悬而未定的。

《C++编程规范——101条规则、准则与最佳实践》(C++ Coding Standards——101 Rules, Guidelines and Best Practices)
类的设计与继承

第32条(C):弄清所要编写的是哪种类
第33条(C):用小类代替巨类

    分而治之。用类表示概念。

第34条(B):用组合代替继承

     即优先使用委托而非继承。

第35条(B):避免从并非要设计成基类的类中继承

    本意是要独立使用的类所遵守的设计蓝图和基类大不相同,将独立类用作基类是一种严重的设计错误。

第36条(C):优先提供抽象接口

    偏爱抽象艺术吧。抽象接口是完全由(纯)虚函数构成的抽象类,没有状态(即没有成员数据),通常也没有成员函数实现。注意:在抽象接口中避免使用状态能够简化整个层次结构的设计。

    依赖倒置原理(DIP):1)高层模块不应该依赖低层模块。相反,两者都应该依赖于抽象。2)抽象不应该依赖细节(实现)。相反,细节应该依赖抽象。通常说的“要面向接口编程,而不要面向实现编程”也是这个意思。

第37条(C):公有继承即可替换性。继承,不是为了重用,而是为了被重用

    继承塑模的是“是一个(is a kind of )”关系【Liskov替换原则】。组合塑模的是“有一个(has a kind of )”关系。

第38条(D):实施安全的覆盖

第39条(D):考虑将虚拟函数声明为非公有的,将公有函数声明为非虚拟的

     在基类中进行修改代价是高昂的(尤其对于框架或库)。非虚拟接口模式(NonVirtual Interface, NVI)。

第40条(A):要避免提供隐式转换

     explicit构造函数和命名的转换函数。

第41条(A):将数据成员设为私有的,无行为的聚集除外(即C语言形式的struct)

     保护数据具有公有数据的所有缺点。可以考虑使用Pimpl惯用法用来隐藏类的私有数据成员。

第42条(C):不要公开内部数据

     隐藏数据却又暴露句柄是一种自欺欺人的做法,就像锁上了自家的门,却把钥匙留在了锁上。

第43条(D):明智的使用Pimpl惯用法

[cpp]  view plain copy
  1. // 将类的私有数据隐藏在一个不透明的指针后面  
  2. class Map  
  3. {  
  4.  public:  
  5.    // .... 接口  
  6. private:  
  7.       struct PrivateImpl; // 类Map的嵌套类型  
  8.      shared_ptr<PrivateImpl> m_Impl;  
  9. };  


第44条(D):优先编写非成员非友元函数    

       要避免较成员费:尽可能优先指定为非成员非友元函数。1)减少依赖;2)分离巨类;3)提供通用性。

第45条(D):总是一起提供new和delete

第46条(D):如果提供类专门的new,应该提供所有的标准形式(普通,就地和不抛出)

构造、析构与复制

第47条(A):以同样的顺序定义和初始化成员变量     

       如果违反了该条规则,也会违反第1条 在高警告级别下干净利落地进行编译。

第48条(A):在构造函数中用初始化代替赋值     

       这可不是不成熟的优化,这是在避免不成熟的劣化。

第49条(B):避免在构造函数和析构函数中调用虚拟函数     

        这一点其实很好理解:因为在构造期间,对象还是不完整的,如果在基类的构造函数中调用了虚拟函数,那么调用的将是基类的虚拟函数(不管派生类是否对该虚拟函数进行了改写)。C++标准为什么这样?试想:如果调用的是派生类改写后的虚拟函数版本,那么会发生什么事情?派生类改写该虚拟函数势必会调用派生类的成员数据吧?而在构造基类期间,派生类的数据成员还没有被初始化,使用了未初始化的数据,正是通往未定义行为的快速列车。

[cpp]  view plain copy
  1. // 使用工厂函数插入“后构造函数”调用  
  2. class B  
  3. {  
  4.   protect:  
  5.    B() {/*.... */ }  
  6.    virtual void PostInitialize() {/*....*/}  
  7.   public:  
  8.     template<typename T>  
  9.    static shared_ptr<T> create() // 函数模板  
  10.    {  
  11.      shared_ptr<T> p(new T);  
  12.      p->PostInitialize();  
  13.      return p;  
  14.    }  
  15. };  
  16.   
  17. class D : public B  
  18. {  
  19. // ....  
  20. };  
  21.   
  22. shared_ptr<D> pD = D::create<D>();  // 创建一个D的对象  


第50条(A):将基类析构函数设为公用且虚拟的,或者保护且非虚拟的

第51条(D):析构函数、释放和交换绝对不能失败

第52条(D):一致地进行复制和销毁   

        通常,拷贝构造函数,复制赋值操作符函数,析构函数要么都定义,要么都不定义。

第53条(D):显示地启用或者禁止复制

第54条(D):避免切片。在基类中考虑用克隆代替复制

       将基类的拷贝构造函数声明为受保护的protected, 这样就不能将派生类对象直接传递给接收基类对象的函数,从而防止了对象切片。取而代之在基类中增加一个克隆函数clone()的定义,并采用NVI模式实现。在公有的非虚拟接口clone()函数中采用断言检查继承自基类的所有派生类是否忘记了重写virtual B *doClone()。

第55条(D):使用赋值的标准形式

第56条(D):只要可行,就提供不会失败的swap()(而且要正确的提供)

模板与泛型

第64条(C):理智的结合静态多态性和动态多态性

       动态多态性是以某些类的继承体系出现的,通过虚拟函数和(指向继承层次中的对象的)指针或引用来实现的。静态多态性则是通过类模板和函数模板实现。

第65条(D):有意的进行显示自定义

第66条(D):不要特化函数模板

第67条(D):不要无意地编写不通用的代码

STL:容器

第76条(A):默认时使用vetor。否则,选择其他合适的容器

第77条(B):从vector和string代替数组

第78条(A):使用vector和string::c_str与非C++的API交换数据

       vector的存储区总是连续的;大多数标准库对string的实现,也使用连续内存区(但是不能得到保证)。string::c_str总是返回一个空字符'\0'结束的C风格字符串。string::data也是返回指向连续内存的指针,但不保证以空字符'\0'结束。

第79条(D):在容器中只存储值和智能指针

第80条(B):用push_pack代替其他扩展序列的方式

第81条(D):多用范围操作,少用单元素操作

第82条(D):使用公认的惯用法真正的压缩容量,真正的删除元素

container<T>(c).swap(c); // 去除多余容量的

shrink-to-fit惯用法container<T>().swap(c); // 清空容器c

c.erase(remove(c.begin(), c.end(), value), c.end()); // 删除容器c中所有等于value的元素, erase-remove惯用法

STL:算法

算法即循环——只是更好。算法是循环的模式。开始使用算法,也就意味着开始使用函数对象和谓词。

第83条(D):使用带检查的STL实现

      什么是带检查的STL实现?

第84条(C):用算法调用代替手工编写的循环

      有意识的熟悉,使用STL算法吧。

第85条(C):使用正确的STL查找算法

      find/find_if, count/count_if, binary_search, lower_bound, upper_bound, equal_range

第86条(C):使用正确的STL排序算法

      partition, stable_partition, nth_element, partial_sort, partial_sort_copy, sort, stable_sort

第87条(C):使谓词成为纯函数

第88条(C):算法和比较器的参数应多用函数对象少用函数

第89条(D):正确编写函数对象 

       模板与泛型编程,C++标准模板库STL一直是自己很薄弱的地方,因为在工作中很少使用。这一来是因为自己起初就对这一块不熟悉,进而导致编程时很少使用(都不知道用有哪些功能啊),而越是这样,使用得越少,就更没有机会是熟悉STL,正是形成一个循环。STL有很多的实用功能,以后要有意识的加以使用,学习,争取掌握它。

名字空间与模块

第57条(D):将类型及其非成员函数接口置于同一名字空间中

第58条(D):应该将类型和函数分别置于不同的名字空间中,除非有意想让他们一起工作

       ADL(参数依赖查找,也成Koeing查找)。

      关于57条和58条,在机器上实验了一下,没发现啥问题呀。???

第59条(A):不要在头文件中或者#include之前编写名字空间using

       名字空间 using 是为了使我们更方便,而不是让我们用来叨扰别人的:在 #include 之前,绝对不要编写 using 声明或者 using 指令。
       推论:在头文件中,不要编写名字空间级的 using 指令或者 using 声明,相反应该显式地用名字空间限定所有的名字。(第二条规则是从第一条直接得出的,因为头文件无法知道以后其他头文件会出现什么样的 #include 。) 简而言之:可以而且应该在实现文件中的 #include 指令之后自由地使用名字空间级的 using 声明和指令,而且会感觉良好。
第60条(D):要避免在不同的模块中分配和释放内存

第61条(A):不要在头文件中定义具有链接的实体

[cpp]  view plain copy
  1. // 不要在头文件中定义具有外部链接的实体  
  2. int fudgeFactor;  
  3. std::string hello("hi, lcz");  
  4. void foo() { std::cout << "lcz" << std::endl; }  


而解决方法也很简单,应该像如下只在头文件中声明:

[cpp]  view plain copy
  1. extern int fudgeFactor;  
  2. extern std::string hello;  
  3. void foo(); // extern 对函数的声明是可有可无的  


在实现文件中定义:

[cpp]  view plain copy
  1. int fudgeFactor;  
  2. std::string hello("hi, lcz");  
  3. void foo() { std::cout << "lcz" << std::endl; }  


同样的,以下在头文件中定义名字空间级的static实体是更危险的行为(因为链接器通常不会报错):

[cpp]  view plain copy
  1. static int fudgeFactor;  
  2. static std::string hello("hi, lcz");  
  3. static void foo() { std::cout << "lcz" << std::endl; }  


第62条:不要允许异常跨越模块边界传播

第63条(D):在模块的接口中使用具有良好可移植性的类型

错误处理与异常

第68条(B):广泛地使用断言记录内部假设和不变式

     断言的强大怎么高估都不算过分。1)按照信息论的原理,一个事件中所包含的信息量与该事件发生的概率是成反比的。因此,如果assert触发的可能性越低,它触发时所提供的信息量就越大。2)避免使用assert(false),应该使用assert( !"information message" ); // 这样有一个很有用的好处,可以取代注释。3)断言是用来报告程序员的错误的,因此,不要使用断言报告运行时的错误。

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

第70条:区别错误与非错误

第71条:设计和编写错误安全代码

第72条:优先使用异常报告错误

第73条:通过值抛出,通过引用捕获

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

第75条:避免使用异常规范

    错误处理与异常这一块在项目中实践得很肤浅,没有什么心得与体会。一般都是遇到错误了即返回,抛出错误日志。

类型安全

第90条(C):避免使用类型分支,多使用多态

第91条(C):依赖类型,而非其表示方式

第92条(A):避免使用reinterpret_cast

     如果需要在不相关的指针类型之间强制转换,应该通过void进行转换,不要直接使用reinterpret_cast.

例如:

[cpp]  view plain copy
  1. T1 *p1 = ...  
  2. T2 *p2 = reinterpret<T2*>(p1); // 糟糕!   


应该写成:

[cpp]  view plain copy
  1. T1 *p1 = ....  
  2. void *p = p1; // 先用void*指向p1指向的内存区  
  3. T2 *p2 = static_cast<T2>(p);    


第93条(A):避免对指针使用static_cast

第94条(A):避免强制转换const(const_cast)

第95条(D):不要使用C风格的强制转换

第96条(D):不要对非POD进行 memcpy 和 memcmp 操作

第97条(C):不要使用联合(union)重新解释表示方式

     有两种方式是可以接受的:1)读取最后写入的字段(在一些接口需要统一参数而实际参数类型又不相同的时候,经常使用这一招);2)如果两个POD是一union的成员,而且均以相同的字段类型开始,那么对这种匹配的字段来说,写入其中一个而读取另一个是合法的。

第98条(A):不要使用可变长参数(...)

     就算要用,我也不会编写。呵呵

第99条(B):不要使用失效对象。不要使用不安全函数

    不要使用不安全的C语言遗留函数:strcpy, strncpy, sprintf等C函数。

第100条(B):不要多态地处理数组

    1)避免使用数组,而应该使用vector; 2)不要在数组或vector中存储多态的值对象,而应该存储对象指针(最好是智能指针)。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值