C++编码规范与指导
版本:1.4
作者:白杨
推荐浏览设置:
-
屏幕分辨率:≥ 1024x768
-
字体:中(Ctrl+鼠标滚轮设置)
-
最大化本窗口
文档控制
版本号 |
修改时间 |
修改内容 |
修改人 |
审稿人 |
1.0 |
2004-07-22 |
创建 |
白杨 |
田振军 |
1.1 |
2004-08-05 |
根据审稿意建修改 |
白杨 |
田振军、马浩军、叶晓峰 |
1.2 |
2004-08-09 |
根据审稿意建修改;新增RTTI、虚函数和虚基类的开销分析及使用指导 |
白杨 |
田振军、马浩军、叶晓峰 |
1.3 |
2004-08-10 |
重写目录;一些小改动 |
白杨 |
|
1.4 |
2004-08-10 |
新增C++成长篇 :-) |
白杨 |
|
|
|
|
|
|
附件
本文档版权归作者所有。您可以以任意形式免费使用本文档的任意部分,并且无需通知作者。作者对使用本文档所造成的任何直接或者间接的损失不负任何责任。 |
对于任何工程项目来说,统一的施工标准都是保证工程质量的重要因素。堪称当今人类最抽象、最复杂的工程——软件工程,自然更加不能例外。
高品质、易维护的软件开发离不开清晰严格的编码规范。本文档详细描述C++软件开发过程中的编码规范。本规范也适用于所有在文档中出现的源码。
除了“语法高亮”部分,本文档中的编码规范都以:
的格式给出,其中强制性规则使用黑色,建议性规则使用灰色。
|
文字是信息的载体;文字使我们能够把个人的经验和思想长久的保存下来;文字使我们得以站在前人的肩膀上向前发展;文字的诞生标志着人类文明的开始……
扯的太离谱了?好吧,至少你应该承认:
- 没有文字就不可能出现计算机(先不管他是哪国字 )
- 没有文字大家就不可能(也没必要)学会如何写程序
- 在过去、现在和可见的将来,使用文字符号都是编写计算机软件的主要方式方法
既然文字如此重要,它的长相自然会受到广泛的关注。如今这个连MM都可以“千面”的年头,字体的种类当然是数不胜数。
然而,前辈先贤们曾经用篆体教导偶们:。想让大家读到缩进、对其正确一致,而且不出现乱码的源文件,我们就要使用相互兼容的字体。
字体规范如下:
使用等宽字体 |
由于非等宽字体在对其等方面问题多多,任何情况下,源码都必须使用等宽字体编辑和显示。
|
每个制表符(TAB)的宽度为4个半角字符 |
不一致的缩进宽度会导致行与行之间的参差不齐,进而严重影响代码的可读性。
|
优先使用Fixedsys |
在Windows平台中,应该优先使用字体:Fixedsys,这也是操作系统UI(所有的菜单、按钮、标题栏、对话框等等)默认使用的字体。该字体的好处很多:
|
|
|
所有C++的源文件均必须包含一个规范的文件头,文件头包含了该文件的名称、功能概述、作者、版权和版本历史信息等内容。标准文件头的格式为:
/*! @file ******************************************************************************** <PRE> 模块名 : <文件所属的模块名称> 文件名 : <文件名> 相关文件 : <与此文件相关的其它文件> 文件实现功能 : <描述该文件实现的主要功能> 作者 : <作者部门和姓名> 版本 : <当前版本号> -------------------------------------------------------------------------------- 备注 : <其它说明> -------------------------------------------------------------------------------- 修改记录 : 日 期 版本 修改人 修改内容 YYYY/MM/DD X.Y <作者或修改者名> <修改内容> </PRE> *******************************************************************************/
|
如果该文件有其它需要说明的地方,还可以专门为此扩展一节:
/*! @file ******************************************************************************** <PRE> 模块名 : <文件所属的模块名称> 文件名 : <文件名> 相关文件 : <与此文件相关的其它文件> 文件实现功能 : <描述该文件实现的主要功能> 作者 : <作者部门和姓名> 版本 : <当前版本号> -------------------------------------------------------------------------------- 备注 : <其它说明> -------------------------------------------------------------------------------- 修改记录 : 日 期 版本 修改人 修改内容 YYYY/MM/DD X.Y <作者或修改者名> <修改内容> </PRE> ********************************************************************************
* 项目1 - 项目1.1 - 项目1.2
================================================================================ * 项目2 - 项目2.1 - 项目2.2 ....
*******************************************************************************/
|
每行注释的长度都不应该超过80个半角字符。还要注意缩进和对其,以利阅读。
关于文件头的完整例子,请参见:文件头例子
关于文件头的模板,请参见:文件头注释模板
|
头文件通常由以下几部分组成:
文件头注释 |
每个头文件,无论是内部的还是外部的,都应该由一个规范的文件头注释作为开始。
|
预处理块 |
为了防止头文件被重复引用,应当用ifndef/define/endif结构产生预处理块。
|
函数和类/结构的声明等 |
声明模块的接口
|
需要包含的内联函数定义文件(如果有的话) |
如果类中的内联函数较多,或者一个头文件中包含多个类的定义(不推荐),可以将所有内联函数定义放入一个单独的内联函数定义文件中,并在类声明之后用“#include”指令把它包含进来。 |
头文件的编码规则:
引用文件的格式 |
用 #include <filename.h> 格式来引用标准库和系统库的头文件(编译器将从标准库目录开始搜索)。
用 #include "filename.h" 格式来引用当前工程中的头文件(编译器将从该文件所在目录开始搜索)。
|
分割多组接口(如果有的话) |
如果在一个头件中定义了多个类或者多组接口(不推荐),为了便于浏览,应该在每个类/每组接口间使用分割带把它们相互分开。 |
关于头文件的完整例子,请参见:头文件例子
|
如上所述,在内联函数较多的情况下,为了避免头文件过长、版面混乱,可以将所有的内联函数定义移到一个单独的文件中去,然后再用#include指令将它包含到类声明的后面。这样的文件称为一个内联函数定义文件。
按照惯例,应该将这个文件命名为“filename.inl”,其中“filename”与相应的头文件和实现文件相同。
内联函数定义文件由以下几部分组成:
文件头注释 |
每内联函数定义文件都应该由一个规范的文件头注释作为开始 |
内联函数定义 |
内联函数的实现体 |
内联函数定义文件的编码规则:
分割多组接口(如果有的话) |
如果在一个内联函数定义文件中定义了多个类或者多组接口的内联函数(不推荐),必须在每个类/每组接口间使用分割带把它们相互分开。
|
文件组成中为什么没有预处理块? |
与头文件不同,内联函数定义文件通常不需要定义预处理块,这是因为它通常被包含在与其相应的头文件预处理块内。 |
关于内联函数定义文件的完整例子,请参见:内联函数定义文件例子
|
实现文件包含所有数据和代码的实现体。实现文件的格式为:
文件头注释 |
每个实现文件都应该由一个规范的文件头注释作为开始
|
对配套头文件的引用 |
引用声明了此文件实现的类、函数及数据的头文件
|
对一些仅用于实现的头文件的引用(如果有的话) |
将仅与实现相关的接口包含在实现文件里(而不是头文件中)是一个非常好的编程习惯。这样可以有效地屏蔽不应该暴露的实现细节,将实现改变对其它模块的影响降低到最少 。
|
程序的实现体 |
数据和函数的定义 |
实现文件的编码规则:
分割每个部分 |
在本地(静态)定义和外部定义间,以及不同接口或不同类的实现之间,应使用分割带相互分开。 |
关于实现文件的完整例子,请参见:实现文件例子
|
由于项目性质、规模上存在着差异,不同项目间的文件组织形式差别很大。但文件、目录组织的基本原则应当是一致的:使外部接口与内部实现尽量分离;尽可能清晰地表达软件的层次结构……
为此提供两组典型项目的文件组织结构范例作为参考:
功能模块/库的文件组织形式
显而易见,编写功能模块和库的主要目的是为其它模块提供一套完成特定功能的API,这类项目的文件组织结构通常如下图所示:
其中:
contrib |
当前项目所依赖的所有第三方软件,可以按类别分设子目录。 |
doc |
项目文档 |
include |
声明外部接口的所有头文件和内联定义文件。 |
lib |
编译好的二进制库文件,可以按编译器、平台分设子目录。 |
makefile |
用于编译项目的makefile文件和project文件等。可以按编译器、平台分设子目录。 |
src |
所有实现文件和声明内部接口的头文件、内联定义文件。可按功能划分;支持编译器、平台等类别分设子目录。 |
test |
存放测试用代码的目录。 |
|
应用程序的文件组织形式
与功能模块不同,应用程序是一个交付给最终用于使用的、可以独立运行并提供完整功能的软件产品,它通常不提供编程接口,应用程序的典型文件组织形式如下图所示:
contrib |
当前项目所依赖的所有第三方软件,可以按类别分设子目录。 |
doc |
项目文档 |
makefile |
用于编译项目的makefile文件和project文件等。可以按编译器、平台分设子目录。 |
setup |
安装程序,以及制作安装程序所需要的项目文件和角本。 |
src |
所有源文件。可按功能划分;支持编译器、平台等类别分设子目录。 |
test |
存放测试用代码的目录。 |
|
|
|
|
如果想要有效的管理一个稍微复杂一点的体系,针对其中事物的一套统一、带层次结构、清晰明了的命名准则就是必不可少而且非常好用的工具。
活跃在生物学、化学、军队、监狱、黑社会、恐怖组织等各个领域内的大量有识先辈们都曾经无数次地以实际行动证明了以上公理的正确性。除了上帝(设它可以改变世间万物的秩序)以外,相信没人有实力对它不屑一顾
在软件开发这一高度抽象而且十分复杂的活动中,命名规则的重要性更显得尤为突出。一套定义良好并且完整的、在整个项目中统一使用的命名规范将大大提升源代码的可读性和软件的可维护性。
在引入细节之前,先说明一下命名规范的整体原则:
同一性 |
在编写一个子模块或派生类的时候,要遵循其基类或整体模块的命名风格,保持命名风格在整个模块中的同一性。
|
标识符组成 |
标识符采用英文单词或其组合,应当直观且可以拼读,可望文知意,用词应当准确。
|
最小化长度 && 最大化信息量原则 |
在保持一个标识符意思明确的同时,应当尽量缩短其长度。
|
避免过于相似 |
不要出现仅靠大小写区分的相似的标识符,例如“i”与“I”,“function”与“Function”等等。
|
避免在不同级别的作用域中重名 |
程序中不要出现名字完全相同的局部变量和全局变量,尽管两者的作用域不同而不会发生语法错误,但容易使人误解。
|
正确命名具有互斥意义的标识符 |
用正确的反义词组命名具有互斥意义的标识符,如:"nMinValue" 和 "nMaxValue","GetName()" 和 "SetName()" ....
|
避免名字中出现数字编号 |
尽量避免名字中出现数字编号,如Value1,Value2等,除非逻辑上的确需要编号。这是为了防止程序员偷懒,不肯为命名动脑筋而导致产生无意义的名字(因为用数字编号最省事)。 |
除了异常类等个别情况(不希望用户把该类看作一个普通的、正常的类之情况)外,C++类/结构的命名应该遵循以下准则:
C++类/结构的命名 |
类的名称都要以大写字母“C”开头,后跟一个或多个单词。为便于界定,每个单词的首字母要大写。
|
推荐的组成形式 |
类的命名推荐用"名词"或"形容词+名词"的形式,例如:"CAnalyzer", "CFastVector" .... |
不同于C++类的概念,传统的C结构体只是一种将一组数据捆绑在一起的方式。传统C结构体的命名规则为:
传统C结构体的命名 |
传统C结构体的名称全部由大写字母组成,单词间使用下划线界定,例如:"SERVICE_STATUS", "DRIVER_INFO" .... |
|
函数的命名 |
函数的名称由一个或多个单词组成。为便于界定,每个单词的首字母要大写。
|
推荐的组成形式 |
函数名应当使用"动词"或者"动词+名词"(动宾词组)的形式。例如:"GetName()", "SetValue()", "Erase()", "Reserve()" ....
|
保护成员函数 |
保护成员函数的开头应当加上一个下划线“_”以示区别,例如:"_SetState()" ....
|
私有成员函数 |
类似地,私有成员函数的开头应当加上两个下划线“__”,例如:"__DestroyImp()" ....
|
虚函数 |
虚函数习惯以“Do”开头,如:"DoRefresh()", "_DoEncryption()" ....
|
回调和事件处理函数 |
回调和事件处理函数习惯以单词“On”开头。例如:"_OnTimer()", "OnExit()" .... |
|
变量应该是程序中使用最多的标识符了,变量的命名规范可能是一套C++命名准则中最重要的部分:
变量的命名 |
变量名由作用域前缀+类型前缀+一个或多个单词组成。为便于界定,每个单词的首字母要大写。
对于某些用途简单明了的局部变量,也可以使用简化的方式,如:i, j, k, x, y, z ....
|
作用域前缀 |
作用域前缀标明一个变量的可见范围。作用域可以有如下几种:
前缀 |
说明 |
无 |
局部变量 |
m_ |
类的成员变量(member) |
sm_ |
类的静态成员变量(static member) |
s_ |
静态变量(static) |
g_ |
外部全局变量(global) |
sg_ |
静态全局变量(static global) |
gg_ |
进程间共享的共享数据段全局变量(global global) |
除非不得已,否则应该尽可能少使用全局变量。
|
类型前缀 |
类型前缀标明一个变量的类型,可以有如下几种:
前缀 |
说明 |
n |
整型和位域变量(number) |
e |
枚举型变量(enumeration) |
c |
字符型变量(char) |
b |
布尔型变量(bool) |
f |
浮点型变量(float) |
p |
指针型变量和迭代子(pointer) |
pfn |
特别针对指向函数的指针变量和函数对象指针(pointer of function) |
g |
数组(grid) |
i |
类的实例(instance)
对于经常用到的类,也可以定义一些专门的前缀,如:std::string和std::wstring类的前缀可以定义为"st",std::vector类的前缀可以定义为"v"等等。
|
类型前缀可以组合使用,例如"gc"表示字符数组,"ppn"表示指向整型的指针的指针等等。
|
推荐的组成形式 |
变量的名字应当使用"名词"或者"形容词+名词"。例如:"nCode", "m_nState","nMaxWidth" .... |
|
C++中引入了对常量的支持,常量的命名规则如下:
常量的命名 |
常量名由类型前缀+全大写字母组成,单词间通过下划线来界定,如:cDELIMITER, nMAX_BUFFER ....
类型前缀的定义与变量命名规则中的相同。
|
|
枚举、联合及typedef语句都是定义新类型的简单手段,它们的命名规则为:
枚举、联合、typedef的命名 |
枚举、联合、typedef语句生成的类型名由全大写字母组成,单词间通过下划线来界定,如:FAR_PROC, ERROR_TYPE .... |
|
宏、枚举值的命名 |
宏和枚举值由全大写字母组成,单词间通过下划线来界定,如:ERROR_UNKNOWN, OP_STOP .... |
|
C++名空间是“类”概念的一种退化(相当于只包含静态成员且不能实例化的类)。它的引入为标识符名称提供了更好的层次结构,使标识符看起来更加直观简捷,同时大大降低了名字冲突的可能性。
名空间的命名规则包括:
名空间的命名 |
名空间的名称不应该过长,通常都使用缩写的形式来命名。
例如,一个图形库可以将其所有外部接口存放在名空间"GLIB"中,但是将其换成"GRAPHIC_LIBRARY"就不大合适。
如果碰到较长的名空间,为了简化程序书写,可以使用:
namespace new_name = old_long_name; |
语句为其定义一个较短的别名。
|
|
|
代码风格的重要性怎么强调都不过分。一段稍长一点的无格式代码基本上是不可读的。
先来看一下这方面的整体原则:
空行的使用 |
空行起着分隔程序段落的作用。空行得体(不过多也不过少)将使程序的布局更加清晰。空行不会浪费内存,虽然打印含有空行的程序是会多消耗一些纸张,但是值得。所以不要舍不得用空行。
- 在每个类声明之后、每个函数定义结束之后都要加2行空行。
- 在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。
|
语句与代码行 |
- 一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样的代码容易阅读,并且方便于写注释。
- "if"、"for"、"while"、"do"、"try"、"catch" 等语句自占一行,执行语句不得紧跟其后。不论执行语句有多少都要加 "{ }" 。这样可以防止书写失误。
|
缩进和对齐 |
- 程序的分界符 "{" 和 "}" 应独占一行并且位于同一列,同时与引用它们的语句左对齐。
- "{ }" 之内的代码块在 "{" 右边一个制表符(4个半空格符)处左对齐。如果出现嵌套的 "{ }",则使用缩进对齐。
- 如果一条语句会对其后的多条语句产生影响的话,应该只对该语句做半缩进(2个半角空格符),以突出该语句。
例如:
void Function(int x) { CSessionLock iLock(*m_psemLock);
for (初始化; 终止条件; 更新) { // ... }
try { // ... } catch (const exception& err) { // ... } catch (...) { // ... }
// ... } |
|
最大长度 |
代码行最大长度宜控制在70至80个字符以内。代码行不要过长,否则眼睛看不过来,也不便于打印。
|
长行拆分 |
长表达式要在低优先级操作符处拆分成新行,操作符放在新行之首(以便突出操作符)。拆分出的新行要进行适当的缩进,使排版整齐,语句可读。
例如:
if ((very_longer_variable1 >= very_longer_variable2) && (very_longer_variable3 <= very_longer_variable4) && (very_longer_variable5 <= very_longer_variable6)) { dosomething(); } |
|
空格的使用 |
- 关键字之后要留空格。象 "const"、"virtual"、"inline"、"case" 等关键字之后至少要留一个空格,否则无法辨析关键字。象 "if"、"for"、"while"、"catch" 等关键字之后应留一个空格再跟左括号 "(",以突出关键字。
- 函数名之后不要留空格,紧跟左括号 "(" ,以与关键字区别。
- "(" 向后紧跟。而 ")"、","、";" 向前紧跟,紧跟处不留空格。
- "," 之后要留空格,如 Function(x, y, z)。如果 ";" 不是一行的结束符号,其后要留空格,如 for (initialization; condition; update)。
- 赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符,如"="、"+=" ">="、"<="、"+"、"*"、"%"、"&&"、"||"、"<<", "^" 等二元操作符的前后应当加空格。
- 一元操作符如 "!"、"~"、"++"、"--"、"&"(地址运算符)等前后不加空格。
- 象"[]"、"."、"->"这类操作符前后不加空格。
- 对于表达式比较长的for、do、while、switch语句和if语句,为了紧凑起见可以适当地去掉一些空格,如for (i=0; i<10; i++)和if ((a<=b) && (c<=d))
例如:
void Func1(int x, int y, int z); // 良好的风格 void Func1 (int x,int y,int z); // 不良的风格
// =========================================================== if (year >= 2000) // 良好的风格 if(year>=2000) // 不良的风格 if ((a>=b) && (c<=d)) // 良好的风格 if(a>=b&&c<=d) // 不良的风格
// =========================================================== for (i=0; i<10; i++) // 良好的风格 for(i=0;i<10;i++) // 不良的风格 for (i = 0; I < 10; i ++) // 过多的空格
// =========================================================== x = a < b ? a : b; // 良好的风格 x=a<b?a:b; // 不好的风格
// =========================================================== int* x = &y; // 良好的风格 int * x = & y; // 不良的风格
// =========================================================== array[5] = 0; // 不要写成 array [ 5 ] = 0; a.Function(); // 不要写成 a . Function(); b->Function(); // 不要写成 b -> Function(); |
|
修饰符的位置 |
- 为便于理解,应当将修饰符 "*" 和 "&" 紧靠数据类型。
例如:
char* name;
int* x, y; // 为避免y被误解为指针,这里必须分行写。
int* Function(void* p); |
参见:变量、常量的风格与版式 -> 指针或引用类型的定义和声明
|
注释 |
- 注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,不可放在下方。
- 边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。
- 注释应当准确、易懂,防止注释有二义性。错误的注释不但无益反而有害。
- 当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于阅读。
|
与常量的比较 |
在与宏、常量进行 "==", "!=", ">=", "<=" 等比较运算时,应当将常量写在运算符左边,而变量写在运算符右边。这样可以避免因为偶然写错把比较运算变成了赋值运算的问题。
例如:
if (NULL == p) // 如果把 "==" 错打成 "=",编译器就会报错 { // ... } |
|
为增强代码的可读性而定义的宏 |
以下预定义宏对程序的编译没有任何影响,只为了增加代码的可读性:
宏 |
说明 |
NOTE |
需要注意的代码 |
TODO |
尚未实现的接口、类、算法等 |
FOR_DBG |
标记为调试方便而临时增加的代码 |
OK |
仅用于调试的标记 |
例如:
TODO class CMyClass; TODO void Function(void);
FOR_DBG cout << "..."; |
|
类是C++中最重要也是使用频率最高的新特性之一。类的版式好坏将极大地影响代码品质。
注释头与类声明 |
与文件一样,每个类应当有一个注释头用来说明该类的各个方面。
类声明换行紧跟在注释头后面,"class" 关键字由行首开始书写,后跟类名称。界定符 "{" 和 "};" 应独占一行,并与 "class" 关键字左对其。
/*! @class ******************************************************************************** <PRE> 类名称 : CXXX 功能 : <简要说明该类所完成的功能> 异常类 : <属于该类的异常类(如果有的话)> -------------------------------------------------------------------------------- 备注 : <使用该类时需要注意的问题(如果有的话)> 典型用法 : <如果该类的使用方法较复杂或特殊,给出典型的代码例子> -------------------------------------------------------------------------------- 作者 : <xxx> </PRE> *******************************************************************************/ class CXXX { // ... }; |
对于功能明显的简单类(接口小于10个),也可以使用简单的单行注释头:
//! <简要说明该类所完成的功能> class CXXX { // ... }; |
|
继承 |
基类直接跟在类名称之后,不换行,访问说明符(public, private, 或protected)不可省略。如:
class CXXX : public CAAA, private CBBB { // ... }; |
|
以行为为中心 |
没人喜欢上来就看到一大堆私有数据,大多数用户关心的是类的接口与其提供的服务,而不是其实现。
所以应当将公有的定义和成员放在类声明的最前面,保护的放在中间,而私有的摆在最后。
|
访问说明符 |
访问说明符(public, private, 或protected)应该独占一行,并与类声明中的‘class’关键字左对其。
|
类成员的声明版式 |
对于比较复杂(成员多于20个)的类,其成员必须分类声明。
每类成员的声明由访问说明符(public, private, 或protected)+ 全行注释开始。注释不满全行(80个半角字符)的,由 "/" 字符补齐,最后一个 "/" 字符与注释间要留一个半角空格符。
如果一类声明中有很多组功能不同的成员,还应该用分组注释将其分组。分组注释也要与 "class" 关键字对其。
每个成员的声明都应该由 "class" 关键字开始向右缩进一个制表符(4个半角空格符),成员之间左对其。
例如:
class CXXX { public: /////////////////////////////////////////////////////////////////////// 类型定义 typedef vector<string> VSTR;
public: ///////////////////////////////////////////////////////////// 构造、析构、初始化 CXXX(); ~CXXX();
public: /////////////////////////////////////////////////////////////////////// 公用方法
// [[ 功能组1 void Function1(void) const; long Function2(IN int n); // ]] 功能组1
// [[ 功能组2 void Function3(void) const; bool Function4(OUT int& n); // ]] 功能组2
private: /////////////////////////////////////////////////////////////////////////// 属性 // ...
private: ///////////////////////////////////////////////////////////////////// 禁用的方法 // 禁止复制 CXXX(IN const CXXX& rhs); CXXX& operator=(IN const CXXX& rhs); }; |
|
正确地使用const和mutable |
把不改变对象逻辑状态的成员都标记为const成员不仅有利于用户对成员的理解,更可以最大化对象使用方式的灵活性及合理性(比如通过const指针或const引用的形式传递一个对象)。
如果某个属性的改变并不影响该对象逻辑上的状态,而且这个属性需要在const方法中被改变,则该属性应该标记为 "mutable"。
例如:
class CString { public: //! 查找一个子串,find()不会改变字符串的值所以为const函数 int find(IN const CString& str) const; // ...
private: // 最后一次错误值,改动这个值不会影响对象的逻辑状态, // 像find()这样的const函数也可能修改这个值 mutable int m_nLastError; // ... }; |
|
嵌套的类声明 |
在相应的逻辑关系确实存在时,类声明可以嵌套。嵌套类可以使用简单的单行注释头:
class CXXX { //! 嵌套类说明 calss CYYY { // ... }; }; |
|
初始化列表 |
应当尽可能通过构造函数的初始化列表来初始化成员和基类。初始化列表至少独占一行,并且与构造函数的定义保持一个制表符(4个半角空格)的缩进。
例如:
CXXX::CXXXX(IN int nA, IN bool bB) : m_nA(nA), m_bB(bB) { // ... }; |
初始化列表的书写顺序应当与对象的构造顺序一致,即:先按照声明顺序写基类初始化,再按照声明顺序写成员初始化。
如果一个成员 "a" 需要使用另一个成员 "b" 来初始化,则 "b" 必须在 "a" 之前声明,否则将会产生运行时错误(有些编译器会给出警告)。
例如:
// ...
class CXXXX : public CAA, public CBB { // ... CYY m_iA; CZZ m_iB; // m_iA必须在m_iB之前声明 };
CXXX::CXXXX(IN int nA, IN int nB, IN bool bC) : CAA(nA), CBB(nB), m_iA(bC), m_iB(m_iA) // 先基类,后成员, // 分别按照声明顺序书写 { // ... }; |
|
内联函数的实现体 |
定义在类声明之中的函数将自动成为内联函数。但为了使类的声明更为清晰明了,应尽量避免直接在声明中直接定义成员函数的编程风格。鼓励使用 "inline" 关键字将内联函数放在类声明的外部定义。 |
关于类声明的例子,请参见:类/结构的风格与版式例子
关于类声明的模板,请参见:类声明模板
|
函数是程序执行的最小单位,任何一个有效的C/C++程序都少不了函数。
函数原型 |
函数原型的格式为:
[存储类] 返回值类型 [名空间或类]::函数名(参数列表) [const说明符] [异常过滤器] |
例如:
static inline void Function1(void)
int CSem::Function2(IN const char* pcName) const throw(Exp)
|
其中:
|
函数声明 |
函数声明的格式为:
例如:
//! 执行某某操作 static void Function(void); |
函数声明和其它代码间要有空行分割。
声明成员函数时,为了紧凑,返回值类型和函数名之间不用换行,也可以适当减少声明间的空行。
|
函数定义 |
函数定义使用如下格式:
/*! @function ******************************************************************************** <PRE> 函数名 : <函数名> 功能 : <函数实现功能> 参数 : <参数类表及说明(如果有的话),格式为:> [IN|OUT] 参数1 : 参数说明 [IN|OUT] 参数2 : 参数说明 ... 返回值 : <函数返回值的意义(如果有的话)> 抛出异常 : <可能抛出的异常及其说明(如果有的话),格式为:> 类型1 : 说明 类型2 : 说明 ... -------------------------------------------------------------------------------- 备注 : <其它注意事项(如果有的话)> 典型用法 : <如果该函数的使用方法较复杂或特殊,给出典型的代码例子> -------------------------------------------------------------------------------- 作者 : <xxx> </PRE> *******************************************************************************/ 函数原型 { // ... } |
对于返回值、参数意义都很明确简单函数(代码不超过20行),也可以使用单行函数头:
|
参数描述宏 |
以下预定义宏对程序的编译没有任何影响,只为了增强对参数的理解:
宏 |
说明 |
IN |
输入参数 |
OUT |
输出参数 |
OPTIONAL |
可选参数-通常指可以为NULL的指针参数,带默认值的参数不需要这样标明 |
RESERVED |
这个参数当前未被支持,留待以后扩展 |
OWNER |
获得参数的所有权,调用者不再负责销毁参数指定的对象 |
UNUSED |
标明这个参数在此版本中已不再使用 |
CHANGED |
参数类型发出变化 |
ADDED |
新增的参数 |
NOTE |
需要注意的参数-参数意义发生变化 |
其中:
-
除了空参数 "void" 以外,每个参数左侧都必须有 "IN" 和/或 "OUT" 修饰
-
既输入又输出的参数应记为:"IN OUT",而不是 "OUT IN"
-
IN/OUT的左侧还可以根据需要加入一个或多个上表中列出的其它宏
参数描述宏的使用思想是:只要一个宏可以用在指定参数上(即:对这个参数来说,用这个描述宏修饰它是贴切的),那么就应当使用它。
也就是说,应该把能用的描述宏都用上,以期尽量具体地描述一个参数。
|
参数列表 |
参数列表的格式为:
参数描述宏1 参数类型1 参数1, 参数描述宏2 参数类型2 参数2, ... |
例如:
IN const int nCode, OUT string& nName
OWNER IN CDatabase* pDB, OPTIONAL IN OUT int* pRecordCount = NULL
IN OUT string& stRuleList, RESERVED IN int nOperate = 0
... |
其中:
|
存储类 |
"extern", "static", "inline" 等函数存储类说明应该在声明和定义中一致并且显式地使用。不允许隐式地使用一个类型声明,也不允许一个类型声明仅存在于函数的声明或定义中。
|
成员函数的存储类 |
由于C++语言的限制,成员函数的 "static", "virtual", "explicit" 等存储类说明不允许出现在函数定义中。
但是为了明确起见,这些存储类应以注释的形式在定义中给出。
例如:
/*virtual*/ CThread::EXITCODE CSrvCtl::CWrkTrd::Entry(void) { // ... }
/*static*/ inline void stringEx::regex_free(IN OUT void*& pRegEx) { // ... }
|
特别地,为缩短声明的长度,"inline" 关键字可以在成员函数声明中省略。
|
默认参数 |
类似地,参数的默认值只能出现在函数声明中,但是为了明确起见,这些默认值应以注释的形式在定义中给出。
例如:
bool stringEx::regex_find(OUT VREGEXRESULT& vResult, IN stringEx stRegEx, IN size_t nIndex /*= 0*/, IN size_t nStartPos /*= 0*/, IN bool bNoCase /*= false*/, IN bool bNewLine /*= true*/, IN bool bExtended /*= true*/, IN bool bNotBOL /*= false*/, IN bool bNotEOL /*= false*/, IN bool bUsePerlStyle /*= false*/) const { // ... }
|
|
异常过滤器 |
对于任何肯能抛出异常的函数,必须在其声明和定义中显式地指定异常过滤器,并在过滤器中列举该函数可能抛出的异常。
例如:
int Function(IN const char* pcName) throw(byExp, exception); |
|
代码段注释 |
如果函数体中的代码较长,应该根据功能不同将其分段。代码段间以空行分离,并且每段代码都以代码段分割注释作为开始。
例如:
void CXXX::Function(IN void* pmodAddr) { if (NULL == pmodAddr) return;
{ CSessionLock iLock(*sm_hSELock);
// ===================================================================== // = 判断指定模块是不是刚刚被装入,由于在NT系列平台中,“A”系列函数都是 // = 由“W”系列函数实现的。所以可能会有一次LoadLibrary产生多次本函数调 // = 用的情况。为了增加效率,特设此静态变量判断上次调用是否与本次相同。 static PVOID pLastLoadedModule = NULL; if (pLastLoadedModule == pmodAddr) { return; // 相同,忽略这次调用 } pLastLoadedModule = pmodAddr;
// ===================================================================== // = 检查这个模块是否在旁路模块表中 stringEx stModName; if (!BaiY_IMP::GetModuleNameByAddress(pmodAddr, stModName)) { return; } if (CHookProc::sm_sstByPassModTbl.find(stModName) != CHookProc::sm_sstByPassModTbl.end()) { return; }
// ===================================================================== // = 在这个模块中HOOK所有存在于HOOK函数表中的函数 PROCTBL::iterator p; for (p=sm_iProcTbl.begin(); p!=sm_iProcTbl.end(); ++p) { p->HookOneModule(pmodAddr); } } // SessionLock }
|
明显地,如果需要反复用到一段代码的话,这段代码就应当作为一个函数实现。
当一个函数过长时(超过100行),为了便于阅读和理解,也应当将其中的一些代码段实现为单独的函数。
|
调用系统API |
所有系统API调用前都要加上全局名称解析符 "::"。
例如:
::MessageBoxA(NULL, gcErrorMsg, "!FATAL ERROR!", MB_ICONSTOP|MB_OK);
if (0 == ::GetTempFileName(m_basedir.c_str(), byT("bai"), 0, stR.ref())) { // ... }
|
|
关于函数的例子,请参见:函数的风格与版式例子
关于函数的模板,请参见:函数模板
|
声明格式 |
变量、常量的声明格式如下:
其中:
-
以 "[ ]" 括住的为可选项目。
-
"存储类" 的说明见下文
|
定义格式 |
变量、常量的定义格式如下:
其中:
-
以 "[ ]" 括住的为可选项目。
-
"存储类" 的说明见下文
|
存储类 |
除 "auto" 类型以外,诸如 "extern", "static", "register", "volatile" 等存储类均不可省略,且必须在声明和定义中一致地使用(即:不允许仅在声明或定义中使用)。
|
成员变量的存储类 |
由于C++语言的限制,成员变量的 "static" 等存储类说明不允许出现在变量定义中。
但是为了明确起见,这些存储类应以注释的形式在定义中给出。
例如:
/*static*/ int CThread::sm_nPID = 0;
|
|
指针或引用类型的定义和声明 |
在声明和定义多个指针或引用变量/常量时,每个变量至少占一行。例如:
int* pn1, * pn2 = NULL, * pn3;
char* pc1; char* pc2; char* pc3;
// 错误的写法: int* pn11, *pn12, *pn13;
|
|
常指针和指针常量 |
声明/定义一个常指针(指向常量的指针)时,"const" 关键字一律放在类型说明的左侧。
声明/定义一个指针常量(指针本身不能改变)时, "const" 关键字一律放在变量左侧、类型右侧。
例如:
const char* pc1; // 常指针 char* const pc2; // 指针常量 const char* const pc3; // 常指针常量
// 错误的写法: char const* pc1; // 与 const char* pc1 含义相同,但不允许这样写
|
|
全局变量、常量的注释 |
全局变量、常量的注释独占一行,并用 "//!" 开头。
例如:
//! 当前进程的ID static int sg_nPID = 0;
//! 分割符 static const char* pcDTR = "\\/";
|
|
类型转换 |
禁止使用C风格的 "(类型)" 格式转换,应当优先使用C++的 "xxx_cast" 风格的类型转换。C++风格的类型转换可以提供丰富的含义和功能,以及更好的类型检查机制,这对代码的阅读、修改、除错和移植有很大的帮助。其中:
static_cast |
static_cast用于编译器认可的,安全的静态转换,比如将 "char" 转为 "int" 等等。该操作在编译时完成 |
reinterpret_cast |
reinterpret_cast用于编译器不认可的,不安全的静态转换,比如将 "int*" 转为 "int" 等等。这种转换有可能产生移植性方面的问题,该操作在编译时完成 |
const_cast |
const_cast用于将一个常量转化为相应类型的变量,比如将 "const char*" 转换成 "char*" 等等。这种转换通常伴随潜在的错误。该操作在编译时完成 |
dynamic_cast |
dynamic_cast是C++RTTI机制的重要体现,用于在类层次结构中漫游。dynamic_cast可以对指针和引用进行自由度很高的向上、向下和交叉转换。被正确使用的dynamic_cast操作将在运行时完成 |
此外,对于定义了 单参构造函数 或 类型转换操作 的类来说,应当优先使用构造函数风格的类型转换,如:"string("test")" 等等。
通常来说,"xxx_cast" 格式的转换与构造函数风格的类型转换之间,最大的区别在于:构造函数风格的转换通常会生成新的临时对象,可能伴随相当的时间和空间开销。
而 "xxx_cast" 格式的转换只是告诉编译器,将指定内存中的数据当作另一种类型的数据看待,这些操作一般在编译时完成,不会对程序的运行产生额外开销。当然,"dynamic_cast" 则是一个例外。
参见:RTTI、虚函数和虚基类的开销分析和使用指导
|
|
枚举、联合的定义格式 |
枚举、联合的定义格式为:
//! 说明(可选) enum|union 名称 { 内容 }; |
例如:
//! 服务的状态 enum SRVSTATE { SRV_INVALID = 0, SRV_STARTING = 1, SRV_STARTED, SRV_PAUSING, SRV_PAUSED, SRV_STOPPING, SRV_STOPPED };
//! 32位整数 union INT32 { unsigned char cByte[4]; unsigned short nShort[2]; unsigned long nFull; };
|
|
typedef的定义格式 |
typedef 的定义格式为:
//! 说明(可选) typedef 原类型 新类型; |
例如:
//! 返回值类型 typedef int EXITCODE;
//! 字符串数组类型 typedef vector<string> VSTR;
|
|
|
何时使用宏 |
应当尽量减少宏的使用,在所有可能的地方都使用常量和内联函数来代替宏 。
|
边界效应 |
使用宏的时候应当注意边界效应,例如,以下代码将会得出错误的结果:
#define PLUS(x,y) x+y
cout << PLUS(1,1) * 2; |
以上程序的执行结果将会是 "3",而不是 "4",因为 "PLUS(1,1) * 2" 表达式将会被展开为:"1 + 1 * 2"。
在定义宏的时候,只要允许,就应该为它的替换内容括上 "( )" 或 "{ }"。例如:
#define PLUS(x,y) (x+y)
#define SAFEDELETE(x) {delete x; x=0} |
|
|
名空间的使用 |
名空间可以避免名字冲突、分组不同的接口以及简化命名规则。应当尽可能地将所有接口都放入适当的名字空间中。
|
将实现和界面分离 |
提供给用户的界面和用于实现的细节应当分别放入不同的名空间中。
例如:如果将一个软件模块的所有接口都放在名空间 "MODULE" 中,那么这个模块的所有实现细节就可以放入名空间 "MODULE_IMP" 中。
|
|
异常使C++的错误处理更为结构化;错误传递和故障恢复更为安全简便;也使错误处理代码和其它代码间有效的分离开来。
何时使用异常 |
异常机制只用在发生错误的时候,仅在发生错误时才应当抛出异常。这样做有助于错误处理和程序动作两者间的分离,增强程序的结构化,还保证了程序的执行效率。
确定某一状况是否算作错误有时会很困难。比如:未搜索到某个字符串、等待一个信号量超时等等状态,在某些情况下可能并不算作一个错误,而在另一些情况下可能就是一个致命错误。
有鉴于此,仅当某状况必为一个错误时(比如:分配存储失败、创建信号量失败等),才应该抛出一个异常。而对另外一些模棱两可的情况,就应当使用返回值等其它手段报告 。
|
用异常代替goto等其它错误处理手段 |
曾经被广泛使用的传统错误处理手段有goto风格和do...while风格等,以下是一个goto风格的例子:
//! 使用goto进行错误处理的例子 bool Function(void) { int nCode, i; bool r = false;
// ...
if (!Operation1(nCode)) { goto onerr; }
try { Operation2(i); } catch (...) { r = true; goto onerr; }
r = true;
onerr: // ... 清理代码 return r; }
|
由上例可见,goto风格的错误处理至少存在问题如下:
-
错误处理代码和其它代码混杂在一起,使程序不够清晰易读
-
变量必须在 "goto" 之前声明,违反就近原则
-
多处跳转的使用破坏程序的结构化,影响程序的可读性,使程序容易出错
-
对每个会抛出异常的操作都需要用额外的try...catch块检测和处理
-
稍微复杂一点的分类错误处理要使用多个标号和不同的goto跳转(如: "onOp1Err", "onOp2Err" ...)。这将使程序变得无法理解和错误百出。
再来看看do...while风格的错误处理:
//! 使用do...while进行错误处理的例子 bool Function(void) { int nCode, i; bool r = false;
// ...
do { if (!Operation1(nCode)) { break; }
do { try { Operation2(i); } catch (...) { r = true; break; } } while (Operation3())
r = true;
} while (false);
// ... 清理代码 return r; }
|
与goto风格的错误处理相似:
此外,还有一种更为糟糕的错误处理风格——直接在出错的位置完成错误处理:
//! 直接进行错误处理的例子 bool Function(void) { int nCode, i;
// ...
if (!Operation1(nCode)) { // ... 清理代码 return false; }
try { Operation2(i); } catch (...) { // ... 清理代码 return true; }
// ...
// ... 清理代码 return true; }
|
这种错误处理方式所带来的隐患可以说是无穷无尽,这里不再列举。
与传统的错误处理方法不同,C++的异常机制很好地解决了以上问题。使用异常做出错处理时,可以将大部分动作都包含在一个try块中,并以不同的catch块捕获和处理不同的错误:
//! 使用异常进行错误处理的例子 bool Function(void) { int nCode, i; bool r = false;
try { if (!Operation1(nCode)) { throw false; }
Operation2(i); } catch (bool err) { // ... r = err; } catch (const excption& err) { // ... excption类错误处理 } catch (...) { // ... 处理其它错误 }
// ... 清理代码 return r; } |
以上代码示例中,错误处理和动作代码完全分离,错误分类清晰明了,好处不言而喻。
|
构造函数中的异常 |
在构造函数中抛出异常将中止对象的构造,这将产生一个没有被完整构造的对象。
对于C++来说,这种不完整的对象将被视为并为创建而不被认可,也意味着其析构函数永远不会被调用。这个行为本身无可非议,就好像公安局不会为一个流产的婴儿发户口一样。但是这有时也会产生一些问题,例如:
class CSample { // ...
char* m_pc; };
CSample::CSample() { m_pc = new char[256]; // ... throw -1; // m_pc将永远不会被释放 }
CSample::~CSample() // 析构函数不会被调用 { delete m_pc; } |
解决这个问题的方法是在抛出异常以前释放任何已被申请的资源。一种更好的方法是使用“资源申请即初始化”的类型(如:句柄类、灵巧指针类等等)来代替一般的指针类型,如:
templete <class T> struct CAutoPtr { CAutoPtr(T* p = NULL) : m_p(p) {}; ~CAutoPtr() {delete m_p;} T* operator=(T* rhs) { if (rhs == m_p) return m_p; delete m_p; m_p = rhs; return m_p; } // ... T* m_p; };
class CSample { // ...
CAutoPtr<char> m_hc; };
CSample::CSample() { m_hc = new char[256]; // ... throw -1; // 由于m_hc已经成功构造,m_hc.~CAutoPtr()将会 // 被调用,所以申请的内存将被释放 } |
注意:上述CAutoPtr类仅用于示范,对于所有权语义的通用自动指针,应该使用C++标准库中的 "auto_ptr" 模板类。对于带引用计数和自定义销毁策略的通用句柄类,可以使用白杨工具库中的 "CHandle" 模板类。
|
析构函数中的异常 |
析构函数中的异常可能在2种情况下被抛出:
- 对象被正常析构时
- 在一个异常被抛出后的退栈过程中——异常处理机制退出一个作用域,其中所有对象的析构函数都将被调用。
由于C++不支持异常的异常,上述第二种情况将导致一个致命错误,并使程序中止执行。例如:
class CSample { ~CSample(); // ... };
CSample::~CSample() { // ... throw -1; // 在 "throw false" 的过程中再次抛出异常 }
void Function(void) { CSample iTest; throw false; // 错误,iTest.~CSample()中也会抛出异常 } |
如果必须要在析构函数中抛出异常,则应该在异常抛出前用 "std::uncaught_exception()" 事先判断当前是否存在已被抛出但尚未捕获的异常。例如:
class CSample { ~CSample(); // ... };
CSample::~CSample() { // ... if (!std::uncaught_exception()) // 没有尚未捕获的异常 { throw -1; // 抛出异常 } }
void Function(void) { CSample iTest; throw false; // 可以,iTest.~CSample()不会抛出异常 } |
|
异常的组织 |
异常类型应该以继承的方式组织成一个层次结构,这将使以不同粒度分类处理错误成为可能。
通常,某个软件生产组织的所有异常都从一个公共的基类派生出来。而每个类的异常则从该类所属模块的公共异常基类中派生。例如:
|
异常捕获和重新抛出 |
- 异常捕获器的书写顺序应当由特殊到一般(先子类后基类),最后才是处理所有异常的捕获器("catch(...)")。否则将使某些异常捕获器永远不会被执行。
- 为避免捕获到的异常被截断,异常捕获器中的参数类型应当为常引用型或指针型。
- 在某级异常捕获器中无法被彻底处理的错误可以被重新抛出。重新抛出采用一个不带运算对象的 "throw" 语句。重新抛出的对象就是刚刚被抛出的那个异常,而不是处理器捕获到的(有可能被截断的)异常。
例如:
try { // ... } // 公钥加密错误 catch (const CPubKeyCipher::Exp& err) { if (可以恢复) { // 恢复错误 } else { // 完成能做到的事情 throw; // 重新抛出 } } // 处理其它加密库错误 catch (const CryptoExp& err) { // ... } // 处理其它本公司模块抛出的错误 catch (const CompanyExp& err) { // ... } // 处理 dynamic_cast 错误 catch (const bad_cast& err) { // ... } // 处理其它标准库错误 catch (const exception& err) { // ... } // 处理所有其它错误 catch (...) { throw; // 重新抛出 } |
|
异常和效率 |
对于几乎所有现代编译器来说,在不抛出异常的情况下,异常处理的实现在运行时不会有任何额外开销,也就是说:正常情况下,异常机制比传统的通过返回值判断错误的开销还来得小。
相对于函数返回和调用的开销来讲,异常抛出和捕获的开销通常会来得大一些。不过错误处理代码通常不会频繁调用,所以错误处理时开销稍大一点基本上不是什么问题。这也是我们提倡仅将异常用于错误处理的原因之一。
更多关于效率的讨论,参见:RTTI、虚函数和虚基类的开销分析和使用指导
|
|
|
|