牵着老婆满街逛

严以律己,宽以待人. 三思而后行.
GMail/GTalk: yanglinbo#google.com;
MSN/Email: tx7do#yahoo.com.cn;
QQ: 3 0 3 3 9 6 9 2 0 .

C++ 编程指南

版权所有 © 1997 Rational Software Corporation。
保留所有权利。

“Rational”一词和 Rational 产品名是 Rational Software Corporation 的商标。涉及到其他公司及其产品时将使用各自公司的商标且仅限于此目的。

此文档由来自 Calypso Software Inc. (Vancouver, B.C., Canada) 的 Luan Doan-Minh 为 Rational Software Corp. 编写。


目录

简介

基本原则
前提
指南分类
最根本的原则

代码组织与风格

代码结构
代码风格

注释

命名

概述
名字空间

函数
对象与函数参数
异常
其他事项

声明

名字空间

函数
类型
常量与对象

表达式和语句

表达式
语句

特殊主题

内存管理
错误处理和异常

可移植性

路径名
数据表示
类型转换

复用

编译问题

指南总结

要求或限制
建议
提示

参考文献


第一章

简介

大型软件项目通常由相应的大型开发团队承担。大型团队生成的代码要有项目范围内可评测的质量,代码必须遵从于某一标准并以此来评价。因此,对大型的项目团队来说,建立一个编程的标准或一组指南很重要。
使用编程标准也使以下各项成为可能:

  • 增加开发过程代码的强壮性、可读性、易维护性;减少有经验和无经验开发人员编程所需的脑力工作;
  • 在项目范围内统一代码风格;
  • 通过人为以及自动的方式对最终软件应用质量标准;
  • 使新的开发人员快速适应项目氛围;
  • 支持项目资源的复用:允许开发人员从一个项目区域(或子项目团队)移动到另一个,而不需要重新适应新的子项目团队的氛围。

本文的目的是表述 C++ 编程的规则、指南和提示(通常也称之为指南),它们可用来作为编程标准的基础。对工作在大型项目团队的软件工程师,这些都是需要的。
当前版本专注于程序的编制(虽然现在还很难在设计和编程间划定明确的界限);以后将会添加设计指南。
指南包括了以下 C++ 开发的方面:

  • 如何组织项目代码;
  • 编程风格(如何编写实际的源代码);
  • 如何记录源代码;
  • 代码内名称和源文件所使用的命名约定;
  • 何时使用某些语言结构以及何时应避免某些语言结构。

它们从大量的行业知识中搜集而来。(请参见参考文献:作者及参考文献。)它们基于:

  • 广为人知的软件原理;
  • “好的”软件实现;
  • 所学课程;
  • 主观意愿。

大多会基于几个第一种类型,也会基于大量的第二种和第三种类型。不幸的是,某些也基于最后一种类型;这主要是因为编程是一个高度主观的活动:编程没有普遍接受的最好或正确的万灵药。

基本原则

清晰、可理解的 C++ 源代码是规则和指南的主要目标:清晰、可理解的源代码是软件可靠性和可维护性的主要作用因素。清晰、可理解的代码可以表示为以下三个简单的基础原理 [Kruchten, 94]
最小混淆 - 它的生存期中,源代码的读远比写多,规约更是这样。理想情况下,源代码读起来应该象英语一样描述了所要做的事,这同时还带来了它执行的好处。程序更多是为人编写,而不是为计算机而编写。阅读代码是一个复杂的脑力过程,它可由统一标准来简化,在本文中还指最小混淆原则。整个项目中统一样式是软件开发团队在编程标准上达成一致的主要原因,它不应视为一种惩罚或对创造性和生产力的阻碍。
维护的唯一点 - 只要可能,设计决策就应在源中只表述一点,它的多数后果应程序化的派生于此点。不遵守这一原则严重损害了可维护性、可靠性和可理解性。
最小干扰 - 最终,应用最小干扰原则(它是易读性的主要作用因素)。即,避免将源代码与可视干扰(如内容较少或对理解软件目的不起作用的信息)相混合:
指南这里所要表达的精神是不要过于苛刻;而对正确安全的使用语言特性提供指导。优秀软件的关键在于:
了解每一个特性以及它的限制和潜在的危险;
确切了解此特性可安全的使用于哪一个环境中;
做出使用高度可视特性的决定;
在合适的地方小心适度的使用特性。

前提

这里的指南只有少数几个基本前提:
读者了解 C++。
任何有益的地方都鼓励使用 C++ 的高级特性,而不是只允许使用一些程序员都不熟悉的低级特性。这是项目能从使用 C++ 中获益的唯一方式。C++ 不应只当成 C 来使用,事实上 C++ 的面向对象特性使它不会象 C 一样使用。不鼓励将代码直译成注释;相反,在任何可能的地方应当使用源代码代替注释。
遵从大型项目的做法。
即使只是为了项目一级或公司一级的实现和统一,大型项目中许多规则仍很有价值,它们在小型系统中也有使用。
代码遵从于一个面向对象设计。
许多规则都会支持从面向对象 (OO) 概念到 C++ 特性和具体命名约定的系统映射。

指南分类

指南具有不同的重要性;其重要性由以下标准来衡量:


提示:
以上符号所确定的指南只是一个小提示或小建议,即使忽略它也没关系。
建议:
以上符号所确定的指南是一个建议,它通常是建立在更加技术性的基础之上:封装、内聚、耦合、可移植性或可复用性与某些实现中的性能一样都会受到影响。除非有合适的理由,否则必须遵从建议。

要求或限制:
以上符号确定的一个指南就是一个要求或限制;不遵从它肯定会导致坏的、不可靠的或不可移植的代码。没有弃权就不能违反要求或限制

最根本的原则

使用常识

当无法找到可用的规则或指南时;当规则明显不适用时;或当其他都已失败时:使用常识,并核查基本原则。这条规则胜过其它所有的规则。即使存在规则或指南时也需要常识。


第二章

代码组织与风格

本章提供程序结构与层次的指导。

代码结构

开发大型系统时通常将它分成许多小的功能子系统。子系统本身由许多代码模块组成。在 C++ 中,一个模块通常包含了实现单一一个,少数情况下,也可能是一组紧密相关的抽象。C++ 中,一个抽象通常实现为一个。一个类有两个不同的构件:一个是对类客户可见的接口,它提供了类能力和责任的声明或规约;另一个是所声明规约(类定义)的实现
与类相似,模块也有它的接口和实现:模块接口包括对所包含模块抽象(类声明)的规约;模块实现包括对抽象(类定义)的实际实现。
在系统构造时,可将子系统组织成协作的组或层来减少或控制它们间的依赖性。

不同文件中放置模块规约与实现

模块的规约应放置在与模块实施文件不同的文件中,此文件指头文件。模块的实施文件可能放置于一个或多个实施文件中。
如果模块实现包括扩展内嵌函数、普通私有实现声明、测试代码或具体平台的代码,那就把这些部分分开存储,并以那部分内容来命名。
如果程序的可执行大小是一个要考虑的问题,则不常用到的函数应当置于它们各自的文件中。
以以下方式构造一部分文件名:

  • 以模块的主要抽象名作为模块名。
  • 为模块名附加一部分类型名。选择暗示它们类型的部分类型名。
  • 模块名和部分名由分隔符分隔(如“_”(下划线)或“.”(句点));选择一个分隔符,使用它要前后一致。

    File_Name::=

    <Module_Name> [<Separator> <Part_Name>] '.'<File_Extension>

  • 为了更好的预测,对文件名使用相同的大小写,这与代码内名称的使用相同。

以下是模块划分与命名策略的示例:

  • module.inlines.cc - 如果一个模块有多个潜在的内嵌函数,就将函数的定义置于一个单独的内嵌文件中(请参见“将模块内嵌函数定义置于单独文件中”)。
  • module.private.hh - 如果模块有许多常用的被其他部分引用的私有实现声明,就把这些声明分隔出去组成一个私有部分,它可由其他实施文件包含。
  • module.private.cc - 模块的私有实施函数定义,为编辑的方便将它分离出去。
  • module.function_name.cc - 如果可执行的大小是个要考虑的问题,应当分离出许多程序不需要的特殊成员函数,组成各自的实施文件(请参见“如果程序大小需要考虑,将大型模块分成多个变换单元”)。如果重载的函数置于不同文件中,每一文件函数名都应有一实例数字的后缀。如,function_name1 表示第一个独立重载函数实例。
  • module.nested_class.cc - 模块嵌套类的成员函数,置于其本身文件中。
  • module.test.[hh\cc] - 如果一个模块需要扩展测试代码,则测试代码必须在友测试类中予以声明。友测试类应称为Module.Test。将测试代码声明为友类有助于模块及其测试代码的独立开发;这还允许测试代码从最终模块对象代码中删除而源并不改变。
  • module.platform_name.cc - 分离出任意模块的平台依赖性并以平台名称命名部分名称(请参见“分离平台依赖性”)。

选择一个模块划分和命名的机制,使用它要前后一致。

示例
SymaNetwork.hh         //包括类
                       // “SymaNetwork”的声明。
SymaNetwork.Inlines.cc //内嵌定义子单元
SymaNetwork.cc         //模块的主要实施单元
SymaNetwork.Private.cc //私有实施子单元
SymaNetwork.Test.cc    //测试代码子单元
基本原理

从模块的实施中分离出其规约有助于用户和提供者代码的独立开发。
将一个模块的实施分割成多个变换单元,这可以更好的支持删除对象代码,并会导致更小的可执行大小。
使用规范化的文件命名与划分约定允许在不检查模块的实际内容时理解其内容与组织。
将名称从代码让渡给文件名增加了可预测性,有助于建立基于文件的工具而不需要复杂命名映射 [Ellemtel, 1993]

只选择一组文件扩展名以区分头文件和实施文件

常用的文件扩展名是:对头文件,为.h、.H、.hh、.hpp.hxx;对实施文件,为.c、.C、.cc、.cpp和.cxx。选择一组扩展名,使用它要前后一致。

示例
SymaNetwork.hh  //扩展名“.hh”用来指定
                //一个“SymaNetwork”模块的头文件。
SymaNetwork.cc  //扩展名“.cc”用来指定
                //一个“SymaNetwork”模块的实施文件
注意

对由名字空间封装的头文件,C++ 草拟标准工作底稿也使用扩展名“.ns”。

每个模块规约避免定义多个类


只有在极少数情况下多个类才能置于同一个模块中;此时他们必须是紧密关联关系的(如容器与它的迭代程序)如果所有类都需要对客户模块是可见的,可以把模块的主要类及其支持类置于同一个头文件中。

基本原理

减少模块的接口以及其他模块对此接口的依赖。

避免将私有实施声明置于模块的规约中

除了类的私有成员以外,模块的私有实施声明(如实施类型和支持类)不能出现在模块的规约中。除非多个实施文件都需要这些声明,否则它们应当置于所需的实施文件中;如果有多个实施文件,则这些声明应置于辅助私有头文件中。而其他实施文件在需要时应包含这个辅助私有头文件。
这一做法保证了:
模块规约清楚的表示了它的抽象,不再需要实施工件;
模块规约越小越好,并因此减小了模块间的编译依赖(请参见“减小编译依赖”);

示例
//模块 foo 的规约,包含于文件“foo.hh”中;
//
class foo
{
.. 声明
};
//“foo.hh”结束;
//模块 foo 的私有声明,包含于文件
// “foo.private.hh”中;所有 foo 实施文件都使用它。
... 私有声明
//“foo.private.hh”结束;
//模块 foo 的实施,包含于以下多个文件中
// “foo.x.cc”和“foo.y.cc”
//文件“foo.x.cc”;
//
#include "foo.hh" //包含模块本身的头文件
#include "foo.private.hh" //包含实施
//所需的声明。
... 定义
//“foo.x.cc”结束
//文件“foo.y.cc”;
//
#include "foo.hh"
#include "foo.private.hh"
... 定义
//“foo.y.cc”结束

通常使用 #include 来访问模块的规约

模块要使用另一模块,则必须使用预处理 #include 指令来访问提供者模块的规约。相应的,模块不能再次声明提供者模块规约的任何一部分。
当包含文件时,只有对“标准”头文件使用 #include <header> 语法;对其余的使用 #include "header" 语法。
使用 #include 指令也适用于模块自身的实施文件:模块实施文件必须包括它本身的规约和私有辅助头文件(请参见“不同文件中放置模块规约与实施”);

示例
//模块 foo 头文件中的规约
// "foo.hh"
//
class foo
{
... 声明
};
//“foo.hh”结束;

//模块 foo 在文件“foo.cc”中的实施;
//
#include "foo.hh" // 实施文件包含
//它本身的规约
... foo 成员的定义
//“foo.cc”结束

#include 规则的一个例外是当模块只通过引用(使用指针或引用类型声明)使用或包含提供者模块的类型(类);这种情况下引用或包含通过使用预先声明来规约(请参见“减小编译依赖”),而不使用 #include 指令。
只包含绝对需要的文件:这意味着模块头文件不应包含只有模块实施需要的其他头文件。

示例
#include "a_supplier.hh"

class needed_only_by_reference;//对只需要
		            //指针或引用的类
                              //使用
                              //预先声明来访问
void operation_requiring_object(a_supplier required_supplier, ...);
//
//操作需要一个实际的提供者对象;
//提供者规约必须已有 #include 指令。

void some_operation(needed_only_by_reference& a_reference, ...);
//
//某些操作只需要对对象进行引用;
//因此为提供者使用预先声明。
基本原理

这一规则保证了:

  • 只存在唯一的模块接口声明,所有的客户只看到同一接口;
  • 模块间的编译依赖最大程度的减小了;
  • 客户不会为代码无故产生不需要的编译开支。

将模块内嵌函数的定义置于单独的文件中

当模块有许多内嵌函数时,它们的定义应置于一个分离的只有内嵌函数的文件中。在模块头文件的末尾应包含内嵌函数文件。
请参见“使用 No_Inline 条件编译符破坏内嵌编译”。

基本原理

这种技术使实施细节不会聚集到模块头文件中;因此,保留了一个清晰的规约。当不进内嵌编译时,它也有助于减少代码复制:使用条件编译,可将内嵌函数编译成一个对象文件而不是静态的编译成每一个使用的模块。相应的,内嵌函数定义不应在类定义中定义,除非它们非常琐碎。

如果程序规模是个要求考虑的问题,就把大的模块分割成多个变换单元

将大型模块分割成多个变换单元有助于在程序链接中删除未经引用的代码。很少引用的成员函数应当分割成独立文件,与经常使用的函数相分离。最后,各成员函数可置于他们自己的文件中 [Ellemtel, 1993]

基本原理

链接器在对象文件中消除无引用代码的能力各不相同。将大型模块分割成多个文件允许这些链接器通过消除整个对象文件中的链接来减少可执行大小 [Ellemtel, 1993]

注意

先考虑模块是否能分割成小的抽象,这也是很值得的。

分离平台依赖性

从独立于平台的代码中分离出依赖于平台的代码;这有助于提高可移植性。依赖于平台的模块应当具有受平台名限制的文件名以突出对平台的依赖。

示例
SymaLowLevelStuff.hh         //“LowLevelStuff”
                             //规约
SymaLowLevelStuff.SunOS54.cc // SunOS 5.4 实施
SymaLowLevelStuff.HPUX.cc    // HP-UX 实施
SymaLowLevelStuff.AIX.cc     // AIX 实施
注意

从构架与维护角度,将对平台的依赖包含在少数几个低层子系统中也是一个好方法。
采纳一个标准文件内容结构,使用它要前后一致。
建议文件内容就构包括以下次序的以下部分:

    1. 重复包含保护(只适用于规约)。
    2. 确定可选文件与版本控制。
    3. 本单元所需包含的文件。
    4. 模块纪录(只适用于规约)。
    5. 声明(类、类型、常量、对象、函数)和其他文本规约(前置及后置条件,常量)。
    6. 对这种模块内嵌函数定义的包含。
    7. 定义(对象与函数)和私有实施声明。
    8. 版权声明。
    9. 可选版本控制历史。

基本原理

以上文件的内容次序,首先呈现给了客户相关的信息;这与安排类的公共、保护和私有部分次序的基本原理相一致。

注意

根据公司的政策,版权信息应置于文件的首部。

防止重复文件包含的保护

通过在每个头文件中使用以下结构可以防止重复包含与编译文件:

#if !defined(module_name) //使用预处理符
#define module_name       //防止重复
                          //包含...//声明到此
#include "module_name.inlines.cc" //可选的内嵌结构
                                  //包含到此。
//包含模块内嵌函数之后
//没有其他声明。
#endif //module_name.hh 结束

对包含保护符使用模块文件名。对模块名与操作符使用同一大小写。

使用"No_Inline"条件编译符破坏内嵌编译

使用以下条件编译结构控制可内嵌函数的内嵌(相对于外部的)编译。

//在文件 module_name.inlines.hh 首部,
#if !defined(module_name_inlines)
#define module_name_inlines

#if defined(No_Inline)
#define inline //使内嵌关键词无效
#endif

... //内嵌定义到此
#endif //文件 module_name.inlines.hh 结束

//文件 module_name.hh 尾
//
#if !defined(No_Inline)
#include "module_name.inlines.hh"
#endif

//包含module_name.hh之后
// module_name.cc 文件首
//
#if defined(No_Inline)
#include "module_name.inlines.hh"
#endif

条件编译结构与多包含保护结构相似。如果未定义 No_Inline,则内嵌函数与模块规约一同编译并自动从模块实施中排除出去。如果定义了 No_Inline,则模块规约不包含内嵌定义,但废除 inline 关键词的模块实施包含了它。

基本原理

以上技术允许内嵌函数在外部编译时的精简代码复制。使用条件编译,内嵌函数的简单复制编译成了定义模块;而当由编译器开关规约外部编译时,所复制的代码在每个使用的模块里编译成“静态”(内部链接)函数。

注意

条件编译增加了维护构建依赖关系的复杂性。对这种复杂性的管理通常是利用把头文件和内嵌函数定义视为一个逻辑单元:实施文件因此依赖于头文件以及内嵌函数定义文件。

代码风格

对嵌套语句使用小的、一致的缩进风格

使用一致缩进来清楚的界定嵌套语句;为达到这一目的,两到四个空格的缩进是最有效的方式。我们推荐使用规则的两空格缩进。
混合语句或块语句的分界符 ({}),应当与周围的语句处于缩进的同一级上(这意味着垂直布置{})。通过选择空格的数量对块内的语句进行缩进。
switch 语句的 case 标号应当与 switch 语句处于同一缩进级;switch 语句内语句应比 switch 语句本身和 case 标号多缩进一级。

示例
if (true)
{  		//新程序块
foo(); //程序块内语句
       //缩进两个空格
}
else
{
bar();
}
while (expression)
{
statement();
}
switch (i)
{
case 1:
do_something();//相对 switch 语句本身
               //缩进
break;         //一层
case 2:
//...
default:
//...
}            
基本原理

能够轻易识别程序块,还要在不超出显示屏或打印页面界限的前提下有足够多的嵌套,使用两空格的缩进是一个折衷的方案。

相对于函数名或作用域名缩进函数参数

如果一行之内难以容下函数声明,则将函数的第一个参数放在函数名所在的那一行;其余参数每个占一新行,使用与第一个参数相同的缩进。下面所示的声明与缩进的风格,在函数返回类型和名称下留有空格;因此,提高了它们的可见性。

示例
void foo::function_decl( some_type first_parameter, 
some_other_type second_parameter,
status_type and_subsequent);

如果按照以上的指南会导致换行,或参数缩进的太多,则以函数名或作用域名(类、名字空间)缩进所有的参数,每一参数独占一行:

示例
void foo::function_with_a_long_name( //函数名是
                                     //不易看到的
                                     some_type first_parameter, 
                                     some_other_type second_parameter,
                                     status_type and_subsequent);

请参照下面的布置规则。

使用能够适合标准打印纸大小的最大行宽

应限制程序行的宽度以防止在使用标准(信纸)或缺省打印纸张进行打印时丢失信息。

注意

如果缩进使深层的嵌套语句处于太右边,语句又太长以致超出了右边的空白,这时就应当考虑将代码分成更小的更易管理的函数。

使用一致换行

当函数声明、定义、调用或 enum 声明中枚举操作符的参数列表不能置于一行,则将每一个列表元素置于单独的一行(请参见“相对函数名和作用域缩进函数参数”)。

示例
enum color { red, 
orange, 
yellow, 
green, 
             //...
violet
                   };            

如果一个类或函数模板声明太长,则在模板的实参列表后进行连续换行。例如(标准迭代程序库的声明,[X3J16, 95]):

template <class InputIterator, class Distance>
void advance(InputIterator& i, Distance n);



第三章

注释

本章对代码注释的使用提供指导。
注释应当作为源代码的补充,而不是直译源代码:

  • 它们应当解释不能直接从源代码看出东西;它们不应复制语言的语法或语义。
  • 它们应当帮助读者掌握背景中的概念、依赖性、特别是复杂的数据代码和算法。
  • 它们应当突出:与代码或设计标准的不同点、受限特性的使用、以及特殊的“技巧”。

对每一行注释,程序员都应能够轻松回答:这一注释有何价值?通常,合理选择名称就可不要注释。除非注释出现在正规的程序设计语言中 (PDL),否则编译器不对其进行编译;因此,根据单点维护原则,应当以源代码的方式表达设计决策,而不是以注释的方式,即使这样需要更多的声明。

使用 C++ 风格的注释而非 C 风格的注释

应使用 C++ 风格注释分界符"//",而非 C 风格的"/*...*/"。

基本原理

C++ 风格的注释更易理解,它减少了由于偶然缺少一个注释结束分隔符而造成大量代码被注释掉的风险。

反例
/*注释开始,但缺少注释结束分隔符
do_something();
do_something_else(); /*对 do_something_else 的注释*/
                          //注释到此结束。
                          // do_something 和
                          // do_something_else
                          //都意外的被注释掉了!
Do_further();

注释与源代码尽可能靠拢

注释应紧贴它们所要注释的代码;它们使用相同的缩进,使用一个空注释行接于代码之后。
对多行连续语句的注释应置于语句的上方作为语句的介绍性说明。而对单个语句的注释应置于语句的下方

示例
//置于语句前的注释
//它对下面多个语句进行注释
// 
...
void function();
//
//语句之后的注释
//它对前一个语句进行注释。

避免行末注释

注释应避免与源结构处于同一行:否则会使注释与其所注释的源代码不对齐。但在描述长声明中的元素(如 enum 声明中的枚举操作符)时,也能容忍这种不好的注释方式。

避免注释头

避免使用包含作者、电话号码、创建和修改日期的注释头:作者和电话号码很快就过时了;而创建和修改日期以及修改的原因则最好由配置管理工具来维护(或其他形式的版本历史文件)。
即使对主结构(如函数和类),也要避免使用垂直滚动条,闭合的栏或框。它们搞乱了视觉效果,这样就很难保持一致性。使用空行而不是多个注释行来分割相关的源代码块。使用一个空行来分离函数或类中的结构。使用两个空行将函数与函数相分离。
框架或表单看起来具有较好的一致性,还提醒了程序员来注释代码,但它们会导致直译的风格 [Kruchten, 94]

使用空注释行分离注释段

在一个注释块中,使用空注释行而非空行来分割注释段

示例
//在下一段中
//需要继续这里的解释
//
//空注释行使得
//同一注释块中
//不同段间的界限分明。

避免冗余

注释中应避免重复程序标识符,避免复制别处有的信息(此时可使用一个指向信息的指针)。否则程序中的任何一处改动都可能需要多处进行相应的变动。如果其他地方没有进行所需的注释改动,将会导致误注释:这种结果比根本没有注释还要糟糕。

编写自记录代码而非注释

时刻注意要编写自记录代码而非注释。这可通过选择合理的名称、使用特殊的临时变量或重新安排代码的结构来实施。注意注释中的风格、语法和拼写。使用自然语言注释而不是电报或加密格式。

示例
替换如下代码:
do
{
...
} while (string_utility.locate(ch, str) != 0); 
//当找到时退出查找循环。
将以上代码改写成:
do
{
...
found_it = (string_utility.locate(ch, str) == 0);
} while (!found_it);

记录类与函数

虽然自记录代码比注释好;但通常还需要提供一些超出解释代码复杂部分以外的信息。至少需要记录以下信息:

  • 每个类的目的;
  • 函数名不能清楚说明它的目的时,则记录函数相应的目的;
  • 返回值的含义;如不可预测函数布尔返回值的含义:如,ture 值是否表示函数执行成功;
  • 出现异常的条件;
  • 如果有的话,参数的前置或后置条件;
  • 其他数据访问,特别是当数据被修改时:对有副作用的函数特别重要;
  • 合理使用类或函数所需的限制或其他信息;
  • 对类型和对象来说,语言无法表达的任何不变量或其他限制。
基本原理

与声明相结合的代码记录应当足以使客户使用代码;因为只使用 C++ 不能完全表达类、函数、类型和对象的完整语义,所以需要记录存档。


第四章

命名

本章为不同 C++ 实体命名指南。

概述

为程序实体(类、函数、类型、对象、常量、异常、名字空间)取一个好名称并不是一件容易的事。对中大型的应用程序来说,问题就显得更加困难:这里名称容易冲突,并且还缺少一些近义词来区分相似但不相同的概念,这就更增加了困难的程度。
使用名称约定可减少创造合适名称时的脑力劳动。除了这一好处以外,名称约定还带来了代码的一致性。一个有用的名称约定必须在以下方面提供向导:排版风格(或如何书写名称)以及名称的构建(或如何选择名称)。

选择一个名称约定,使用它要前后一致

只要能够前后一致,使用哪一个名称约定并不重要。命名的一致性比实际的约定重要的多:一致性支持“最小混淆”原则。
因为 C++ 是一个区分大小写的语言,并且在 C++ 应用界有多种不同的命名约定,几乎不可能实现命名的绝对一致。我们推荐基于主机的环境(如 UNIX 或 Windows)以及项目所使用的主要的库选择项目的命名约定;尽可能实现代码的一致性:

  • 主机使用 UNIX 且不常使用商业库(如 X Window 库、X Toolkit Intrinsics 和 Motif)的项目可能倾向于使用全部为小写字符、以下划线分隔的约定:这是 UNIX 系统调用以及 C++ 草拟标准工作底稿使用的约定。
  • 以商业库为中心的 UNIX 主机项目可能更倾向于使用大写风格,通常也称为 Smalltalk 风格 - 一种首字母大写,字间直接相连而无分隔符的书写风格。
  • Microsoft 基于 Windows 的项目可能会推荐使用并不常用的 Microsoft® “匈牙利”命名法我们不推荐使用这种命名风格,因为它违背了本文指南的基本原则。
注意:

细心的读者会发现本文的示例现在并未遵循所有的指南。这部分是因为示例的来源有多个,还因为希望减少篇幅。因此,没有严格遵照格式化的指南来做。但需声明:照我说的去做,不要学我的实际做法。

永远不要声明以一个或多个下划线 ('_') 开头的名称

开头带有一个下划线(“_”)的名称通常由库函数(“_main”和“_exit”)使用。开头带有两个下划线(“__”)或一个下划线后接一个大写字母的名称保留给编译器内部使用。
名称还要避免下划线相邻,否则很难识别下划线的确切个数。

避免使用只靠字母大小写才能区分的名称

只通过大小写才能区分的类型名称,它们间的差异是很难记忆的,也就很容易造成混淆。

避免使用缩写

应用领域中常用的缩写(如 FFT 指快速傅立叶变换),或在项目缩写清单中有定义的缩写,才能使用相应的缩写。否则,很有可能相似但不相同的缩写到处出现,之后就引进了混淆和错误(如将 track_identification 缩写成 trid、trck_id、tr_iden、tid、tr_ident等)。

避免使用后缀来表明程序语言结构

使用后缀划分实体种类(如对type(类型)使用 type,对 exceptions(异常)使用 error)对帮助理解代码通常并不有效。后缀如 arraystruct 还意味着一个具体的实现;在实现改变时(结构或数组表示的改变)对客户代码要么会有反面的结果,要么会起误导的作用。
但后缀在有限几种情况下也会有一定用处:

  • 当可供选择的标识符非常有限时,选择一个最佳名称并用后缀表明它的类型。
  • 当它表示一个应用领域的概念如 aircraft_type(飞行器类型)时。

选择清晰的、易辨认的、有意义的名称

从应用的角度选择名称,名词使用形容词修饰来提高局部(具体上下文)的含义。确保名称与其类型保持一致。
选择合适的名称,使以下结构:

object_name.function_name(...);
object_name->function_name(...);

易于阅读并有实际含义。
短名称或缩写名称打字速度快不是使用它们的可以接受的理由。单字母或很短几个字母的标识符通常出于选择不当或懒惰。但使用人们熟知的 E 作为自然对数的底数或 Pi 作为圆周率就是例外了。
不幸的是,编译器和支持的工具有时限制名称的长度。因此,应当注意:长名称不应只在结尾的几个字符有所不同,因为结尾的几个用于区分的字符可能会被这些工具截断。

示例
void set_color(color new_color)
{
...
the_color = new_color;
...
}
优于
void set_foreground_color(color fg)
和:
oid set_foreground_color(color foreground);{
...
the_foreground_color = foreground;
...
}      

第一个示例中名称的选择要优于其它两个:对 new_color 进行了限定使它与其类型一致;因此增强了函数的语义。
第二个示例中,读者直觉上会认为 fg 意指 foreground(前景);但是,任何一个好的编程风格都不应留给读者作直觉推导的余地。
第三个示例中,当使用参数 foreground(远离其声明)时,读者会认为 foreground 实际上就是指 foreground 的颜色。可以想象,它能代表任何一种可暗中转化为 color 的类型。

注意:

使用名词和形容词构成名称、确保名称与类型相符、遵循自然语言以增强代码的可读性和语义。

使用名称的正确拼写

英语字符名称部分应正确的拼写,遵守项目要求的形式,如使用一致的英国英语或美国英语,但不能同时使用。这对注释也同样适用。

布尔值使用正值谓词从句

对布尔值类型的对象、函数及函数实参使用正值形式的判定句式,如 found_itis_available,但不使用 is_not_available

基本原理

当否定谓词时,双重否定更难以理解。

名字空间

使用名字空间由子系统或库划分潜在全局名称

如果将系统解构成子系统,使用子系统名称作为名字空间的名称,这样可划分系统的全局名字空间并使其最小化。如果系统是一个库,对整个库使用一个最外层的名字空间。
为每一个子系统或库名字空间取一个有意义的名称;此外,为它取一个简化的或缩写的别名。选择不会引起冲突的简写或缩写的别名,如 ANSI C++ draft standard library(ANSI C++ 草拟标准库)[Plauger, 95]std 定义为 iso_standard_library 的别名。
如果编译器尚未支持名字空间结构,使用名称前缀模拟名字空间。例如,系统管理子系统接口中的公共名称应带有前缀 syma (System Management(系统管理)的简写)。

基本原理

使用名字空间包含可能的全局名称,这样有助于子项目团队或厂商独立开发代码时避免名称冲突。这必然导致只有名字空间是全局的。

类的名称使用名词或名词短语

使用简单形式的常用名词或名词短语,为类取名时要表达出它的抽象含义。基类使用更通用的名称,而对派生类使用更专用的名称。

typedef ... reference; //出自标准库
typedef ... pointer;   //出自标准库
typedef ... iterator;  //出自标准库
class bank_account {...};
class savings_account : public bank_account {...};
class checking_account : public bank_account {...};

当对象和类型的名称冲突或缺少合适的名称时,对象使用简单名称,类型名称添加后缀 mode、kind、code 等。
表明是对对象集合的抽象时使用复数形式。

typedef some_container<...>yellow_pages;

当需要对象集合外的其他语义时,使用以下标准库中的数据结构作为行为模式和名称后缀:

  • vector(向量) - 一种可随机访问的顺序容器;
  • list(表) - 一种有序的顺序容器;
  • queue(队列) - 一种先入先出的顺序容器;
  • deque(双端队列) - 一种两个端口都可进行操作的队列;
  • stack(堆栈) - 一种后入先出的顺序容器;
  • set(集合) - 一种关键词访问(关联关系)容器;
  • map(图) - 一种关键词访问(关联关系)容器;

函数

过程类型的函数名称使用动词

对无返回值的函数(函数声明为 void 返回类型)或返回值使用指针或引用类型的函数,使用动词或动词短语。
对使用非 void 返回类型返回唯一一个值的函数,使用名词或名词短语。
对带有常用操作(行为模式)的类,使用项目选项列表中的操作名。例如:begin, end, insert, erase (标准库中的容器操作)。
避免使用“get”和“set”来命名表明状态的函数(给函数添加前缀“get”和“set”),特别是对取出和设置对象属性的公共操作更是这样。操作命名应当停留在类抽象和服务提供一级上;取出和设置对象属性是低层次的实现细节,如果使它们公共化,则会降低封装的完整性。
返回布尔值(谓词)的函数使用形容词(或过去分词)。谓词经常使用名词前添加前缀 ishas 的形式使名称表达一个肯定的声明。当对象、类型名或枚举型常量也使用简单名称时,这也同样非常有用。时态上要保持一致,力求准确。

示例
void insert(...);
void erase(...);

Name first_name();
bool has_first_name();
bool is_found();
bool is_available();

不要使用否定意义的名称,因为这会导致双重否定表达式出现(如 !is_not_found);这使得代码更难以理解。有些情况下,通过使用反义词,如“is_invalid”代替“is_not_valid”,可以使否定谓词变成肯定的而不需改变其语义。

示例
bool is_not_valid(...);
void find_client(name with_the_name, bool& not_found);
Should be re-defined as:
bool is_valid(...);
void find_client(name with_the_name, bool& found);

当需要使用同一通用含义时,使用函数的重载

当操作目的相同时,使用重载而不使用同义词;这尽量减少了系统中概念的数量和不同的操作,因此也就降低了整体的复杂性。
当重载操作符时,要确保操作符的语义保留了下来;如果不能保留约定俗成的操作符含义,则为函数选用另一个名称而不使用操作符重载方式。

对象与函数参数

实参名使用语法元素强调其含义

为表明其唯一性或表明本实体是活动的主要核心,在对象或参数名称前加前缀“the”或“this”。要表明次要、临时、辅助对象,则加前缀“a”或“current”:

示例
void change_name( subscriber& the_subscriber,
const subscriber::name new_name)
{
...
the_subscriber.name = new_name;
...
}
void update(subscriber_list& the_list,
const subscriber::identification with_id,
structure& on_structure,
const value for_value);
void change( object& the_object,
const object using_object);

异常


异常名选用否定的含义

只有在处理错误时才必须使用异常,故使用名词或名词短语来清楚的表达一个否定的含义:

overflow, threshold_exceeded, bad_initial_value

异常名使用项目定义过的形容词

使用项目定义列表中以下词的一个 bad、incomplete、invalid、wrong、missingillegal 作为名称的一部分,而不要机械的套用 errorexception,因为它们并不表达具体的信息。

其他事项

浮点指数和十六进制数使用大写字母。

浮点数中的字母“E”和十六进制数从“A”到“F”应当一直保持大写。


第五章

声明

本章为不同 C++ 声明类型的使用及形式提供指南。

名字空间

C++ 语言中名字空间存在之前,管理名称的作用域只有有限的几种手段;因此,全局名字空间的使用过于泛滥,导致众多的冲突,这使同一程序中难以同时使用一些库。新的名字空间语言特征解决了全局名字空间的干扰问题。

将全局声明限定在名字空间中

这意味着只有名字空间的名称可以是全局的;所有其他的声明都必须在某个名字空间的作用域内。
忽略了这一规则可能最终导致名称冲突。

使用名字空间划分非类功能

逻辑上划分非类功能类的类别,或作用域远大于类的功能(如库或子系统),使用名字空间逻辑上统一的声明(请参见“使用名字空间由系统或库划分潜在的全局名称”)。
名称表达了功能的逻辑划分。

示例
namespace transport_layer_interface { /* ...*/ };
namespace math_definitions { /* ...*/ };      

尽量不使用全局和名字空间范围的数据。

使用全局和名字空间范围内数据与封装原则是背道而驰的。

C++ 中类是基本的设计实施单元。使用它们纪录域与设计的抽象,并作为实施抽象数据类型 (ADT) 的封装机制。

使用 class 而不是 struct 来实现抽象数据类型

使用 class(类的关键字)而不是 struct 来实现一个表示抽象数据类型的类。
使用 struct(类的关键字)来定义类似 C 中的旧式数据结构 (POD),尤其是在与 C 代码程序接口时。
虽然 classstruct 是等价的,可以互换,但 class 具有更好的默认访问控制 (private),这样就提供了更好的封装性能。

基本原理

区分 classstruct 的的一致做法引入了以上的语义差别,并且还超出了语言规则的范围:class 是纪录抽象与封装最先考虑的结构;而 struct 代表一个纯数据结构,它可以在混合编程语言程序中交换使用。

以可访问权限逐次降低的顺序声明类的成员

类声明中的访问限定符应以顺序 public, protected, private 出现。

基本原理

以 public、protected、private 顺序排列的成员声明保证了类用户最感兴趣的信息置于最前列,因此减少了类用户访问不相关信息或实现细节。

抽象数据类型避免声明公共或保护数据成员

公共或保护数据成员的使用降低了类的封装性并影响了系统抵抗变化的能力:公共数据成员将类的实现暴露给了它的用户;保护数据成员将类的实现暴露给了从它继承的类。类公共数据成员或保护数据成员的任何改变都会影响到用户和继承类。

使用友元保留封装性

初次碰到时,它好像是违反直觉的:友元关系将一个类的私有部分暴露给了它的友元,还如何实现封装呢?当类之间高度相互依赖,需要相互间的内部信息时,最好是赋予它们间友元关系而不是通过类的接口将其内部细节暴露出来。
不希望如同公共成员一样暴露类的内部细节以提供给类客户访问权。将保护成员暴露给潜在的继承类,鼓励了分级设计,但这也是不希望的。友元关系有选择性的赋予了对私有成员的访问权,而不用实现子类限制。因此再所需访问之外保留了封装。
将友元关系赋予友元测试类很好的说明了友元关系保留了封装特性。友元测试类通过访问类的内部可完成相应的测试代码,之后,友元测试类可从交付的代码中删除。因此,没有丢失封装,也没有向可交付代码增加代码。

避免在类声明中定义函数

类的声明应当只包含函数的声明,永远不要进行函数定义(实现)。

基本原理

类声明中定义函数干扰了类对其实现细节的规约;使类的接口难以辨认并难以阅读;并增加了对编译的依赖。
类声明中的函数定义也减少了对内嵌函数的控制(请参照“使用 No_Inline 条件编译符禁止内嵌编译”)。

明确声明构造函数的类使用默认构造函数

为了在数组中或任何 STL 容器中使用类,类必须提供公共的默认构造函数或允许编译器生成一个构造函数。

注意:

当类有非静态引用类型的数据成员时是以上规则的一个例外,这种情况下通常不可能创建一个有意义的默认构造函数。因此,使用对象数据成员的引用就是不可靠的。

带有指针类型数据成员的类要声明其复制构造函数和赋值操作符

如果需要,并且没有明确声明时,编译器会为类暗中生成一个复制构造函数和一个赋值操作符。编译器定义了复制构造函数和赋值操作符,在 Smalltalk 术语中这通常称为“shallow-copy”(浅拷贝):明确的说,即按成员拷贝以及对指针按位拷贝。使用编译器生成的复制构造函数和默认的赋值操作符肯定会造成内存泄漏。

示例
//摘自 [Meyers, 92].
void f()
{
String hello("Hello");//假设 String 类型
                      //由指向 char 型
                      //数组的指针来实现。
{ //进入新的域(程序块)
String world("World");
world = hello;        //赋值语句使 world 丢失了
                      //它最初指向的内存单元
}	//从域中退出时
	//解构 world;
	//此时也间接的解构了 hello
String hello2 = hello; //将已解构过的 hello 赋给
                       // hello2
}      

以上代码中,储存字符串“World”的内存单元在赋值语句之后丢失了。结束内部程序块时,销毁了world;因此,由 hello 引用的内存单元也丢失了。已经解构的 hello 赋给了 hello2

示例
//摘自 [Meyers, 1992]。
void foo(String bar) {};
void f()
{
String lost = "String that will be lost!";
foo(lost);
}      

以上代码中,当调用函数 foo 使用实参 lost 时,利用编译器定义的复制构造函数将 lost 复制到 foo 中。因为复制 lost 时对指向“String that will be lost!”的指针进行逐位复制,当从 foo 中退出时,对 lost 的复制(即形参 bar,译者注)将会被销毁(假设析构函数正确的释放了内存单元),同时存储字符串“String that will be lost! ”的内存单元也会被释放。

不要重复声明构造函数参数有默认值

示例
//示例摘自 [X3J16, 95; section 12.8]
class X {
public:
X(const X&, int);	// int 参数没有
				   //初始化
				   //没有用户声明的复制构造函数,因此
				   //编译器暗中声明了一个。
};
//int 参数滞后的初始化
//将构造函数变成了复制构造函数。
//
X::X(const X& x, int i = 0) { ...}      
基本原理

编译器在类声明中没有发现“标准”的复制构造函数时会暗中声明一个复制构造函数。但是,默认参数滞后的初始化可能将构造函数改变成复制构造函数:使用复制构造函数时导致混淆。因为这种混淆,任何复制构造函数的使用都因此而变为病态的。[X3J16, 95; section 12.8].

将析构函数总是声明为 virtual 类型

除非明确将类的设计为不可继承的,否则应将析构函数声明为 virtual 类型。

基本原理

如果基类的析构函数没有声明为 virtual 类型,则通过基类的指针或引用删除派生类对象将导致不确定的行为。

示例
//为了简便起见,这里使用了不好的命名风格
class B {
public:
B(size_t size) { tp = new T[size]; }
~B() { delete [] tp; tp = 0; }
//...
private:
T* tp;
};

class D : public B {
public:
D(size_t size) : B(size) {}
~D() {}
//... 
};

void f()
{
B* bp = new D(10);
delete bp; //由于基类
		    //的析构函数没有定义成 virtual 类型,
	       //这里的行为是不确定的
}      

避免声明太多的转换操作符和单参数构造函数

使用限定符 explicit 可防止单参数构造函数用于隐式转换。

不要重定义非虚函数

非虚函数实现固定的行为,并且不希望专用于派生类。违反这一原则将导致不可预料的行为:同一对象不同时间可能表现不同的行为。
非虚函数是静态绑定的;以下示例中,对象函数的调用由变量(指向 A 或 B 的指针)的静态类型控制,而不是对象的实际类型。

示例
// Adapted from [Meyers, 92].
class A {
public:
oid f(); //非虚函数:静态绑定
};
class B : public A {
public:
void f(); //非虚函数:静态绑定
};
void g()
{
B x;
A* pA = &x; //静态类型,指向 A 的指针
B* pB = &x; //静态类型,指向 B 的指针
pA->f(); //调用 A::f
pB->f(); //调用 B::f
}      

谨慎使用非虚函数

因为非虚函数通过限制特殊化(即重载,译者注)和多态的使用来限制子类,声明为非虚拟之前,必须注意确保操作对所有子类确实是固定不变的。

使用初始化构造函数而不是使用构造函数中的赋值语句

对象构造时其状态的初始化应由初始化构造函数(一个成员初始化列表)完成,而不是通过构造函数内的赋值操作符完成。

示例
代码如下:
class X 
{
public:
X();
private
Y the_y;
};
X::X() : the_y(some_y_expression) { } 
//
// “the_y”由初始化构造函数进行初始化
而不是如下进行初始化
X::X() { the_y = some_y_expression; }
//
// “the_y”由赋值操作符进行初始化
基本原理

对象的构造涉及到在执行构造函数体之前构造所有的基类以及数据成员。使用初始化构造函数只需要一个操作(由初值构造),而在构造函数体中初始化数据成员需要两个操作(构造以及赋值)。
对有多重嵌套的类(类包含类,所包含的类又包含其他的类),多个操作(构造+成员赋值)造成的性能的开支就十分重要了。

初始化构造函数不要调用成员函数

示例
class A 
{
public:
A(int an_int);
};
class B : public A
{
public:
int f();
B();
};

B::B() : A(f()) {} 
//不确定:调用成员函数时A
//尚未初始化[X3J16, 95].
基本原理

当所有基类的成员初始化完成之前如果初始化构造函数直接或间接的调用了成员函数,此操作的结果是不确定的。[X3J16, 95].

注意构造函数和析构函数调用时的情况

构造函数中调用成员函数时要格外注意;即使调用的是虚函数时也要注意。执行的将是在类或其基类的构造函数、析构函数中定义的操作。

整形类常量使用 static const

定义整形(整数)类常量时,使用 static const 数据成员,不要使用 #define 或全局常量。如果编译器不支持 static const,则使用 enum

示例
代码如下:
class X {
static const buffer_size = 100;
char buffer[buffer_size];
};
static const buffer_size;
或:
class C {
enum { buffer_size = 100 };
char buffer[buffer_size];
};
但不要使用:
#define BUFFER_SIZE 100
class C {
char buffer[BUFFER_SIZE];
};      

函数

一定要明确声明函数的返回值类型

这可以防止编译器发现函数未声明返回值类型时所造成的模糊不清。

函数声明中要提供正规参数名称

函数的声明和定义中要使用相同的名称;这可使混淆程度降至最小。提供参数名称有利于提高代码的易注释性和易读性。

函数要尽量只有一个返回点

返回语句在程序体中自由放置与 goto 语句的情况类似,会造成代码难以阅读和维护。
只有在少数几种函数中才能容忍出现多个返回语句,此时可以同时看到所有的返回语句 return 并且代码还有非常规则的结构:

type_t foo()
{

if (this_condition)
return this_value;
else
return some_other_value;
}      

函数的返回类型为 void 时无返回语句。

避免创建对全局有副作用的函数

应尽量避免创建对全局有副作用的函数,因为它可能改变并不知情的数据而非它们内部对象的状态,如全局和名字空间数据(另请参见“尽量不使用全局或名字空间范围的数据”)但如果这是不可避免的,则应明确记录下所有的副作用以作为函数规约的一部分。
只在所需的对象中传递参数增加了代码的上下文无关性,并提高了它的强壮性和易理解性。

以重要性和活跃性递减的顺序声明函数的参数

从调用者的角度讲,参数声明的顺序是非常重要的:

  • 首先以重要性递减的顺序定义非默认参数;
  • 再以被修改可能性递减的顺序定义有默认值的参数。

这种顺序使用了参数默认值的优点来减少函数调用中实参的个数。

避免声明带有不定参数个数的函数

带有不定参数个数的函数无法对其实参进行类型检查。

避免重复声明带有默认参数的函数

函数的进一步重复声明中应避免添加默认值:远离先前声明,一个函数只应声明一次。否则,如果读者没有意识到此后的声明,这将会造成混淆。

函数声明中尽可能使用 const

检查函数是否有常量行为(返回常数值;接受常量实参;或其操作不带有副作用),如果有则使用限定符 const 表明其行为。

示例
const T f(...); //函数返回一个常量
		         //对象。
T f(T* const arg);	        //函数接受一个常量指针
			          //为其参数。
//可以改变指针所指的对象,
//但不可以改变指针本身。
T f(const T* arg);      //函数参数为指针类型
T f(const T& arg);      // 函数参数为引用类型
                        //它们都指向一个常量对象。指针可以
                        //改变,但所指的对象
                        //不能改变。
T f(const T* const arg);  //函数参数为
                          //常量指针,它指向常量对象。
                          //指针和它所指的对象
                          //都不改变。
T f(...) const;  //函数没有副作用:
                 //不改变它对象的状态
                 //所以可应用于
                 //常量对象。

避免利用值传递对象

利用值传递和返回对象可能带来构造函数和析构函数的大量开支。可通过引用传递和返回对象,这样可避免构造函数和析构函数的开支。
可用 Const 引用指定通过引用传递的实参是不可改变的。复制构造函数和赋值操作符是典型的示例:

C::C(const C& aC);
C& C::operator=(const C& aC);
示例:

考虑以下例子:

the_class the_class::return_by_value(the_class a_copy)
{
return a_copy;
}
the_class an_object;
return_by_value(an_object);

调用函数 return_by_value,其实参为 an_object,此时调用 the_class 的复制构造函数将 an_object 复制到 a_copy。再次调用 the_class 的复制构造函数将 a_copy 复制到函数返回的临时对象中。从函数返回时调用 the_class 的析构函数销毁 a_copy。一段时间之后再次调用 the_class 的析构函数销毁 return_by_value 返回的对象。以上不完成任何事的函数的所有开支是两个构造函数和两个析构函数。
如果 the_class 是个派生类且包含其他类的成员数据,情况会更糟:会调用基类以及所包含类的构造函数和析构函数,因此函数调用所带来的构造函数和析构函数的调用就会随之急剧增长。

注意:

以上指南似乎旨在建议开发人员在传递和返回对象时总是使用引用类型,但应格外当心不要返回引用到局部对象上,当需要对象时,也不要返回引用。返回引用到局部对象会造成灾难性后果,因为函数返回时,所返回的引用绑定到了所销毁的对象上。

不能返回引用到局部对象

离开函数作用域时会销毁局部对象;使用销毁了的对象会造成灾难。

不可返回由 new 初始化,之后又已解除引用的指针

违背这一原则将导致内存泄漏

示例
class C {
public:
...
friend C& operator+( const C& left,
const C& right);
};
C& operator+(const C& left, const C& right)
{
C* new_c = new C(left..., right...);
return *new_c;
}
C a, b, c, d;
C sum;
sum = a + b + c + d;

因为在计算加和 sum 值时没有存储操作符“+”的中间结果,因此中间对象不能删除,否则会导致内存泄漏。

不要返回非常量引用或指针到成员数据上

违背了这一原则也就违背了数据的封装性,可能导致不好的结果。

宏扩展使用内嵌定义函数不使用 #define

但要有选择的使用内嵌定义:只对非常小的函数才使用;内嵌定义大型函数可能导致代码膨胀。
由于用户代码编译时需要内嵌定义函数的实现,因此内嵌定义函数也增加了模块间编译的依赖。
[Meyers, 1992] 提供了以下不良宏使用极端示例的详细讨论:

示例

不要这样做:

#define MAX(a, b) ((a) > (b) ?(a) : (b))

而应这样做:

inline int max(int a, int b) { return a > b ? a : b; }

MAX 有好几个问题:它不是安全的类型;它的行为不是确定的:

int a = 1, b = 0;
MAX(a++, b);     // a 增 1 了两次
MAX(a++, b+10);  // a 增 1 了一次
MAX(a, "Hello"); //比较整形和指针

使用默认参数而不使用函数重载

当只使用一个算法并且此算法可由少量几个参数进行参数化时,使用默认参数而不使用函数重载。
使用默认参数有利于减少重载函数的数量、提高可维护性、减少函数调用时所需的实参数量、以及提高代码的可读性。

使用函数重载表达常用语义

当同一语义操作需要多个实现时使用函数重载,但重载使用不同的实参类型。
重载操作符时保留其传统含义。不要忘记定义关系操作符,如操作符 operator==operator!=

避免重载以指针和整形数为实参的函数

避免使用带单一整形实参的函数来重载带单一指针实参的函数:

void f(char* p);
void f(int i);

以下调用可能会导致混淆:

PRE>f(NULL); f(0);

此重载解析为 f(int) 而不是 f(char*)

令操作符 operator= 返回对 *this 的引用

C++ 允许赋值链:

String x, y, z;
x = y = z = "A string";

因为复制操作符是右结合的,将字符串“A string”赋给 z,再将 z 赋给 y,最后将 y 赋给 x。以从右到左的顺序,对每个在 = 右端的表达式有效的调用一次 operator= 。这也意味着每一次 operator= 运算的结果都是一个对象,但其左端或右端的对象都有可能成为返回值。
使用复制操作符好的做法总有以下形式:

C& C::operator=(const C&);

只有左端的对象是可能的(rhs 为常量引用,lhs 为变量引用),因此应返回 *this。详情请参见[Meyers, 1992]

使 operator= 检查自赋值

执行检查有两点重要的理由:首先,派生类对象的赋值涉及到调用每一个基类(在继承层次结构中位于此类的上方)的赋值操作符,跳过这些操作符就可以节省很多运行时间。其次,在复制“rvalue”对象前,赋值涉及到解构“lvalue”对象。在自赋值时,rvalue 对象在赋值前就已销毁了,因此赋值的结果是不确定的。

尽可能减少复杂性

不要书写过长的函数,例如其代码超过 60 行。
尽可能减少返回语句的数量,1 是理想值。
尽量使循环复杂性降到 10 以下(对单一退出语句函数为判定语句总数加 1)。
尽量使扩展循环复杂性降到 15 以下(对单一退出语句函数为判定语句+逻辑操作符+1的和)。
尽量减小引用的平均最大作用范围(局部对象声明与对象第一次使用间的行距)。

类型

定义项目范围的全局系统类型

大型项目中通常有整个系统中经常使用的类型的集合;这种情况下,将这些类型收集入一个或多个低级全局实用程序的名字空间中,这样做是明智的(请参见“避免使用基本类型”的示例)。

避免使用基本类型

当对可移植性要求较高、需要控制数字对象占据的内存空间或需要一个具体范围的值时,不使用基本类型。这种情况下最好通过使用适当的基本类型明确声明带有大小限制的类型的名称。
确保基本类型不会通过循环计数器、数组下标等潜回代码中。

示例
namespace system_types {
typedef unsigned char byte;
typedef short int integer16; //16 位有符号整数
typedef int integer32; //32 位有符号整数
typedef unsigned short int natural16; //16 位无符号整数
typedef unsigned int natural32; //32 位无符号整数
...
}      
基本原理

基本类型的表示依赖于具体实施。

使用 typedef 创建同义词来加强局部含义

对现有名称使用 typedef 创建同义词,提供更有意义的局部名称并提高易读性(这样做不会增加运行时间)。
typedef 也可用来提供受限名称的速记法。

示例
//标准库的向量声明
//
namespace std {
template <class T, class Alloc = allocator>
class vector {
public:
typedef typename 
Alloc::types<T>reference reference;
typedef typename 
Alloc::types<T>const_reference const_reference;
typedef typename 
Alloc::types<T>pointer iterator;
typedef typename 
Alloc::types<T>const_pointer const_iterator;
...
}
}      

使用 typedef 创建的名称时,同一代码段中不要混合使用原名称和它的同义词。

常量与对象

避免使用常量值

使用引用形式的已命名常量

定义常量时避免使用 #define 预处理指示符

代之以 constenum
不要这样做:

#define LIGHT_SPEED 3E8
而应这样做:
const int light_speed = 3E8;
或调整数组大小如下:
enum { small_buffer_size = 100, 
large_buffer_size = 1000 };
基本原理

因为由 #defines 引入的名称在编译预处理时会被替换掉,不出现在符号表中,因此调试就更加困难了。

对象声明靠近其第一次使用点
声明时总要初始化 const 对象

未声明为 externconst 对象有内部链接,声明时初始化这些常量对象允许在编译时使用初始化函数。

永远不要忘记常量对象的“不变性”

常量对象可能存在于只读存储器中。

定义时初始化对象

如果对象不是自初始化的,在对象定义中指定初始值。如果不可能指定一个有意义的初始值,则赋值“nil”或考虑后面再作定义。
对大型对象,通常不建议先构造对象,此后再使用赋值语句初始化的作法。因为这样做的代价高昂(请参见“使用初始化构造函数而不使用构造函数中的赋值语句”)。
如果构造时不可能合适的初始化对象,则使用传统的“nil”值进行对象初始化,它意味着“未初始化”。只在初始化过程中声明“不可用但值已知”的对象时才使用 nil 值。但算法在受控模式下可以拒绝接受:在对象合适的初始化之前使用它时可以指出这是未经初始化的变量错误。
注意有时不可能所有类型都声明为 nil 值,尤其是在模运算类型中,例如角度。I这种情况下选择最不可能出现的值。


第六章

表达式和语句

本章对不同种类的 C++ 表达式和语句提供指南。

表达式

使用冗余的圆括号使复合表达式含义更加清晰
避免表达式的过深嵌套

表达式的嵌套层数定义为:忽略操作符优先级原则时,从左到右求表达式的值所需圆括号对的嵌套数。
嵌套层数过多会使表达式难以理解。

不要假定任何特殊的表达式计算次序

除非计算次序由操作符指定(如逗号操作符、三元表达式、以及连接和分离),否则不要假定任何特殊的计算次序,因为这种假定可能导致严重的混淆并降低可移植性。
例如,同一语句中不要混合使用变量的增 1 和减 1 操作。

示例
foo(i, i++);
array[i] = i--;

空指针使用 0 而不使用 NULL

空指针应使用 0 还是 NULL 是一个有高度争议的论题。
C 和 C++ 中定义任何零值常量表达式都可解释为空指针。因为零难以阅读,并且不赞成使用常量,传统上程序员使用宏 NULL 作为空指针。不幸的是,NULL 没有可移植的定义。一些 ANSI C 编译器使用 (void *)0,但对 C++ 来说这不是一个好的选择:

char* cp = (void*)0; /* C 合法但 C++ 不合法*/

因此任何形如 (T*)0 而非一个 0 的 NULL 定义,在C++ 中都需要类型转换。历史上,很多指南都拥护使用 0 代替空指针以减少类型转换、增强代码的可移植性。但许多 C++ 开发人员还是感觉使用 NULL 比 0 更加舒服,也争辩说当今多数的编译器(准确的说是多数头文件)将 NULL 实施为 0。
本指南倾向使用 0,因为不管 NULL 的值是多少,0 都是有效的。但由于争议,这一点降级为一个小技巧,可在适当的时候遵循或忽略它。

不要使用旧类型转换

使用新类型转换符(dynamic_cast、static_cast、reinterpret_cast、const_cast)而不要使用旧类型转换符。
如果您没有新类型转换符,避免同时转换,特别是向下转换(将基类对象转换成派生类对象)。
使用转换操作符如下:

  • dynamic_cast - 类同一层次(子类型)成员间的的转换,使用运行类型信息(对带虚函数的类,运行类型信息是可用的)。这种类间的转换一定是安全的。
  • static_cast - 类同一层次成员间的的转换,不使用运行类型信息;所以这可能是不安全的。如果程序员不能保证类型的安全性,则使用 dynamic_cast。
  • reinterpret_cast - 不相关指针类型和整形(整数)间的转换;这是不安全的,只应在所提及的类型间使用。
  • const_cast - 将指定为 const 型函数实参的“不变性”转换掉。注意:const_cast 不打算将确实定义为常量对象(它可能位于只读存储器中)的“不变性”转换掉。

不要使用 typeid 实现类型转换逻辑:使类型转换操作符完成类型检查和转换的操作不可再分,详情请参见 [Stroustrup, 1994]

示例

不要这样做:

void foo (const base& b)
{
if (typeid(b) == typeid(derived1)) {
do_derived1_stuff();
else if (typeid(b) == typeid(derived2)) {
do_derived2_stuff();
else if () {

}
}      
基本原理

旧类型转换使类型系统失效,可能导致编译器难以察觉的缺陷:内存管理系统可能会崩溃,虚函数表可能遭严重错误修改,当对象作为派生类对象访问时无关对象可能会遭破坏。注意即使读访问也可能造成破坏,因为有可能引用了并不存在的指针或区域。
新类型转换操作符使类型转换更加安全(多数情况下)、更加明确。

Boolean(布尔)表达式使用新的 bool 类型

不要使用旧的 Boolean 宏或常数:没有标准的布尔值 true(真);代之以新的 bool 类型。

布尔值 true(真)之间不要直接比较

因为传统上 true 值 (1 或 !0) 没有标准值,非零表达式和真值的比较就可能无效。
代之以使用布尔表达式。

示例

不要这样做:

if (someNonZeroExpression == true) 
//可能不会解释为真值条件
最好这样做:
if (someNonZeroExpression) 
//一定会解释为一个真值条件

指针不要与不在同一数组中的对象比较

这种操作的结果几乎都是毫无意义的。

已删除的对象指针要赋予空指针值

设置已删除对象的指针为空指针可避免灾难的发生:重复删除非空指针是有害的,但重复删除空指针是无害的。
即使在函数返回前,删除操作后也总要赋一个空指针,因为以后可能添加新的代码。

语句

当从布尔表达式分支时使用 if 语句
当从离散值分支时使用 switch 语句

当分支条件为离散值时使用 switch 语句而不使用一系列的“else if”。

一定为 switch 语句提供一个 default 分支以记录错误

switch 语句应总包含一个 default 分支,default 分支应用以捕获错误。
这一原则保证了当引入新的 switch 值而处理这一新值的分支被删除时,现有的 default 分支可以捕获到错误。

当循环需要迭代前测试时使用 for 语句或 while 语句

当迭代和循环的结束是根据循环计数器的值时使用 for 语句不使用 while 语句。

当循环需要迭代后测试时使用 do while 语句
循环中避免使用 jump 语句

避免使用除循环结束条件以外的循环退出方式(使用 break、returngoto),不要使用 continue 不成熟的跳转到下一迭代上。这减少了控制路径流程的数量,使代码更易理解。

不要使用 goto 语句

这是一条通用原则。

避免嵌套作用域内隐藏标识符

这会使读者模糊不清,维护时造成潜在的危险。


第七章

特殊主题

本章为内存管理与错误报告提供指南

内存管理

C 和 C++ 内存操作要避免混合使用

C 库函数 malloccallocrealloc 不应用于分配对象空间:此时应使用 C++ 操作符 new。
只有在内存传递给C 库函数处理时,才使用 C 函数分配内存。
不要使用 delete 来释放由 C 函数分配的内存,或释放由 new 创建的对象。

删除由 new 创建的数组对象时总使用 delete[]

使用 delete 删除数组对象时如果不使用空方括号(“[]”),则只删除数组的第一个元素,因此导致内存泄漏。

错误处理和异常

由于使用 C++ 异常机制的经验不足,此处的指南将来可能会作较大的改变。
C++ 草拟标准定义了两大类错误:逻辑错误和运行错误。逻辑错误是可避免的编程错误。运行错误定义为程序范围外事件导致的错误。
使用异常的通用规则为:正常条件下的系统如果没有重载或硬件错误则不应当产生任何异常。

开发过程中多使用声明语句以发现错误

开发过程中使用函数的前置和后置条件声明语句以发现“倒毙”错误。
在实现最终错误处理代码前,声明语句提供了简单有用的临时错误监测机制。声明语句还有额外的好处,使用“NDEBUG”预处理符,可在编译时去掉这些声明(请参见“用具体值定义 NDEBUG 预处理符”)。
传统上使用宏 assert 达到这一目的;但文献 [Stroustrup, 1994] 提供了一个可替代的模板如下。

示例
template<class T, class Exception>
inline void assert ( T a_boolean_expression, 
Exception the_exception)
{
if (! NDEBUG)
if (! a_boolean_expression) 
throw the_exception;
}      

只在真实的异常情况时使用异常

对经常的、预料中的事件不要使用异常:异常打断了代码的正常控制流程,使它难以理解和维护。
预料中的事件应用代码的正常控制流程处理;需要时使用函数返回值或“out”参数状态码。
异常也不应用于实现控制结构:这会是另一种形式的“goto”语句。

从标准异常中派生项目异常

这保证了所有异常都支持常用操作的最小集合并可由一小组高级处理程序处理。
使用逻辑错误(域错误、非法实参错误、长度错误和越界错误)表示应用程序域错误、函数调用传递了非法实参、对象构造超出了它们允许的大小以及实参值不在允许范围内。
使用运行错误(范围错误和溢出错误)表示只有在运行时才能查出的算术和配置错误、数据已遭破坏或资源耗尽错误。

给定抽象尽量少使用异常

大型系统中,每一层都不得不处理大量的异常会使代码难以阅读和维护。异常处理可能会严重影响了正常处理。
减少使用异常的方法有:
通过使用少量的异常种类在抽象间共享异常。
引发派生于标准异常的特殊异常,处理更通用的异常。
为对象添加“异常”状态,提供明确检查对象合法性的原语。

将所有异常声明为已引发

产生异常(而不仅仅是传递异常)的函数应在它们的异常规约中将所有异常声明为已引发:它们不应平静的产生异常而不警告它们的用户。

在异常首次出现时就报告它

开发过程中,采用合适的记录机制尽早报告异常,包括在“异常引发点”。

按照从派生结构最底端到最顶端的顺序定义异常处理

定义异常处理要按照从派生结构最底端到最顶端(即从最终子类到最初父类)的顺序,这样可以避免编写无法到达的处理过程;请参见下面的示例 how-not-to-it。因为是以声明的顺序匹配处理过程的,所以这也保证了异常由最合适的处理过程捕获。

示例

不要这样做:

class base { ...};
class derived : public base { ...};
...
try {
...
throw derived(...);
//
//引发一个派生类异常
}
catch (base& a_base_failure)
//
//但基类异常处理过程“捕获”了它,因为
//基类处理过程首先匹配!
{
...
}
catch (derived& a_derived_failure) 
//
//这一处理过程是无法达到的!
{ 
...
}      

避免使用捕获所有异常的处理过程

避免使用捕获所有异常的处理过程(处理过程声明使用 “…”),除非重引发了异常只有在做本地内务管理时才使用捕获所有异常的处理过程,这时应重引发异常以避免掩盖了本层不能处理此异常的事实。

try {
...
}
catch (...)
{ 
if (io.is_open(local_file))
{
io.close(local_file);
}
throw;
}      

确保函数状态代码有合适的值

当状态代码作为函数参数返回时,要赋值给参数作为程序体中的第一个可执行语句。系统的设置所有状态的默认值为成功或失败。考虑函数所有可能的出口,包括异常处理。

本地执行安全检查;不要希望您的客户会这样做

如果没有合适的输入,函数就会产生错误输出,在函数中装载代码以受控方式监测和报告非法输入。不要依赖于标注来告知客户传递合适的值。事实上标注迟早会被忽略掉,如果检测到非法参数就会导致难以调试的错误。


第八章

可移植性

本章论述先验不可移植的语言特征。

路径名

不要使用绝对代码文件路径名

各操作系统中的路径名不是以标准形式表示的。使用它们将会引入对平台的依赖。

示例
#include "somePath/filename.hh" // Unix
#include "somePath\filename.hh" // MSDOS

数据表示

类型的表示和排列高度依赖于机器的结构。对表示和排列的假设会导致混淆,降低可移植性。

不要假设类型的表示

特别的,不可以在 int、长整形或任何其他数字类型中存储指针类型,因为这是高度不可移植的。

不要假设类型的排列

不要依赖于一个特殊的下溢或上溢行为
尽可能使用“可伸缩”常量

可伸缩常量避免了字长变化的问题。

示例
const int all_ones = ~0;
const int last_3_bits = ~0x7;

类型转换

不要从一个“短”类型转换成一个“长类型”

机器结构可能指定了某种类型的排列方式。从需要较松散排列的类型转换为需要较严密排列的类型可能会导致程序错误。


第九章

复用

本章为 C++ 代码复用提供指南。

尽可能使用标准库构件

如果没有标准库,则基于标准库接口创建类,这有利于将来的移植。

使用模板复用独立于数据的行为

当行为不依赖于一个具体的数据类型时,使用模板复用行为。

使用公共继承复用类接口(子类型化)

使用 public 继承表达“is a”关系并复用基类接口,还可有选择的复用它们的实现。

使用包容而不是私有继承复用类的实现

当复用实现或建模“部分/整体”关系时,避免使用私有继承。复用未重新定义的实现最好由包容而非私有继承来实现。
当需要重新定义基类的操作时使用私有继承。

谨慎使用多继承

多继承应谨慎使用,因为它带来了许多额外的复杂性。[Meyers, 1992] 提供了对潜在名称歧义和重复继承所带来复杂性的详细讨论。复杂性来自于:
歧义,当多个类使用相同的名称时,任何对名称不加限定的引用天生就是产生歧义的。可通过使用类名称限定其成员名称来解决歧义的问题。但这也带来了不幸的后果:使多态无效且将虚函数准变成了静态绑定函数。
从同一基类重复继承(派生类通过继承结构的不同路径多次继承一个基类)多组数据成员产生了如下问题:应当使用多组数据成员中的哪一个?
可使用虚继承(对虚基类的继承)防止多继承数据成员。那么为什么不总使用虚继承呢?虚继承有负面影响,会改变基础对象表示并降低访问的效率。
要求所有的继承都是虚拟的,同时也就强加了包罗一切的空间,带来了时间上的低效,实现这样的政策过于独断了。
因此,为了决定是使用虚继承还是非虚继承,多继承需要类的设计者对将来类的使用有敏锐的洞察力。


第十章

编译问题

本章为编译问题指南

尽量减少对编译的依赖

模块规约中不要包含只是此模块实施所需的其他头文件。
避免在只需要指针或引用可见时就为了能看到其他类而在规约中包含头文件;代之以预先声明。

示例
//模块 A 规约,包含于文件“A.hh”中
#include "B.hh" //当只有实施需要时
                //不要包含。
#include "C.hh" //只有引用需要时不要包含;
                //代之以预先声明。
class C;
class A
{
C* a_c_by_reference; //有 a 的引用。
};
// “A.hh”结束
注意:

尽量减少对编译的依赖是某些设计代码模式或模式的基本原理,如不同的命名:Handle(句柄)或 Envelope(信包)[Meyers, 1992],或 Bridge(桥)[Gamma] 类。将类抽象的责任在两个关联类间分割,一个提供类接口,另一个提供类实现;因为任何实现(实现类)上的改变都不再需要客户重新编译,因此类与其客户间的依赖关系就最小化了。

示例
//模块 A 规约,包含于文件“A.hh”中
class A_implementation;
class A
{
A_implementation* the_implementation;
};
//“A.hh”结束

这一做法也允许接口类和实现类特殊化为两个单独的类层次结构。

用具体值定义 NDEBUG

通常使用符号 NDEBUG 在编译过程中去掉由宏 assert 实现的声明代码。传统做法是当需要消除声明语句时定义这一符号;但是,开发人员经常没有意识到声明的存在,因此没有定义符号。
我们支持使用声明的模板版本;这种情况下符号 NDEBUG 必须明确的赋值:需要声明代码时为 0;要消除时为非 0。任何没有提供符号 NDEBUG 以具体值的声明代码最终编译时将产生编译错误;因此,提醒了开发人员注意声明代码的存在。


指南总结

以下是本手册所有指南的总结。

要求或限制

使用常识
通常使用 #include 来访问模块的规约
永远不要声明以一个或多个下划线 ('_') 开头的名称
将全局声明限定在名字空间中
明确声明构造函数的类使用默认构造函数
带有指针类型数据成员的类要声明其复制构造函数和赋值操作符
不要重复声明构造函数参数有默认值
将析构函数总是声明为 virtual 类型
不要重定义非虚函数
初始化构造函数不要调用成员函数
不能返回引用到局部对象
不可返回由 new 初始化,之后又已解除引用的指针
不要返回非常量引用或指针到成员数据上
令操作符 operator= 返回对 *this 的引用
使 operator= 检查自赋值
永远不要忘记常量对象的“不变性”
不要假定任何特殊的表达式计算次序
不要使用旧类型转换
Boolean(布尔)表达式使用新的 bool 类型
布尔值 true(真)之间不要直接比较
指针不要与不在同一数组中的对象比较
已删除的对象指针要赋予空指针值
一定为 switch 语句提供一个 default 分支以记录错误
不要使用 goto 语句
C 和 C++ 内存操作要避免混合使用
删除由 new 创建的数组对象时总使用 delete[]
不要使用绝对代码文件路径名
不要假设类型的表示
不要假设类型的排列
不要依赖于一个特殊的下溢或上溢行为
不要从一个“短”类型转换成一个“长类型”
用具体值定义 NDEBUG

建议

不同文件中放置模块规约与实现
只选择一组文件扩展名以区分头文件和实施文件
每个模块规约避免定义多个类
避免将私有实施声明置于模块的规约中
将模块内嵌函数的定义置于单独的文件中
如果程序规模是个要求考虑的问题,就把大的模块分割成多个变换单元
分离平台依赖性
防止重复文件包含的保护
使用"No_Inline"条件编译符破坏内嵌编译
对嵌套语句使用小的、一致的缩进风格
相对于函数名或作用域名缩进函数参数
使用能够适合标准打印纸大小的最大行宽
使用一致换行
使用 C++ 风格的注释而非 C 风格的注释
注释与源代码尽可能靠拢
避免行末注释
避免注释头
使用空注释行分离注释段
避免冗余
编写自记录代码而非注释
记录类与函数
选择一个名称约定,使用它要前后一致
避免使用只靠字母大小写才能区分的名称
避免使用缩写
避免使用后缀来表明程序语言结构
选择清晰的、易辨认的、有意义的名称
使用名称的正确拼写
布尔值使用正值谓词从句
使用名字空间由子系统或库划分潜在全局名称
类的名称使用名词或名词短语
过程类型的函数名称使用动词
当需要使用同一通用含义时,使用函数的重载
实参名使用语法元素强调其含义
异常名选用否定的含义
异常名使用项目定义过的形容词
浮点指数和十六进制数使用大写字母
使用名字空间划分非类功能
尽量不使用全局和名字空间范围的数据
使用 class 而不是 struct 来实现抽象数据类型
以可访问权限逐次降低的顺序声明类的成员
抽象数据类型避免声明公共或保护数据成员
使用友元保留封装性
避免在类声明中定义函数
避免声明太多的转换操作符和单参数构造函数
谨慎使用非虚函数
使用初始化构造函数而不是使用构造函数中的赋值语句
注意构造函数和析构函数调用时的情况
整形类常量使用 static const
一定要明确声明函数的返回值类型
函数声明中要提供正规参数名称
函数要尽量只有一个返回点
避免创建对全局有副作用的函数
以重要性和活跃性递减的顺序声明函数的参数
避免声明带有不定参数个数的函数
避免重复声明带有默认参数的函数
函数声明中尽可能使用 const
避免利用值传递对象
宏扩展使用内嵌定义函数不使用 #define
使用默认参数而不使用函数重载
使用函数重载表达常用语义
避免重载以指针和整形数为实参的函数
尽可能减少复杂性
避免使用基本类型
避免使用常量值
定义常量时避免使用 #define 预处理指示符
对象声明靠近其第一次使用点
声明时总要初始化 const 对象
定义时初始化对象
当从布尔表达式分支时使用 if 语句
当从离散值分支时使用 switch 语句
当循环需要迭代前测试时使用 for 语句或 while 语句
当循环需要迭代后测试时使用 do while 语句
循环中避免使用 jump 语句
避免嵌套作用域内隐藏标识符
开发过程中多使用声明语句以发现错误
只在真实的异常情况时使用异常
从标准异常中派生项目异常
给定抽象尽量少使用异常
将所有异常声明为已引发
按照从派生结构最底端到最顶端的顺序定义异常处理
避免使用捕获所有异常的处理过程
确保函数状态代码有合适的值
本地执行安全检查;不要希望您的客户会这样做
尽可能使用“可伸缩”常量
尽可能使用标准库构件

提示

定义项目范围的全局系统类型
使用 typedef 创建同义词来加强局部含义
使用冗余的圆括号使复合表达式含义更加清晰
避免表达式的过深嵌套
空指针使用 0 而不使用 NULL
在异常首次出现时就报告它


参考文献

[Cargill, 92] Cargill, Tom.1992. C++ Programming Styles Addison-Wesley.

[Coplien, 92] Coplien, James O. 1992. Advanced C++ Programming Styles and Idioms, Addison-Wesley.

[Ellemtel, 93] Ellemtel Telecommunications Systems Laboratories.June 1993. Programming in C++ Rules and Recommendations.

[Ellis, 90] Ellis, Margaret A. and Stroustrup, Bjarne.1990.The Annotated C++ Reference Manual, Addison-Wesley.

[Kruchten, 94] Kruchten, P. May 1994. Ada Programming Guidelines for the Canadian Automated Air Traffic System.

[Lippman, 96] Lippman, Stanley, B. 1996. Inside the C++ Object Model, Addison-Wesley.

[Meyers, 92] Meyers, Scott.1992. Effective C++, Addison-Wesley.

[Meyers, 96] Meyers, Scott.1996. More Effective C++, Addison-Wesley.

[Plauger, 95] Plauger, P.J. 1995. The Draft Standard C++ Library, Prentice Hall, Inc.

[Plum, 91] Plum, Thomas and Saks, Dan.1991. C++ Programming Guidelines, Plum Hall Inc.

[Rational, 92] Rational Software Corporation, December 1992. Rose C++ Programming Style Guidelines.

[Stroustrup, 94] Stroustrup, Bjarne.1994. The Design and Evolution of C++, Addison-Wesley.

[X3J16, 95] X3J16/95-0087 | WG21/N0687.April 1995. Working Paper for Draft Proposed International Standard for Information Systems-Programming Language C++.


© 1987 - 2001 Rational Software Corporation。版权所有。

分栏显示 Rational Unified Process

Rational Unified Process  

posted on 2006-04-24 10:09 杨粼波 阅读(794) 评论(0)  编辑 收藏 引用 所属分类: Windows编程


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理