C++编码规范与指导

文字是信息的载体;文字使我们能够把个人的经验和思想长久的保存下来;文字使我们得以站在前人的肩膀上向前发展;文字的诞生标志着人类文明的开始……

扯的太离谱了?好吧,至少你应该承认:

  • 没有文字就不可能出现计算机(先不管他是哪国字
  • 没有文字大家就不可能(也没必要)学会如何写程序
  • 在过去、现在和可见的将来,使用文字符号都是编写计算机软件的主要方式方法

既然文字如此重要,它的长相自然会受到广泛的关注。如今这个连MM都可以“千面”的年头,字体的种类当然是数不胜数。

然而,前辈先贤们曾经用篆体教导偶们:。想让大家读到缩进、对其正确一致,而且不出现乱码的源文件,我们就要使用相互兼容的字体。

字体规范如下:

使用等宽字体由于非等宽字体在对其等方面问题多多,任何情况下,源码都必须使用等宽字体编辑和显示。

 

每个制表符(TAB)的宽度为4个半角字符不一致的缩进宽度会导致行与行之间的参差不齐,进而严重影响代码的可读性。

 

优先使用Fixedsys在Windows平台中,应该优先使用字体:Fixedsys,这也是操作系统UI(所有的菜单、按钮、标题栏、对话框等等)默认使用的字体。该字体的好处很多:
  • 兼容性好:所有Windows平台都支持该字体
     
  • 显示清晰:该字体为点阵字体,相对于矢量字体来说在显示器中呈现的影像更为清晰。矢量字体虽然可以自由缩放,但这个功能对于纯文本格式的程序源码来说没有任何实际作用。

    而且当显示字号较小(12pt以下)时,矢量字体还有一些明显的缺陷:
     
    • 文字的边缘会有严重的凹凸感。
    • 一些笔画的比例也会失调。
    • 开启了柔化字体边缘后,还会使文字显得模糊不清。

    说句题外话,这也是Gnome和KDE等其它GUI环境不如Windows的一个重要方面。
     

  • 支持多语言:Fixedsys是UNICODE字体,支持世界上几乎所有的文字符号。这对编写中文注释是很方便的。

 

语法高亮

几乎所有的现代源码编辑器均不同在程度上支持语法高亮显示的功能。缤纷的色彩不但可以吸引MM们的目光,还可以在很大程度上帮助我们阅读那些奥涩如咒语般的源代码。

统一的语法高亮规则不仅能让我们望色生意,还可以帮助我们阅读没有 编码规范,或者规范执行很烂的源码。

所有在文档中出现的代码段均必须严格符合下表定义的语法高亮规范。在编辑源码时,应该根据编辑器支持的自定义选项最大限度地满足下表定义的高亮规范。

类型颜色举例
注释 R0;G128;B0(深绿)// 注释例子
关键字 R0;G0;B255(蓝)typedef, int, dynamic_cast class ...
类、结构、联合、枚举等其它自定义类型 R0;G0;B255(蓝)class CMyClass, enum ERRTYPE, typedef int CODE ...
名空间 R0;G0;B255(蓝)namespace BaiY
数字 R255;G0;B0(红)012 119u 0xff ...
字符、字符串 R0;G128;B128(深蓝绿)"string", 'c ...
宏定义、枚举值 R255;G128;B0(橙黄)#define UNICODE, enum { RED, GREEN, BLUE };
操作符 R136;G0;B0(棕色)< > , = + - * / ; { } ( ) [ ] ...
方法/函数 R136;G0;B0(棕色)MyFunc()
变量 R128;G128;B128(中灰色)int nMyVar;
背景 R255;G255;B255(白色) 
其它 R0;G0;B0(黑色)other things(通常是一个错误)

 

返回目录


文件结构

文件头注释

所有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的命名枚举、联合、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 << "...";

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值