《C++编程规范》读书笔记

本文探讨了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有很多的实用功能,以后要有意识的加以使用,学习,争取掌握它。


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

名字空间与模块

第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中存储多态的值对象,而应该存储对象指针(最好是智能指针)。



组织和策略问题 1 第0 不要拘泥于小节(又名:了解哪些东西不应该标准化) 2 第1 在高警告级别干净利落地进行编译 4 第2 使用自动构建系统 7 第3 使用版本控制系统 8 第4 做代码审查 9设计风格 11 第5 一个实体应该只有一个紧凑的职责 12 第6 正确、简单和清晰第一 13 第7 编程中应知道何时和如何考虑可伸缩性 14 第8 不要进行不成熟的优化 16 第9 不要进行不成熟的劣化 18 第10 尽量减少全局和共享数据 19 第11 隐藏信息 20 第12 懂得何时和如何进行并发性编程 21 第13 确保资源为对象所拥有。使用显式的RAII和智能指针 24 编程风格 27 第14 宁要编译时和连接时错误,也不要运行时错误 28 第15 积极使用const 30 第16 避免使用宏 32 第17 避免使用“魔数” 34 第18 尽可能局部地声明变量 35 第19 总是初始化变量 36 第20 避免函数过长,避免嵌套过深 38 第21 避免跨编译单元的初始化依赖 39 第22 尽量减少定义性依赖。避免循环依赖 40 第23 头文件应该自给自足 42 第24 总是编写内部#include保护符,决不要编写外部#include保护符 43 函数操作符 45 第25 正确地选择通过值、(智能)指针或者引用传递参数 46 第26 保持重载操作符的自然语义 47 第27 优先使用算术操作符和赋值操作符的标准形式 48 第28 优先使用++和--的标准形式。优先调用前缀形式 50 第29 考虑重载以避免隐含类型转换 51 第30 避免重载&&、||或 ,(逗号) 52 第31 不要编写依赖于函数参数求值顺序的代码 54 类的设计继承 55 第32 弄清所要编写的是哪种类 56 第33 用小类代替巨类 57 第34 用组合代替继承 58 第35 避免从并非要设计成基类的类中继承 60 第36 优先提供抽象接口 62 第37 公用继承即可替换性。继承,不是为了重用,而是为了被重用 64 第38 实施安全的覆盖 66 第39 考虑将虚拟函数声明为非公用的,将公用函数声明为非虚拟的 68
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值