C++规范的整理---王海波

 

C++编程风格与规范

本文档中的编码规范都以:

规则(或建议)

解释

的格式给出,其中强制性规则使用黑色,建议性规则使用灰色

1.      排版

缩进

n  程序块要采用缩进风格编写,缩进采用TAB键,长度统一为4个半角字符。

n  对齐只使用TAB键,不使用空格键。

n  函数或过程的开始、结构的定义及循环、判断等语句中的代码都要采用缩进风格,case语句下的情况处理语句也要遵从语句缩进要求。

最大长度、长行拆分

较长的语句(>80字符)要分成多行书写,长表达式要在低优先级操作符处划分新行,操作符放在新行之首,划分出的新行要进行适当的缩进,使排版整齐,语句可读。

例如:

if ((very_longer_variable1 >= very_longer_variable2)
    && (very_longer_variable3 <= very_longer_variable4)
    && (very_longer_variable5 <= very_longer_variable6))
{
    DoSomething();
}

空行的使用

相对独立的程序块之间必须加空行。

n  在每个类声明之后、每个函数定义结束之后都要加2空行。

n  在一个函数体内,逻辑上密切相关的语句之间不加空行,其它地方应加空行分隔。

一行语句独占一行

n  一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样的代码容易阅读,并且方便于写注释。

n  "if""for""while""do""try""catch" 等语句自占一行,执行语句不得紧跟其后。不论执行语句有多少都要加 "{ }" 。这样可以防止书写 和修改代码时出现失误。

函数参数较长,进行划分

对于参数比较多的函数,声明或者定义时注意把参数进行分行编写,适当时候预留空间给予注释。

空格的使用

n  关键字之后要留空格。像 "const""virtual""inline""case" 等关键字之后至少要留一个空格,否则无法辨析关键字。像 "if""for""while""catch" 等关键字之后应留一个空格再跟左括号 "(",以突出关键字。

n  函数名之后不要留空格,紧跟左括号 "(" ,以与关键字区别。

n  "(" 向后紧跟。而 ")"","";" 向前紧跟,紧跟处不留空格。

n  "," 之后要留空格,如 Function(x, y, z)。如果 ";" 不是一行的结束符号,其后要留空格,如 for (initialization; condition; update)

n  赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符,如"=""+=" ">=""<=""+""*""%""&&""||""<<", "^" 等二元操作符的前后应当加空格。

n  一元操作符如 "!""~""++""--""&"(地址运算符)等前后不加空格。

n  "[]"".""->"这类操作符前后不加空格。

n  对于表达式比较长的fordowhileswitch语句和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();

注释

n  一般情况下,源程序有效注释量必须在20%以上。

n  注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,不可放在下方。将注释与其上面的代码用空行隔开。

n  边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。

n  注释应当准确、易懂,防止注释有二义性。错误的注释不但无益反而有害。避免在注释中使用缩写,特别是非常用缩写。

n  当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于阅读。

n  对于所有有物理含义的变量、常量,如果其命名不是充分自注释的,在声明时都必须加以注释,说明其物理含义。变量、常量、宏的注释应放在其上方相邻位置或右方。

n  文件头部应进行注释,列出:版权说明、版本号、生成日期、作者、模块目的/功能、主要函数及其功能、修改日志等。

例如:

// *! @file

// *********************************************************************

// <PRE>

// 模块名       : <文件所属的模块名称>

// 文件名       : <文件名>

// 相关文件     : <与此文件相关的其它文件>

// 文件实现功能 : <描述该文件实现的主要功能>

// 作者         : <作者部门和姓名>

// 版本         : <当前版本号>

//  --------------------------------------------------------------------

// 多线程安全性 : </>[,说明]

// 异常时安全性 : </>[,说明]

// --------------------------------------------------------------------

// 备注         : <其它说明>

// ---------------------------------------------------------------------

//修改记录 :

//         版本     修改人              修改内容

// YYYY/MM/DD   X.Y      <作者或修改者名>    <修改内容>

// </PRE>

// **********************************************************************

附注:尽量避免使用/*  */风格的注释,因为它不支持嵌套,容易引起问题。以上例中,每个“//”之后统一加入两个空格,为了美观。

n  函数头部应进行注释,列出:函数的目的/功能、输入参数、输出参数、返回值、调用关系(函数、表)等。

n  数据结构声明(包括数组、结构、类、枚举等),如果其命名不是充分自注释的,必须加以注释。对数据结构的注释应放在其上方相邻位置,不可放在下面;对结构中的每个域的注释放在此域的右方。

n  全局变量要有较详细的注释,包括对其功能、取值范围、哪些函数或过程存取它以及存取时注意事项等的说明。

n  对于switch语句下的case语句,如果因为特殊情况需要处理完一个case后进入下一个case处理,必须在该case语句处理完、下一个case语句前加上明确的注释。

2.      命名规则

如果想要有效的管理一个稍微复杂一点的体系,针对其中事物的一套统一、带层次结构、清晰明了的命名准则就是必不可少而且非常好用的工具。

在软件开发这一高度抽象而且十分复杂的活动中,命名规则的重要性更显得尤为突出。一套定义良好并且完整的、在整个项目中统一使用的命名规范将大大提升源代码的可读性和软件的可维护性。

在引入细节之前,先说明一下命名规范的整体原则:

同一性

在编写一个子模块或派生类的时候,要遵循其基类或整体模块的命名风格,保持命名风格在整个模块中的同一性。

标识符组成

标识符采用英文单词或其组合,应当直观且可以拼读,可望文知意,用词应当准确。

最小化长度 && 最大化信息量原则

在保持一个标识符意思明确的同时,应当尽量缩短其长度。

避免过于相似

不要出现仅靠大小写区分的相似的标识符,例如“i”“I”“function”“Function”等等。

避免在不同级别的作用域中重名

程序中不要出现名字完全相同的局部变量和全局变量,尽管两者的作用域不同而不会发生语法错误,但容易使人误解。

正确命名具有互斥意义的标识符

用正确的反义词组命名具有互斥意义的标识符,如:"nMinValue" "nMaxValue""GetName()" "SetName()" ....

避免名字中出现数字编号

尽量避免名字中出现数字编号,如Value1,Value2等,除非逻辑上的确需要编号。这是为了防止程序员偷懒,不肯为命名动脑筋而导致产生无意义的名字(因为用数字编号最省事)。

函数

函数的命名

函数的名称由一个或多个单词组成。为便于界定,每个单词的首字母要大写。

推荐的组成形式

函数名应当使用"动词"或者"动词+名词"(动宾词组)的形式。例如:"GetName()", "SetValue()", "Erase()", "Reserve()" ....

保护成员函数

保护成员函数的开头应当加上一个下划线“_”以示区别,例如:"_SetState()" ....

私有成员函数

类似地,私有成员函数的开头应当加上两个下划线“__”,例如:"__DestroyImp()" ....

私有成员函数的层次结构表示

通常来说,在一个类中,公有方法、保护方法和私有方法所完成的任务总是呈现一种逐级依次细化的层次结构(意即:保护方法所实现的功能通常比该类中的公有方法更为细小琐碎;类似地,私有方法的功能也比其保护方法更具原子性)。

因此,对于遵循以上规则,并且功能较为复杂的类,在按照公有、保护、私有的三级形式划分以后,如果其私有成员中仍然存在明显不同的功能粒度,则可以通过追加更多下划线前缀的形式予以表示。

例如:由三个下划线开头的私有方法“___PushCdr”就要比同一类中,仅由两个下划线开头的私有方法“__MergeConCall”所完成的功能粒度更细小、更琐碎;而四个下划线开头的“____CalcCompensate”则比“___PushCdr”完成的功能 更具原子性。

如果发现类中的功能层数太多(从公有方法到最原子的私有方法间,一般不应该超过 7 层),那通常反应一个不良的设计。此时请检查这个类的功能是否过于臃肿,已使接口显得不太清晰。另外一个常见的问题是将无需访问该类中 私有或保护成员的功能定义成了方法。第一个问题可以通过重新划分类层次结构或将一个类分裂为多个类等方法解决。对于第二个问题,由于这些方法无需访问 受限成员,大多数时候都可以把它们转变成局部函数(放在无名空间或使用“static”前缀定义)。

成员函数的下划线后缀命名

对一些本应该作为保护或私有成员的函数,由于设计方面的其它考虑(例如:灵活性、功能等方面)将其提升为公有成员的,应该在其后面添加与其原本访问控制级别相应的下划线后缀。

另外,对于其它不推荐直接使用的成员函数(例如:会引起兼容性或可移植性方面问题的函数),也应当在其后面加相应下划线提示。

例如:"ioctl_()", "SetSysOpt_()", "GetSysOpt_()", "PreParser__()" ....

回调和事件处理函数

回调和事件处理函数习惯以单词“On”开头。例如:"_OnTimer()", "OnExit()" ....

虚函数

回调函数以外的虚函数习惯以“Do”开头,如:"DoRefresh()", "_DoEncryption()" ....

变量

变量应该是程序中使用最多的标识符了,变量的命名规范可能是一套C++命名准则中最重要的部分:

变量的命名

n  变量名由作用域前缀+类型前缀+一个或多个单词组成。为便于界定,每个单词的首字母要大写。

n  对于某些用途简单明了的局部变量,也可以使用简化的方式,如:i, j, k, x, y, z ....

n  使用有意义的变量名。比如字符串拷贝函数:void StringCopy(char *str1, char *str2);我们很难搞清楚究竟是把str1拷贝到str2中,还是刚好倒过来。可以把参数名字起得更有意义,如叫strSourcetrDestination。这样从名字上就可以看出应该把strSource拷贝到strDestination

n  在保证名字有意义的前提下,使名字长度更加精炼。

作用域前缀

作用域前缀标明一个变量的可见范围。作用域可以有如下几种:

前缀

说明

局部变量

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

pm

指向成员的指针(pointer of member

g

数组(grid

fo

函数对象(Function Object

i

类的实例(instance

对于经常用到的类,也可以定义一些专门的前缀,如:std::stringstd::wstring类的前缀可以定义为"st"std::vector类的前缀可以定义为"v"等等。

类型前缀可以组合使用,例如"gc"表示字符数组,"ppn"表示指向整型的指针的指针等等。

数值前缀的特别记法

“n”作为所有整形前缀是由于大多数情况下,编写程序时不需要过多考虑整形的宽度,但在某些场合中,整形宽度是需要特别注意并且仔细加以区分的,这时可使用如下记法代替“n”前缀:

前缀

说明

b

字节(8bitbyte

w

字(16bitword

dw

双字(32bitdouble word

qw -- nn

四字(64bitquad word

bf

位域(bit field

对浮点型变量也有类似记法如下:

前缀

说明

f

单精度浮点(32bitfloat

d

双精度浮点(64bitdouble

ld

扩展精度浮点(80bitlong double

推荐的组成形式

变量的名字应当使用"名词"或者"形容词+名词"。例如:"nCode", "m_nState""nMaxWidth" ....

常量

C++中引入了对常量的支持,常量的命名规则如下:

常量的命名

常量名由类型前缀+全大写字母组成,单词间通过下划线来界定,如:cDELIMITER, nMAX_BUFFER ....

类型前缀的定义与变量命名规则中的相同。

宏、枚举值的命名

宏和枚举值由全大写字母组成,单词间通过下划线来界定,如:ERROR_UNKNOWN, OP_STOP ....

3.      编程风格

基本风格

修饰符的位置

为便于理解,应当将修饰符 "*" "&" 紧靠数据类型。

例如:

char* name;

int* x;

int  y;    // 为避免y被误解为指针,这里必须分行写。

int* Function(void* p);

附注:int* a, b;//b的实际类型为int,而不是int*,故为避免犯错,应该分行写。

与常量的比较

在与宏、常量进行 "==", "!=", ">=", "<=" 等比较运算时,应当将常量写在运算符左边,而变量写在运算符右边。这样可以避免因为偶然写错把比较运算变成了赋值运算的问题。

例如:

if (NULL == p// 如果把 "==" 错打成 "=",编译器就会报错
{
    // ...
}

运算符的优先级

 如果代码行中的运算符比较多,应该用括号确定表达式的操作顺序,避免使用默认的优先级。因为熟记各运算符的优先级是比较困难的,就算你熟记并正确使用了,写出来的代码也容易产生歧义而使其可读性较差。

     好的风格 if ((a | b) && (a & c))

     坏的风格 if (a | b && a & c)

  虽然后者和前者功能一样,但后者是很恐怖的,难以阅读。

不要编写太复杂的复合表达式

复合表达式使用在适当的场合可以使代码更加简洁,但不能因为这个简洁而带来理解的复杂。

例如:

  max = a > b (a > c ? a : c) : (b > c ? b : c)  // 复合表达式过于复杂

应该修改为:

1

2

3

4

5

6

7

8

9

max = a;

if(max < b)

{

  max = b;

}

if(max < c)

{

  max = c;

}

上面的if的执行语句只有一行也加了{},是因为遵循了不论ifforwhile的执行语句有多少都要加{}”的规则,这样可以防止书写失误,当这样的语句层层嵌套的时候你就会知道这样做的好处

各种数据类型与零值比较

JAVA中,对于布尔变量flag,与零值(注意:不是0)比较的方式自然是if (flag == TRUE)或者if (flag == FALSE)但是在C/C++中这却不是正确的选择

n  正确的选择应该是if (flag)或者if (flag),这是因为TRUE的值究竟是什么并没有统一的标准,例如Visual C++ TRUE定义为1,而Visual Basic则将TRUE定义为-1if (flag == TRUE)if (flag == 1 )if(flag == FALSE)if (flag == 0)都属于不良风格。

n  应当将整型变量用“==”=”直接与0比较if (value == 0)

if (value != 0)

不可以写成

if (value)  // 会让人误解 value是布尔变量

if (!value)

n  指针变量的零值是NULL。尽管NULL的值与0相同,但是两者意义不同。对于指针变量p ,它与零值比较的if语句如下:

if (NULL == p)

if (NULL != p)

  不要写成

  if (0 == p)  // 容易让人误解p是整型变量

  if (0 != p)

避免嵌套过深

不要出现这样的结构:
   

1

2

3

4

5

6

7

if (condition1)

    {

     

      if (condition2)

         

           if (condition3)

             

而应该代之以if-else-if结构:

1

 2

 3

 4

 5

 6

 7

 8

 9

10

11

12

13

if (condition1)

 { 

   

 

 }

 else  if (condition2)

 {

  

 }

 else if (condition3)

 {

    

 }

这样的结构条理清楚,前者则容易导致写到后来自己都不知道写了些什么的事实。

乱指一气的指针

 “野指针者,乱指一气的指针也,它不是NULL指针,是指向垃圾内存的指针。野指针是很危险的,是经常导致bug的原因,它的成因主有两种:一是指针变量没有被初始化。在C/C++中任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。

   二是指针pfree或者delete之后,没有置为NULL,让人误以为p是个合法的指针。

避免使用宏:

用内联函数代替宏函数、用const声明常量代替宏常量

宏是CC++中最生硬的工具,要避免使用宏。C++之父Bjarne Stroustrup说过,C++的目标之一就是使C的预处理器成为多余的,因为这天生就容易出错。

C++中几乎不需要宏,可以const或者enum定义易于理解的常量,用inline避免函数调用的开销,用template指定函数系列和类型系列,用namespace避免名称冲突

关于宏的第一规则就是:不要使用它,除非不得不用。几乎每个宏都说明程序设计存在缺陷。

例外情况:

宏仍然是几个重要任务的唯一解决方案,比如#include保护符,条件编译中的#ifdef#defined,以及assert的实现。

总是初始化变量

一切从白纸开始,未初始化的变量时CC++中错误的常见来源。养成在使用内存之前先清除的习惯,可以避免这种错误,在定义变量的时候就将其初始化。

使用过程式的语言(如PascalCFortranCobol)的人,可能有这样的习惯:独立于使用它们的代码来定义变量,然后再要使用的时候赋值。这种方法已经过时了,是不可取的。

避免函数过长

短胜于长,平优于深:过长的函数和嵌套过深的代码块的出现,经常是因为没能赋予一个函数以紧凑的职责所致,这两种情况通常能够通过重构予以解决。

函数试图将多个这样的小概念单元合并到一个长的函数体中,那么它最终将不堪重负。建议如下:

l  尽量紧凑:对一个函数只赋予一种职责。

l  不要自我重复:优先使用命名函数,而不要让相似代码片段反复出现。

l  优先使用&&在可以使用&&条件判断的地方要避免使用连续嵌套的if

l  不要过分使用try优先使用析构函数进行自动清除而避免使用try代码块。

l  优先使用标准算法:算法比循环嵌套要少,通常也更好。

l  不要根据类型标签(type tag)来进行分支(switch,优先使用多态函数。

尽量减少定义性依赖。避免循环依赖

不要过分依赖:如果用前向声明(forward declaration)能够实现,那么不要包含(#include)定义。

不要互相依赖:循环依赖是指两个模块之间或间接地互相依赖。所谓模块是指紧凑的发布单元(如一个.cpp和同名.h就是一个单元),互相依赖的多个模块并不是真正的独立模块,而是紧凑的更大模块,因此循环依赖有碍于模块性,是大型项目的祸根。请避免循环依赖。

一般而言,应该在模块层次上考虑依赖性及其循环。模块是一同发布的类和函数的紧凑几何。最简单形式的循环依赖是两个互相依赖的类:

1

2

3

4

5

6

7

8

class Child;            //打破循环依赖

class Parent{//...

  Child* myChild_;

};

 

class Child{//...

  Parent* myParent_;    //有可能位于不用头文件中

};

这里的ParentChild存在互相依赖。代码能够编译,但是有一个根本性的问题:两个类不在是独立的,而是互相依赖的了。这种情况不是很糟糕,但前提是两者作为一个模块的部分同时发布。

如果依赖循环跨越多个模块的话,情况会变得很糟糕,因为必须联合这些模块组成更大的单元再进行发布。

为了打破循环,可以应用面向对象中的优秀思想原则:依赖倒置原则:不要让高层模块依赖于底层模块;相反,应该让两者都依赖于抽象。如果能够为ParentChild定于独立的抽象类,那么就能打破循环了。多数设计模式都是本着依赖倒置原则的设计理念进行面向对象设计的。

尽量使用初始化而不是在构造函数里赋值

对象创建分为两步:

1.    数据成员初始化;

2.    执行被调用构造函数体内的动作。

对于有基类的对象来说,基类的成员初始化和构造函数体内的执行发生在派生类的成员初始化和构造函数的执行之前)。这就经常意味着对象的成员在执行到构造函数体内之前已经被构造了,然后再构造函数体内又进行了一次赋值操作。这样每个对象成员都进行了调用:一次是缺省构造函数,另一次是赋值。当对象成员较多或其成员本身比较“大”时,导致了严重效率浪费。

例外情况:

1.   当有大量的固定类型的数据成员要在每个构造函数里以相同的方式初始化的时候,对类的数据成员赋值比用初始化更合理。

1

2

3

4

ManyDataMbrs::ManyDatambrs()

:a(1),b(1),c(1),d(1),e(1),f(1),g(1),h(1),i(1),

 j(0),k(0),l(0),m(0)

{...}

应该替换为:

1

 2

 3

 4

 5

 6

 7

 8

 9

10

11

ManyDataMbrs::ManyDatambrs()

{

  init();

}

//...

//private:

void ManyDataMbrs::init()

{

  a = b = c = d = e = f = g = h = 1;

  i = j = k = l = m = 0;

}

2.   MFC中大多数MFC类的成员变量,只能在函数体内进行赋值。

/结构风格

类是C++中最重要也是使用频率最高的新特性之一,类的版式好坏将极大地影响代码品质。

注释头与类声明

与文件一样,每个类应当有一个注释头用来说明该类的各个方面。

类声明换行紧跟在注释头后面,"class" 关键字由行首开始书写,后跟类名称。界定符 "{" "};" 应独占一行,并与 "class" 关键字左 对齐。

/*! @class
********************************************************************************
<PRE>
类名称   : CXXX
功能     : <简要说明该类所完成的功能>
异常类   : <属于该类的异常类(如果有的话)>
--------------------------------------------------------------------------------
备注     : <使用该类时需要注意的问题(如果有的话)>
典型用法 : <如果该类的使用方法较复杂或特殊,给出典型的代码例子>
--------------------------------------------------------------------------------
作者     : <xxx>, [yyy], [zzz] ...(作者和逗号分割的修改者列表)
</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);
};

 

正确地使用constmutable

把不改变对象逻辑状态的成员都标记为const成员不仅有利于用户对成员的理解,更可以最大化对象使用方式的灵活性及合理性(比如通过const指针或const引用的形式传递一个对象)。

如果某个属性的改变并不影响该对象逻辑上的状态,而且这个属性需要在const方法中被改变,则该属性应该标记为 "mutable"

例如:

class CString
{
public:
   
//! 查找一个子串,find()不会改变字符串的值所以为const函数
   
int find(IN const CString& str) const;
    // ...

private:
   
// 最后一次错误值,改动这个值不会影响对象的逻辑状态,
    //
find()这样的const函数也可能修改这个值
    mutable int m_nLastError;
    // ...
};

也就是说,应当尽量使所有逻辑上只读的操作成为const方法,然后使用mutable解决那些存在逻辑冲突的属性。

嵌套的类声明

在相应的逻辑关系确实存在时,类声明可以嵌套。嵌套类可以使用简单的单行注释头:

 

// ...
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) // 先基类,后成员,
                                             //
分别按照声明顺序书写
{
   
// ...
};

函数

函数原型

函数原型的格式为:

[存储类] 返回值类型
[
名空间或类::]函数名(参数列表) [const说明符] [异常过滤器]

例如:

static inline void
Function1(void)

int
CSem
::Function2(IN const char* pcName) const throw(Exp)

其中:

  • "[ ]" 括住的为可选项目。
  • 除了构造/析构函数及类型转换操作符外,"返回值类型" "参数列表" 项不可省略(可以为 "void")。
  • "const说明符" 仅用于成员函数中 。
  • "存储类", "参数列表" "异常过滤器" 的说明见下文 。

函数声明

函数声明的格式为:

 

//! 函数功能简单说明(可选)
函数原型;

例如:

//! 执行某某操作
static
void
Function(void);

函数声明和其它代码间要有空行分割。

声明类的成员函数时,为了紧凑,返回值类型和函数名之间不用换行,也可以适当减少声明间的空行。

 

函数定义

函数定义使用如下格式:
 

/*! @function
********************************************************************************
<PRE>
函数名   : <函数名>
功能     : <函数实现功能>
参数     : <参数类表及说明(如果有的话),格式为:>
           [IN|OUT]
参数1 : 参数说明
           [IN|OUT]
参数2 : 参数说明
           ...
返回值   : <函数返回值的意义(如果有的话)>
抛出异常 : <可能抛出的异常及其说明(如果有的话),格式为:>
          
类型1 : 说明
          
类型2 : 说明
           ...
--------------------------------------------------------------------------------
复杂度   : <描述函数的复杂度/开销(可选)>
备注     : <其它注意事项(如果有的话)>
典型用法 : <如果该函数的使用方法较复杂或特殊,给出典型的代码例子>
--------------------------------------------------------------------------------
作者     : <xxx>, [yyy], [zzz] ...(作者和逗号分割的修改者列表)
</PRE>
*******************************************************************************/
函数原型
{
    // ...
}

对于返回值、参数意义都很明确简单函数(代码不超过20行),也可以使用单行函数头:

//! 函数实现功能
函数原型
{
    // ...
}

函数定义和其它代码之间至少分开2行空行。

 

参数描述宏

以下预定义宏对程序的编译没有任何影响,只为了增强对参数的理解:

 

说明

IN

输入参数

OUT

输出参数

DUMMY

哑元参数-不使用参数的值,仅为帮助函数重载解析等目的而设置的参数

OPTIONAL

可选参数-通常指可以为NULL的指针参数,带默认值的参数不需要这样标明

RESERVED

保留参数-这个参数当前未被支持,留待以后扩展;或者该参数为内部使用,用户无需关心

OWNER

获得参数的所有权,调用者不再负责销毁实参指定的对象;如果用来修饰返回值,则表示调用者获得返回值的所有权,并负责将其销毁

UNUSED

标明这个参数在此版本中已不再使用

CHANGED

参数类型或用途与较早版本相比发生了变化

ADDED

新增的参数

NOTE

需要注意的参数-参数意义发生变化或者与习惯用法不同

WRKBUF

工作缓冲区-为避免频繁分配临时资源而传入的临时工作区

其中:

  • 除了空参数 "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* piDB, OPTIONAL IN OUT int* pnRecordCount = 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);

如果一个函数本身及其直接调用的函数不会显式抛出异常(没有指定异常过滤器),那么该函数可以省略异过滤器。

特别地:如果一个函数内部显式地捕获了任何可能的异常(例如:使用了 "catch (...)" ),并且保证不抛出任何异常,那么应该在其声明和定义中显式地指定一个空异常过滤器:"throw()"

例如:

int
Function(IN const char* pcName) throw();

特别地:进程入口函数("main()"应当使用异常过滤器。

代码段注释

如果函数体中的代码较长,应该根据功能不同将其分段。代码段间以空行分离,并且每段代码都以代码段分割注释作为开始。

例如:

void
CXXX::Function(IN void* pmodAddr)
{
   
if (NULL == pmodAddr)
    {
       
return;
    }

    {
CSessionLock iLock(mxLock);

       
// =====================================================================
        // = 判断指定模块是不是刚刚被装入,由于在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 = sm_iProcTbl.begin();
       
for (; p!=sm_iProcTbl.end(); ++p)
        {
           
p->HookOneModule(pmodAddr);
        }
    }
// SessionLock
}

调用系统API

所有系统API调用前都要加上全局名称解析符 "::"

例如:

::MessageBoxA(NULL, gcErrorMsg, "!FATAL ERROR!", MB_ICONSTOP|MB_OK);

if (0 == ::GetTempFileName(m_basedir.c_str(), byT("bai"), 0, stR.ref()))
{
    // ...
}

 

让相同的代码只出现一次

为了使程序更容易调试、修改,尽量降低日后维护的复杂性,应该把需要在一个以上位置使用的代码段封装成函数。哪怕这段代码很短,为了以后维护方便着想,也应当将其封装为内联函数。

函数版式实例

/*! @function

********************************************************************************

<PRE>

函数名   : GetAllSubDirs

功能     : 获得所有符合条件的子目录列表

参数     : [OUT] vResult       : 用来储存结果

           [IN]  dirPattern    : 要查找的子目录通配符

           [IN]  includeSubdir : 是否包含子目录

           [IN]  includeHidden : 是否包含隐含目录

           [IN]  includeDot    : 是否包含“.”“..”系统目录

返回值   : 符合条件的子目录数量

抛出异常 : -

--------------------------------------------------------------------------------

备注     : 无论是否成功,vResult中的原内容都将被清空

典型用法 : -

--------------------------------------------------------------------------------

作者     :

</PRE>

*******************************************************************************/

ULONG

CDir::GetAllSubDirs(OUT VSTR&     vResult,

                    IN  tstringEx dirPattern    /*= byT("*")*/,

                    IN  bool      includeSubdir /*= false*/,

                    IN  bool      includeHidden /*= true*/,

                    IN  bool      includeDot    /*= false*/) const

{

    // =========================================================================

    // = 初始化

    vResult.clear();

    if (dirPattern.empty() || !IsValid())

    {

        return 0;

    }

 

    WIN32_FIND_DATA FindFileData;

    HANDLE hFind;

    tstringEx Pattern = m_basedir + DELIMITER + dirPattern;

 

    // =========================================================================

    // = 匹配子目录

    hFind = ::FindFirstFile(Pattern.c_str(), &FindFileData);

    if(INVALID_HANDLE_VALUE == hFind)
    {

        return 0;

    }

   
int n;

    tstringEx stDir;

    if (-1 != (n=dirPattern.find_last_of(ALLDELIMITERS)))

    {

        stDir = m_basedir + DELIMITER + dirPattern.substr(0, n) + DELIMITER;

    }

    else

    {

        stDir = m_basedir + DELIMITER;

    }

 

    do

    {

        if (!(FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))

        {

            goto findnext;

        }

 

        if (IsDotDir(FindFileData.cFileName) && !includeDot)

        {

            goto findnext;

        }

 

        if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)

            && !includeHidden)

        {

            goto findnext;

        }

 

        // 递归搜索子目录

        if (includeSubdir && !IsDotDir(FindFileData.cFileName))

        {

            VSTR rr;

            tstringEx rdir;

            if (-1 != n)

            {

                rdir = dirPattern.substr(0, n)+DELIMITER+FindFileData.cFileName

                       +DELIMITER+dirPattern.substr(n+1, -1);

            }

            else

            {

                rdir = FindFileData.cFileName+DELIMITER+dirPattern;

            }

 

            GetAllSubDirs(rr, rdir, includeSubdir, includeHidden, includeDot);

            const ULONG count = rr.size();

            for (ULONG i=0; i<count; ++i)

            {

                vResult.push_back(rr[i]);

            }

        }  // if (includeSubdir && !IsDotDir(FindFileData.cFileName))

 

        vResult.push_back(stDir + FindFileData.cFileName);

 

findnext:

        if (!::FindNextFile(hFind, &FindFileData))

        {

            break;

        }

    } while(true);

 

    ::FindClose(hFind);

    return vResult.size();

}

 

声明格式

变量、常量的声明格式如下:  

[存储类] 类型 变量名;

其中:

  • "[ ]" 括住的为可选项目。
  • "存储类" 的说明见下文

定义格式

变量、常量的定义格式如下:  

[存储类] 类型 变量名 = 初始值;

其中:

  • "[ ]" 括住的为可选项目。
  • "存储类" 的说明见下文

存储类

"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" 等等。该操作通常在编译时完成,但有可能调用用户定义的类型转换操作或非 explicit 的单参(或至少从第二个参数开始带缺省值的)构造函数 。

reinterpret_cast

reinterpret_cast 用于编译器不认可的,不安全的静态转换,比如将 "int*" 转为 "int" 等等。这种转换有可能产生可移植性方面的问题,该操作在编译时完成 。(注意:reinterpret_cast C 风格的类型转换还要野蛮,它不进行任何地址对齐和调整,也不调用任何用户定义的类型转换操作)

const_cast

const_cast 用于将一个常量转化为相应类型的变量,比如将 "const char*" 转换成 "char*" 等等。这种转换可能伴随潜在的错误。该操作在编译时完成 。

dynamic_cast

dynamic_cast C++ RTTI 机制的重要体现,用于在类层次结构中漫游。dynamic_cast 可以对指针和引用进行自由度很高的向上、向下和交叉转换。被正确使用的 dynamic_cast 操作将在运行时完成 。反之,若编辑器关闭了 RTTI 支持,或被转换的类层次结构中没有抽象类存在,则此操作在编译时完成(有些编译器会给出警告)。

此外,对于定义了单参构造函数或类型转换操作的类来说,应当优先使用构造函数风格的类型转换,如:'string("test")' 等等。

通常来说,"xxx_cast" 格式的转换与构造函数风格的类型转换之间,最大的区别在于:构造函数风格的转换经常会生成新的临时对象,可能伴随相当的时间和空间开销。而 "xxx_cast" 格式的转换只是告诉编译器,将指定内存中的数据当作另一种类型的数据看待,这些操作一般在编译时完成,不会对程序的运行产生额外开销。当然,"dynamic_cast" 和某些 "static_cast" 则例外。

4.      版本控制

n  源代码的版本按文件的粒度进行维护。

n  创建一个新文件时,其初始版本为 "1.0",创建过程中的任何修改都不需要增加修改记录。

n  从软件第一次正式发布开始,对其源文件的每次修改都应该在文件头中加入相应的修改记录,并将文件的子版本加1

n  升级软件的主版本时,其源文件的相应主版本号随之增加。与创建新文件时一样,在该主版本第一 次发布之前,对文件的任何修改都不需要再增加修改记录。

修改标记

在代码交叉审查,或使用带完整源代码的第三方库时,经常需要为某些目的修改源码。这时应当为被改动的部分添加修改标记。

何时使用修改标记

修改标记通常仅用于修改者不是被修改模块(或项目)的主要作者时,但也可以用于在调试、重构或添加新特性时进行临时标注。

在交叉审查中使用的修改标记,当原作者已经确认并将其合入主要版本之后,应当予以消除,以避免由于多次交叉审查累积的标记混乱。但是相应的修改应当记入文件头的修改记录中。

修改标记的格式

修改标记分为单行标记和段落标记两种,单行标记用于指示对零星的单行代码进行的修改,段落标记则用于指出对一组任意长度的代码作出的修改。它们的格式如下:

// 单行标记:
// code ...; // by <
修改者> - <目的> [@ YYYY-MM-DD(可选的修改日期)]

//
段落标记:
// [[ by <
修改者> - <目的> [@ YYYY-MM-DD(可选的修改日期)]
//   
详细说明(可选,可多行)
// ... //
被修改的代码段落
// ]] [by <
修改者>]

注意段落标记结尾的 "by <修改者>" 字段是可选的。

此外,在比较混乱或较长的代码段中,可以将段落开始("// [[")和段落结束("// ]]")标记扩展层次结构更为明显的:"// ---- [[" "// ---- ]]"

例如:

    // [[ by BaiYang - limit @ 2005-03-29
    //    add pre compile and delay binding support to "limit [s,]n".
    void
setStatementLimit(dbQuery const& q) {
       
// ...
    }
    // ]]

// ...

// ---- [[ by Mark - multithread
void dbCompiler::compileLimitPart(dbQuery& query)
{

    // ...
   
int4* lp1 = INVPTR; // by BaiYang - limit
   
switch (scan())
    {
      case tkn_iconst:
    // ...
}
// ---- ]] by Mark

修改标记的语言

修改标记当中的说明性文字应当尽量选择与被修改项目一致的语言书写。例如在全英文的项目中应当尽量避免添加中文注释。

否则能完全看懂修改后项目的程序员将会被限制于同时掌握多种自然语言的人。

5.      常用英文注释

/*! @file
********************************************************************************
<PRE>
Module              :
File                :
Related Files       :
Intro               :
Author              : <xxx>
Version             : 1.0
--------------------------------------------------------------------------------
MutiThread Safety   :
On Exception Safety :
--------------------------------------------------------------------------------
Notes               :
--------------------------------------------------------------------------------
Change History      :
Date         Version  Changed By      Changes
YYYY/MM/DD   1.0      <xxx>           Create
</PRE>
********************************************************************************

* Copyright(c) YYYY, by <xxx>, All rights reserved

*******************************************************************************/

以下定义的各种成员类型可以根据实际需要增删。

/*! @class
********************************************************************************
<PRE>
Class    :
Desc     :
Exception:
--------------------------------------------------------------------------------
Notes    :
Examples :
--------------------------------------------------------------------------------
Author   : <xxx>
</PRE>
*******************************************************************************/
class CXXX
{
public:
defines

public:
// big three

public:
// virtual funcs

public:
/// public funcs

public:
/// static funcs

protected:
/ internal funcs

private:
private defines

private:
// private funcs

private:
/ properties

private:
// static properties

private:
disabled method

};

/*! @function
********************************************************************************
<PRE>
Function  :
Desc      :
Params    :
Return    :
Exception :
--------------------------------------------------------------------------------
Complexity:
Notes     :
Examples  :
--------------------------------------------------------------------------------
Author    : <xxx>
</PRE>
*******************************************************************************/

 语句/函数组

// [[ Group Title

    ...

// ]] [Group Title]

OR

// ---- [[ Group Title

    ...

// ---- ]] [Group Title]

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
以下是gec6818网络视频点播器的12条中文参考文献: 1. 张勇, 陈振洲. 基于Android的网络视频播放器设计与实现[J]. 多媒体技术, 2015, 10(2): 192-198. 2. 魏巍, 李勇, 李静. 基于STM32的网络视频播放器设计与实现[J]. 计算机应用, 2016, 36(2): 553-558. 3. 王敏, 李军. 基于Raspberry Pi的网络视频播放器研究与实现[J]. 电子与信息学报, 2017, 39(3): 609-614. 4. 陈媛媛, 张宏. 基于FPGA的网络视频播放器设计与实现[J]. 计算机应用, 2018, 38(1): 157-162. 5. 李晓, 李勇. 基于ARM Cortex-M3的网络视频播放器设计与实现[J]. 电子测量与仪器学报, 2019, 33(6): 777-782. 6. 刘欣, 徐浩. 基于BeagleBone Black的网络视频播放器设计与实现[J]. 计算机应用, 2020, 40(3): 812-817. 7. 孙媛, 刘杰. 基于Raspberry Pi 4的网络视频播放器设计与实现[J]. 信息技术与应用, 2021, 11(1): 1-6. 8. 王艳, 张勇. 基于STM32H7的网络视频播放器设计与实现[J]. 电子科技, 2021, 19(2): 148-154. 9. 吴泽, 李军. 基于NVIDIA Jetson Nano的网络视频播放器研究与实现[J]. 计算机应用, 2021, 41(2): 542-547. 10. 张旭, 李勇. 基于HiSilicon Hi3516DV300的网络视频播放器设计与实现[J]. 电子测量与仪器学报, 2021, 35(3): 179-184. 11. 张磊, 王海波. 基于Allwinner H6的网络视频播放器设计与实现[J]. 电子与信息学报, 2021, 43(2): 251-256. 12. 周婧, 李晓. 基于ESP32的网络视频播放器设计与实现[J]. 电子科技, 2021, 19(3): 255-260.

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值