老万聊编程—编码规范


老万聊编程—C/C++编码规范
在阅读别人的代码时,你是否遇到这样的情况;看到一个字符串不知道它是局部变量,或是全局变量以及该字符串表示的含义等,你必须反复的查看该字符串表示的含义,这样几次之后你的感觉如何?现在思考下微软的软件为什么被广泛的使用?用过Windows API编程都知道,它的名字非常好记,它的程序可读性非常强,编写软件不仅要实现功能,还要追求软件的艺术,给人一种美的享受。对初学编程的人来说,常常用int a,b等对变量的命名;对函数的命名采用的是f(),g()等。用这种方法写出的程序有什么优点吗?在个人英雄主义时代便于程序的保密,在当今合作的时代,这样的程序犹如鸡肋,对这样的程序我的处理方法为:要勇敢的抛弃,免受折磨。怎样使一个项目组中的任何成员看懂别人的程序及修改别人的程序。我通过几年的工作经验与教训及借鉴他人的经验总结出了编码规范共大家参考。
编码规范是由实际经验得到的常识,它不是随意的规则或处方。代码应该是清楚的、简单的、具有直截了当的逻辑、自然的表达公式、通行的语言使用方式、有意义的名字和有帮助作用的注释等。同时编码规范对程序的兼容性,排错性以及性能的提高等都有很大的影响。好的开始是成功的一半,好的编码规范是成功的关键。闲话少说,现在我从以下几个方面来说明编码规范。

1 名字的命名原则:尽量用少的字符精确的表达出其内容

一个变量、函数以及文件的名字标识了这个对象,同时也携带着其用途的一些信息。因此对变量、函数、文件等命名要遵守一定的规则。

1.1要用英文命名。

在软件开发中英语是一种通用的语言,尽量避免使用汉语拼音。汉语有几千年的历史博大精深,存在着大量的同音字或同音词,据统计读YI的有一百多个,所以仅靠汉语拼音是很难识别出具体的含义。

1.2名字的长短与其作用域成正比,作用域越大名字越长。

例如全局变量的名字要比局部变量的名字长,全局函数要比成员函数的名字长等。

1.3尽量选用通用的词汇,并贯穿始终。

例如Get,Obtain都有“得到”的意思,但Get比Obtain更常用些。不要选用一些比较长、难记的单词,在命名时牢记这里是编程,方便与别人交流,不是英语的专业考试。

1.4最好使用能够拼读的词汇,便于交流。

1.5不要使用不标准的缩写或自定义的缩写。

在一个项目组中可以规定一些在项目开发中常用的缩写,但不要太多,对标准的缩写不要重新定义。

1.6名字尽量能表达出其表示的内容。

例如判断一个char c是否为字符,IsChar©比CheckChar( c )好。

1.7避免使用容易混淆的字母或数字。

例如大写字母O与数字0,小写字母l与数字1等。

1.8书写规则为每个单词的首字母用大写,其余的字母小写。

例如GetTime。有的采用下画线“_”(例如,get_time),我认为这种格式书写不方便。

2 命名规则

   在C/C++编程中,全局变量可以和局部变量重名,函数名可以和局部变量重名,标号也可以与局部变量重名等一些重名的现象。重名对阅读程序,调试带来了巨大的困难,我们在编程要尽量避免重名现象。对于航天型号的C语言程序严格规定了以下重名现象。

1)严禁过程名(函数名)被用于其他处,例如函数名与局部变量重名。
2)严禁标号名被用于其他处。
3)严禁全局变量与局部变量重名。
4)严禁对C/C++关键字的重新定义。
为了避免重名现象、提高程序的可靠性、性能、书写的规范性等,现在对各种命名做如下规定。

3 变量与标号的命名规则

对于变量的名字能够表示出其作用域、类型及含义,在C/C++语言中任何一个变量都有其作用域、变量类型及变量所表示的含义等。下面给出变量的命名规则:

3.1变量的作用域:

根据变量的作用域可把变量分为:局部变量、静态变量、全局变量、类的成员变量、常量型变量,寄存器变量等。对于不同类型的变量要加以区别。区别如下:
1)局部变量:变量类型(小写)+名字
2)静态变量:加s_ s表示static
3)全局变量:加g_ g表示global
4)类的成员变量:加m_ m表示member
5)常量型变量:加c_ c表示constant
6)标号命名:加L_ L表示label;注意L用大写避免小写字母l与数字1混淆。
7)寄存器变量不建议使用。

3.2变量的类型:

1)字符串类型:加前缀 str(string)
2)宽字符串类型:加前缀wstr (wide string)
3)字节类型:加前缀c(char)
4)无符号字节类型:加前缀uc(unsingned char)
5)指针类型: 加前缀p(point)
6)布尔类型:加前缀 b(bool)
7)短整型:加前缀s(short)
8)无符号短整型:加前缀us(unsigned short)
9)对于int型:加前缀 n(integer)
10)对于无符号的int型:加前缀 un(unsigned integer)
11)对于long型:加加前缀l(long)
12) 对于无符号的long型:加前缀 ul(unsigned long)
13)对于浮点类型:加前缀f(float)
14)对于双精度浮点类型:加前缀d(double)
15)对于类对象:加前缀cls(class)
16)对于结构变量:加前缀st(struct)
17)句柄类型:加前缀h(handle)
18)对于文件流变量: 加前缀pf(point file)等。
3.3变量的命名规则:
变量的作用域——变量类型——变量名字。
例如:int g_nStatSum=0表示统计总和的变量,该变量的类型为int型,作用域为全局变量。
const float c_fPI=3.1425926表示PI值,该值为常量类型。
FILE *pfSource含义为指向源文件流,该文件流为局部变量。

4、宏命名与使用:尽量避免使用宏

4.1常数宏的命名:加前缀M_,后面名字全部大写

在C++可以用常量定义替代,因为常量定义可以执行类型检查具有更大的安全性。在调试中可以读取常量的值,一般不能直接读取宏定义的值。
例如:#define M_SCREENX 1024
在C++中定义为: const int c_iScreenX 1024;

4.2在写宏时最后语句不要加分号

因为宏执行的是替代操作,若加分号可能会引起程序的混乱。
例如:#define M_MAXNUM 100;
for(int n=0;n<M_MAXNUM;n++)
会出现什么情况,大家可以实验。

4.3函数宏的命名:加前缀MF_,后面名字全部大写

例如:#define MF_SQUARE(X)
对函数宏的使用要注意以下事项:
1)尽量避免使用函数宏。在C++里可以用内联函数来替代。即使在C语言里,他带来的麻烦比解决问题更多。函数宏最常见的一个严重问题是:如果一个参数在定义中出现多次,它就可能被多次求值。如果调用时的实际参数带有副作用,结果将会产生一个难以琢磨的错误。例如:
#define MF_ISUPPER© (©>=’A’&&(©<=’Z’)
如果这样调用:while(MF_ISUPPER(c=getchar())会出现??
改正:while((c=getchar())!EOF&& MF_ISUPPER©)
2)如果使用宏,要给宏的体和参数都加上括号。
例如:#define MF_SQUARE(X) (X)(X)
对于下面的表达式: 1/ MF_SQUARE(X),出现什么结果?
改正:#define MF_SQUARE(X) ((X)
(X))
例如:#define MF_SQUARE(X) (XX)
对于下面的表达式: MF_SQUARE(X+1),出现什么结果?
改正:#define MF_SQUARE(X) ((X)
(X))
3)一定要用{ }将具有多行函数宏的函数体括起来。

4.4彻底用typedef代替宏定义新类型

例如:#deine MT_PINT int*
typedef PINT int*;

4.5禁止在同一个宏中使用多个#或##

在同一个宏中使用多于一个的#或##,或同时使用#和##都是很危险的

4.6禁止重新定义保留字

宏预处理可以重新定义保留字,但这种做法会引起混淆,因此禁止重新定义保留字。
例如:#define FILE unsigned int(不允许)

4.7避免在一个程序块中单独使用#define和#undef

例如:这是禁止的:
int ForbidUse(void)
{
#define M_EXAMPLE 100
}

4. 8谨慎使用#pragma

#pragma 可以用来掩盖所有类型的问题,因此应谨慎使用#pragma。

5 对于常数的命名与使用

5.1用enum取代(一组相关的)常量,具有明确的意义。

例如:typedef enum
{
DIB_1BPP, // 2色图像
DIB_2BPP, // 4色图像
DIB_4BPP, // 16色图像
DIB_4BPPRLE, // 16色图像, RLE 压缩格式
……………………
}DIBFormat;
对于enum保证全部赋值,方法为:
1)全部不赋值
2)第1个赋值,其余全部不赋值。
3)全部赋值

5.2在C++中把数定义为常数,不要定义为宏。

在VC编译器下,定义为宏的常数在调试状态下无法获取该值且不执行类型检查。
例如:C++里定义常数为const int c_nScreenX=1024。

5.3使用字符形式的常量,不要使用整数。

人们常用到<ctype.h>里的函数。
例如:if(cChar>=65&&cChar<=90)等价于if(cChar>=’A’&&cChar<=’Z’)
对字符串赋0要用’/0’不要用0
例如strName[0]=0等价于strName[0]=’/0’

5.4利用语言去计算对象的大小。

不要对任何数据类型使用显示写出来大小,例如用2代替sizeof(short),基于同样的原因,写sizeof(aArray[0])可能比sizeof(int)更好,因为即使数组的类型改变了,也不用改变程序。原则是:能清楚地表明对象的类型,即使当变量改动时程序改动最小。

6、指针的命名与使用

6.1 指针的变量的前面加小写的字母p

例如 short *psData; //p表示指针类型,s表示short类型,所以为表示指向short类型的指针。

6.2用new、delete取代malloc、calloc、realloc和free

因为malloc、calloc、realloc和free是C语言的用法,它们不会理会到对象是否存在,更不会去调用构造和析构函数,所以在C++中经常引起麻烦。

6.3 new、delete和new[ ]、delete[ ]要成对使用

▲调用new所包含的动作:
1)从系统堆中申请恰当的一块内存
2)若是对象,调用相应类的构造函数,并以刚申请的内存地址作为this参数。
▲ 调用new[ ] 所包含的动作:
1)从系统堆中申请可容纳n个对象外加一个整型的一块内存。
2)将n记录在额外的那个整型内存中(其位置依赖于不同的实现,有的在申请内存块的开头,有的末尾)。
3)调用n次构造函数初始化这块内存中的n个连续对象。
▲调用delete所包含的动作:
1)若是对象,调用相应类的析构函数(在delete参数所指的内存处)
2)将内存返还系统堆。
▲调用delete[ ] 所包含的动作:
1)从new[ ]记录n的地方将n值找出。
2)调用n次析构函数析构这块内存中n个连续的对象。
3)将这一整块内存(包括记录n的整数)还系统堆。

6.4指针应该有一个合理的值

在指针使用前必须有一个合理值,释放后必须把指针置为无效。
例如:
int* piData=NULL;
piData=new int[1024];
………………………
………………………
//删除操作
if(piData!=NULL)
{
delete[] piData;
piDada=NULL;
}
达到两种目的:
1)防止其后再次使用该指针。
2)防止其后再次删除该指针。

6.5牢记给字符串结束符申请空间

例如:char *pstrNamer=new char[strlen+1];

6.6 指针的运算规则

1)禁止将参数指针赋值给函数指针。
2)尽量避免使用三重指针。
对指针进行控制是很困难的,当指针的指针超过两级时,使用起来更是具有风险,因此禁止指针的指针超过两级。
3)尽量避免使用函数指针。
使用过程指针是具有较大风险的,因此禁止将过程声明为指针类型。
4)谨慎使用指针的逻辑比较
使用大于和小于的操作符对指针进行比较是具有较大风险的,应谨慎使用指针的逻辑比较。
5) 谨慎对指针进行代数运算
对指针进行代数运算是具有较大风险的,应谨慎对指针进行代数运算。

6.7遵守谁申请谁释放的原则。

7表达式的书写规则

7.1谨慎使用三重表达式

例如 nMax=nData1>nData2? nData1: nData2;

7.2禁止对有符号类型进行移位运算

对有符号类型进行移位运算会导致不可预料的后果,对变量进行移位运算必须保证不会产生溢出,一些编译器不检查移位运算是否超出机器字长。

7.3禁止给无符号变量赋负值

给无符号变量赋负值会导致不可预料的结果,因此禁止给无符号变量赋负值。

7.4使用表达式的自然形式。

例如:含有否定运算的条件表达式比较难理解:
if(!(a>b) || !(c>d))
应写为:
if(a<=b) || (c<=d))更好理解。
因为人类的思维习惯一般为正向思维,对于逆向思维来说需要多转一个弯,给审查程序者带来不方便。

7.5用加括号的方式排除二义性及增加可读性

例如:if( x & MASK= = BITS) 实际上意味着 if( x & (MASK= = BITS)。这个表达式肯定不是程序员的本意。
程序员的本意为 if (( x & MASK)= =BITS)。

7.6分解复杂的表达式,把复杂的表达式分解成多个简单的表达式。

7.7表达式要清晰:

我们的目标是写出最清晰的代码,而不是最巧妙的代码。
例如:subkey=subkey>>(bitoff-(bitoff>>3)<<3)。
等价于:subkey=subkey>>(bitoff&0x7);

7.8当心副作用。

像++这一类运算符具有副作用,他们除了返回一个值外,还将隐含的改变变量的值**。**
例如:str[i++]=str[i++]=’ ‘; 这样写意图是给str中随后的两个位置赋空格,但实际效果却依赖于i的更新时刻,很可能把str里的一个位置跳过去,也可能导致只对i 实际更新一次。这里应该把它分为两个语句:
str[i++]=’ ‘;
str[i++]=’ ‘;
例如:array[i++]=i;
如果初始时i的值是3,那吗数组元素可能被设置成3或4。
这里还有一个奇怪的例子a=(++x)+(++x)+(++x),a等于多少,大家可以试一试。

7.9避免对浮点类型做等于或不等于判断

因为浮点数在计算机内部是用符号位(用±表示)、尾码(用D表示)、阶码表示(用n表示),一个浮点数等于±Dx2n,它不能精确的表示所有的十进制浮点数。所以对浮点类型做等于或不等于判断存在着一定的安全隐患,用大于等于或小于等于是比较明智的替代方法。

7.10对于浮点类型可以用范围比较代替精确比较。

7.11慎用无符号数,尤其是无符号数与零的比较。

例如无符号数小于零的条件是永远无法满足的,这种错误也是非常常见的。

7.12尽量避免类型转换,

从大尺寸类型到小尺寸类型转换时会出现信息丢失的现象,这要求丢失的信息一定是无用的信息。

7.13若必须用类型转换,尽量用显示类型转换

显示方式更清晰、直观、可控性好,同时给其他阅读程序的人带来方便。

7.14慎用union

由于union成员公用内存空间,所以容易出错,并且维护困难。

7.15慎用位操作,可能带来兼容性问题

7.16慎用goto语句

8 语句书写规则

8.1用缩行显示程序的结构

例如:下面的例子的格式不好。
for(n=0;n<100;field[n++]=’\0’);
return(‘\n’);
应该为
for(n=0;n<100;n++)
{
field[n]=’\0’;
}
return(‘\n’);
在缩行时,用Tab键比空格键好。
1)在大多数编译器中Tab键可以定义成表示多少个空格,输入时用Tab键比空格键方便。
2)用Tab键对齐的文件可在不同的编译器下对齐。用空格键在某一编译器下对齐的文件,在另一个编译器下不一定能对齐。

8.2不要在一行中放置多条语句,不便于阅读

8.3每一行语句不要太长

最好能在一屏内完全显示,语句过长时可以按逻辑划分成不同的行。

8.4用空行将代码逻辑分段

8.5不要用过多的嵌套语句

过多的嵌套语句使理解起来非常困难、容易出错、对于程序的维护也带来很大的不便。

8.6 if语句的使用

1)if的左手法则,在使用判断时使用下面形式
if(TRUEbIsChar)而不是下面形式
if(bIsChar
TRUE)避免了下面的情况发生
if(bIsChar=TRUE)
2)if-else if-else中要用{}把语句括起来,即使一句,防止以后修改出现错误。
3)条件判别成立时要用相应的处理。
4)在if…**else if 语句中必须使用else **分支,保证逻辑处理的完整性。

8.7 switch语句的使用

1)case语句要按某重顺序排列,符合一定的逻辑关系。
2)每个case语句要单独占一行,每个case后都要有一个break语句,若没有要特殊说明,这个特殊说明非常重要,便于他人理解、修改及确认不需要break语句。若不加特殊说明审查程序的人还意味你写错了呢!
3)要有一个default语句处理缺省的情况,便于程序的控制及程序结构的完整。
4)避免使用空switch和空case语句。

8.8.在循环过程中不要修改循环计数器

例如
for(i=0;i<10;i++)
{
……….
i+=2;
}
若出现这样的语句意味着对循环控制能力变弱,程序出错的概率加大。

8.9变量定义应集中放置、各占一行

它的好处有便于修改变量的类型、了解变量的用途、便于程序的移植等,尤其是一些编译器不支持在函数的其他地方(开始的地方除外)定义变量。

8.10花括号{ }要单独占一行,这样便于对起。

8.11花括号{ }中没有或只有一条语句时也不能省略花括号

   例如 if(i==0) i++;
   应该写成 if(i==0)

{
i++;
}
原因,便于该语句的扩充,有时为了在该条件下增加一条语句忘了加{},造成比较难查出的逻辑错误。

8.12不要在引用操作符(”.””->”)前后加空格

若加空格打断了语义的连贯性。

8.13不要在单目操作符和其操作对象间加空格

例如 i++。

8.14不要在::前后加空格

8.15整个程序要保持 一致性

一致性带来的将是更好的程序。如果程序中的格式很随意,例如对数组做循环,一会二采用下标量从下到上开始,一会儿采用从上到下开始;一会儿采用for循环一会儿采用while循环。这些变化就会使人很难看清实际上到底怎么回事。而如果相同的计算每次出现总是采用相同的方式,任何变化就预示着是经过深思熟滤,引起读程序的人注意。

8.16 使用习惯用法

例如,对数组赋值
i=0;
while(i<nSum)
{
aArray[i++]=2;
}
然而习惯用法为:
for(i=0;i<nSum;i++)
{
nArray[i]=2;
}

9 函数的命名与要求

 在高校C/C++教材中对函数的命名为f(),g()这些命名使读者不知道到底什么含义。一个函数的名字要反映出该函数的主要功能。下面根据本人的经验谈谈函数的命名。

9.1函数命名

1)在C语言中由于没有类的概念,对于函数来说,前面要带有其模块名。命名规则为:模块名__函数名,在书写时每个单词的第一个字母要大写,例如Str_IsChar(char c),表示的含义为在字符处理的模块中判断一个char型的变量是否为字符类型的函数。这些命名的好处是对具有同样功能的函数在以字母排序时能集中在一起,便于修改与审查等。
2)函数要采用动作性的名字,即动词+名词,在书写时每个单词的第一个字母要大写,例如Sys__GetTime(),表示获取时间。
3)对返回布尔类型值的函数命名,应该清楚地反映其返回值情况。例如下面的函数命名BOOL Str__CheckChar( char c ),检查c是否为字符,这样命名不如下面的好BOOL str__IsChar(char c )。
4)在C++中对于成员函数不需要模块名,因为类名就能表示出功能,直接写函数名。
5)对于内联函数在函数的后面加大写的I,例如CmpStrI()。

9.2函数的使用

1)函数一定要做到先定义后使用,C++必须这样做(否则编译器通不过)。C程序没有强制要求,但也应该先提供原型,再使用函数。
2)函数原型的声明应集中放在一个头文件中,因为将同类/相关的(非成员)函数的原型声明集中放在一个头文件中,有利于引用和修改,因为只引用了一个熟知的头文件,修改也只在一个地方修改。
3)函数有参数时一定使用类型声明,不能使用缺省的参数类型;无参数一定要用void标注,例如int SetZero(void)比 int SetZero()好,C++和C对function()的解释不同。C++认为是不带参数;C认为是带任意参数(虽然ANSI C现在已经废除这一规则,但其他标准和编译器不一定保证这一点)。在有的编译器下会出现烦人的warning。
4)禁止过程参数只有类型没有标识符。例如 GetPoint(long,long)不允许。
5)函数的参数不要太多,一般一个函数的参数应该限制在5个以内。
6)函数的行数不要太多,一般在200行以内。
7)对于内置类型(如int,char,long等)参数应该用传值形式。
8)对于非内置类型参数应传递引用或指针。非内置类型指的是自定义的类、结构和联合,原因:
▲不用拷贝:非内置类型的尺寸一般大于引用和指针类型,尤其还要考虑非内置类型可能包含隐式的数据成员,比如虚函数指针等,所以拷贝代价高于传递引用或指针。
▲不用构造和析构。
▲有利于非内置类型的扩充。
▲有利于函数支持派生类。
9)显示定义返回类型。
10)若函数返回状态,尝试用枚举类型,返回枚举类型可以使编译器对返回值做合法检查(看看是不是枚举的合法成员)。
11)返回指针类型的函数应该用NULL表示失败,不要用0。
12)当函数返回引用或指针时,一定要用文字描述其有效期,若不特殊说明,可能在程序中引用无效的指针,修改该bug时可能带来惨重的代价。
13)嵌入汇编程序的过程(函数)必须是纯汇编程序。
14)谨慎使用abort,exit 等函数。
15)过程参数中慎用省略号,不利于参数匹配。

10 自定义的结构和类的命名及类的设计

10.1结构的命名与使用

1)全部用大写字母表示
例如:typedef struct
{
long x;
long y;
}LPOINT;
2)禁止在结构体中定义空域
例如: struct tagPOINT
{
long x;
struct //此处是空域,禁止使用
{
Long lTemp1;
Long lTemp2;
};
}LPOINT;
3)在结构体定义中谨慎使用位域

10.2类的命名规则与设计

1)C+表示类的功能名,功能名中每个开头的字母用大写。
例如CDrawMap表示画地图的类。
2)对象名应是名词,对象名用来标明事物,而事物应是名词。
3)实现行为的类成员函数名应是动词或动词+名词。
4)在类中先写成员变量,再写成员函数。这样可以保证成员变量与成员函数集中放置。
5)类的成员变量应该是私有的(private),利于封装。
6)提高类内的聚合度,降低类间的偶合度。
7)尽量使类的接口少而完备。
8)不要在类声明时提供成员的函数体。
9)确保成员变量在类的整个生命周期都有效,例如
class CDrawMap
{
private:
int *m_piHeigh;
}
该指针变量应该在构造函数中申请空间,在析构函数中释放。这样能保证该指针变量在整个生命周期中有效。
10)限制类继承的层数。
11)慎用友元函数,因为友元函数破坏了类的封装特性。
12)保证重载函数的所有版本都有共同的目的和相似的行为。
13)重载操作符时要遵守原操作符的含义、不要创新。例如不要把“+”运算符定义为减法的功能。
14)构造函数要实现完全的初始化,避免成员变量出现随机的值。如果成员变量使用了随机的值,这很可能造成不可重现的错误(这是一种非常难捉的BUG)。
15)在析构函数里要实现成员变量(主要是指针变量)的完全清除,避免内存泄露。内存的泄露是一个非常严重的问题,无论你的资源多丰富,只要存在内存泄露,资源一定有耗尽的时候。

11 注释

注释是一种工具,它的作用是帮助读者理解程序中的某些部分,而这些部分的意义不容易通过代码本身直接看到。注释要清晰、简洁,并且有价值。

11.1不要大谈明显的东西

注释不是去说明明明白白的事情,比如i++能够将i值加1等。

11、2确保所有的注释随代码及时更新

注释与代码保持一致性,不要让读者去猜是代码正确或注释正确。

11.3澄清情况,不要添乱

注释应该在困难的地方尽量帮助读者,而不是设置障碍。

11.4注释不要嵌套

11.5不要用/**/注释大块代码

应该用宏的条件编译语句#if()……….#endif。

11.6对每个引用的头文件给出行末注释

例如#include<iostream.h> //use cout,etc

11.7对空循环体给出确认性注释

例如while(str[I++]!=EOF)
{
//这是一个空循环体
}

11.8 给全局变量加注释

说明全局变量表示的含义。例如int g_hComm1;表示串口1的句柄。

11.9程序文件的开始部分应该有下面的注释。

/
// 主要功能:字符处理
// 版权信息:
// 文件名:dealstr.cpp
// 编写者:万方杰
// 相关的头文件:dealstr.h
/

11.10函数的开始处注释

函数的开始处应该有函数的功能、参数的含义、返回值及用到的全局变量等说明,例如
/
//功能描述: 计算两点之间的最短路径:
//用到的全局数据:
// GPS_CHAR *pNet:指向路网的指针
//参数说明:GPS_UNINT unStartNode 出发点的节点号
// GPS_UNINT unEndNode 目的点的节点号
//返回值:0 找路失败 1找路成功
//历史记录: 作者名字 日期 备注
// 万方杰 2000-01-10 创建
// 万方杰 2000-03-27 添加经由地
/

12 文件的命名与分类

在软件开发时,不知你碰到这样的问题没有,对一个文件几个人同时修改,最后出现版本不一致,甚至出现错误等。这里可以通过对文件的命名与分类部分的解决,如果结合版本管理软件(如微软的Source safe等)效果更好,下面主要谈谈文件的命名与分类

12.1建议一个文件中的程序总行不超过2000 行

12.2文件的命名最好能反映出该文件的功能,防止文件名冲突

可以通过目录或加前缀的方式来防止文件名冲突。头文件名禁止使用“‘”、“\”和“/*”等字符

12.3使用统一的文件名后缀

例如C的源文件后缀为.c;C++的源文件后缀为.cpp;头文件的后缀为.h;动态库文件的后缀.dll等。

12.4文件段落的安排

1)前言:文件的功能、版权等
2)包含的头文件即 先#include<>(<>表示系统或语言本身提供的头文件)后#include””(”表示用户自定义的头文件”)。避免了在头文件前有可执行代码。
3)宏定义部分即#define
4)类型声明或定义
5)函数的声明
6)全局变量声明或定义
7)常量声明或定义
8)函数的实现

12.5为了防止头文件的多次引用,一般用下面的格式

   #ifndef MYFILENAME_H_
   #define MYFILENAME_H_
   //头文件的内容
   #endif

注:禁止在同一个文件中有#if 而没有#endif

12.6引用头文件时不要使用绝对路径而要使用相对路径

好处为可以把程序放在不同的目录下不需要更改程序。若使用绝对路径例如g:/…/…/,若机器上没有g盘,其不是你要重新修改程序。
“.”表示当前路径,例如“./log.txt”表示当前目录下的log.txt,“…”表示上一级的目录。

12.7不要在头文件中定义常量或变量

因为如果多个源文件引用一个头文件时会出现该变量的多次定义而出错,同时头文件的编译顺序与对应的源文件的包含顺序有关。不同的包含顺序有不同的编译顺序,所以头文件的编译顺序存在着一定的偶然性。

12.8应该有一个typedef.h文件

对编译器的内置类型(如int,char,long等)进行重新定义这样便于移植:例如我在开发一个嵌入式项目时,刚开始有的16位的CPU,后来改为32位的CPU,在换CPU的时候出现以下问题:对于16位的CPU来说int类型为2个字节,对于32位的CPU来说int类型为4个字节,对文件操作时原来用的int类型在32位下必须该为short型。这些修改需要很大的工作量且容易出错,如果开始时进行类型定义,仅改一句语句就行即typedef int INT16;该为typedef short INT16。

12.9对于字符处理要有一个专门文件

现在字符存在两种类型ANSI和UICODE形式,不同的操作系统有采用不同的方式,例如WINCE操作系统采用的是UICODE,而WINDOW XP操作系统采用的是两种类型。因此把它专门分离出来便于程序的移植。
例如:
size_t DSstrlen(const TCHAR *str)
{
#ifdef _UNICODE
return wcslen(str);
#else
return strlen(str);
#endif
}

12.10与硬件有关的程序必须集中防在一个或几个文件中

保证在移植程序时使修改的文件数最少。

12.11、在C/C++嵌入的其它语言如汇编

应集中放在一个或几个文件中,便于移植和修改。

12.12程序必须包含一个初始化文件

该文件有一系列的初始化函数组成且只能包含初始化的函数。因为该文件是一个公用的,所以修改该文件时要非常慎重,以免自己的修改影响他人。

12.13在C++中源文件与头文件要一一对应

13 排错处理

13.1所有的变量都要进行初始化

若没有,它就可能取得某个具有随机性的值,这可能造成不可重现的错误,这种错误非常难排除。

13.2防御性的程序设计

在程序里增加一些代码,专门处理所有“不可能”出现的情况。

13.3检查错误的返回值

一个常被忽略的防御措施是检查函数的返回值。例如判断文件打开是否成功时一定要检查其返回值。

13.4充分利用const的健壮性

作为一个程序员或设计师一定要掌握const的健壮性

13.5代码审查

通过相关人员的分头阅读代码,可发现大家常犯的错误、笔误或不符合规范的代码。代码审查也是一种非常有效的排错手段。

13.6在编译时打开所有的编译检查,要关注编译时的警告提示

在警告提示中暗含某种错误,不要放过任何警告。

13.7在代码出现异常时能正确处理

   C++的异常处理是该语言中最难掌握的部分之一,同时又是最容易被忽略的地方。很多程序员都有这样的想法:如果出现问题的可能性不大,就先不予考虑,以后有机会再说。这是非常有害的想法,因为编程是一种严谨的工作,所以任何小的疏漏或隐患都会造成一些不良的后果。关于异常处理看相关的书籍。

13.8在出现异常时能够回收资源

13.9在程序中增加错误信息的提示或记录

如打印错误信息,把错误信息记录到日志文件中等。最好把用户不需要的错误提示错误信息记录到日志文件中,以避免用户对你的程序产生怀疑,这也可以称为“错误信息隐藏”。

13.10一定要把不可重现的错误弄成可以重复出现

不可重现的错误是错误中最难排除的一种。常见的有以下几种情况造成:
1)使用内存超出边界,例如char *pName;pName=new[2056],若在程序中使用了pName[2056]=-1这样的语句,必然会破坏了后面的内存,造成后面内存数据混乱而出错。
2)使用了未赋值或未始化的变量。
3)使用了无效的指针。例如下面的程序
int *GetPoint(void)
{
int nPoint=0;
return &nPoint;
}
这个指针是无效的指针,因为局部变量的生命周期时从函数调用时开始到函数的返回时结束。

13.10对于难查的错误可以采取分而治之的方法

把一个函数分成若干块,分别对每块进行测试,对错误进行定位,修改错误,最后合成到一起进行测试。

14 可移植性或兼容性

指源代码适应不同的编译器、操作系统、硬件平台等。

14.1要遵守ANSI C和ISO C++国际标准

1)避免使用某个便于器自定义的语法法则。可能很好用,但不利于移植
2)慎用最新的国际标准,因为有可能主流编译器还不支持。

14.2将不符合国际标准的代码与其他代码分开。

14.3确保类型转换不会丢失信息

从大尺寸类型转换成小尺寸类型一定会丢失某些数据,要确保丢失的数据是不需要的,若有过多的类型转换说明你的程序设计有问题。

14.4注意数据类型的大小

在C或C++里,基本数据类型的大小并没有明确的定义,仅给出下面这些规则 **sizeof( char )<=sizeof( short )<=sizeof( int )<=sizeof( long)<=sizeof(float)<=sizeof( double )。可以对变量类型进行重新定义,以便在不同的编译器或机器上移植。
例如
:**在导航项目中,对类型进行了重新定义。
typedef unsigned char GPS_UNCHAR;
typedef char GPS_CHAR;
typedef unsigned short GPS_UNINT;
typedef short GPS_INT;
用sizeof 计算类型和对象的值。

14.5用typedef 自定义的类型禁止被重新定义

例如:禁止这样使用:
typedef int mytype;
typedef float mytype

14.6对于算术或者逻辑移位要慎重

在对有符号的量用运算符>>做右移时,这个移位可以是算术的(符号位在移位的过程中复制),也可以是逻辑的(移位中空出的位被自动补0)。绝不右移带符号的值。

14.7注意结构或类成员的对齐

在结构、类里,各个成分的对齐方式并没有规定。只规定各个成分按说明的顺序排列。你绝不能假定在一个结构里各成员占着连续的存储区,对齐限制实际上会造成结构中的“空洞”。
例如:在VC编译器下默认的4字节对齐方式。如下面结构:
typedef stuct{
char cBlockNum;
long lLen;
}NODEATR;
在这里插入图片描述

结构中的 cBlockNum指向0字节,而lLen指向第4个字节,造成1~3字节的内存空洞,

14.8位操作对机器的依赖性太强,使用时要仔细考虑

因为不同机器对数据类型的存储方式不同,例如Intel采用的是小端序(little endian)。IBM S/370采用的大端序(big_endian)。ARM编译器可以选择小端序或大端序。
小端序:低字节存在低位,高字节存在高位。大端序低字节存在高位,高字节存在低位。例如:长整型数0X12345678的存储
在这里插入图片描述
因此,对位进行操作时,不同的机器会产生不同的结果。

14.9一定不要重新实现标准库函数

标准的库函数是经过检验的在性能和稳定性能上都是非常可靠的。

14.10合理的使用条件编译语句可增加代码的移植性

15、性能

15.1学会使用性能分析工具,例如VC的Profile等。

15.2找到问题的瓶颈所在,针对瓶颈进行优化。

15.3使用系统的自动计时进行测量

我曾经见过很多人用秒表进行测量某一功能所用的时间,这种方法太笨了吧,不要笑也许你也用过,我是曾经用过。在进行测量时尽量用系统提供的功能,例如在在VC下测量时间可用GetTickCount()函数进行测量。

15.4对于瓶颈所在的地方使用更好的算法和数据结构

15.5对于瓶颈所在的地方进行代码调整和优化。

1) 收集公共表达式,如果一个代价昂贵的计算出现多次,那么就只在一个地方计算,并记录计算结果。
2) 用低代价代码代替高代价代码,例如:在进行平方根的比较时,可变成平方的比较,这样就可以大大的提高程序的运行速度。
3) 用移位操作代替整数的乘除操作等。
4) 用整数运算代替浮点运算。例如在进行图象旋转时要进行浮点运算,而对于开始坐标和结果坐标都为整数,所以对于这种情况可进行下列优化,优化原理如下:
int ComputeRotateX(int nOrgX)
{
int nDestX=0;
int nFactor=65536;//2的16次方
float fRotate=0.40500;
int nRotate= 0;
nRotate= (int)(fRotate nFactor);
nDestX=(nOrgX
nRotate)>>16;
}
在该函数中nRotate可只进行一次计算,这样就实现了用整数的运算代替的浮点运算。该优化在做图像处理时非常有用。

15.6不要在循环体内定义变量或对象

因为在C++中每个变量的生命周期开始于“{”后的定义结束于“}”之后。所以在循环体内定义变量或对象,出现该变量或对象的多次构造和析构。

16、其他

16.1避免产生全局数据对象

原则上既然在C++中引入了类就不需要全局数据对象。

16.2确保任何定义只发生一次

若不能保证哪你是自找麻烦,同时也说明你的设计存在问题。

16.3编程时不要试图展示自己的非凡手法与自己得意的技巧

比如使用语言罕见的功能或使用一些技巧等。软件开发是一项协作工程,让别人容易看懂、容易使用、容易维护是基本的要求。

16.4将不用的代码删掉

现在对版本控制的工具很多及硬盘的容量很大,没有必要保留删除的代码。

16.5要熟悉编译器的各种性能

工欲善其事,必先利其器。所以我们在编程之前要熟悉其编译器的各种功能,这对开发程序、调试、移植等都有很大的帮助。

16.6把成熟的模块变成库

16.7写自动测试代码

尤其是对重复性及验证性的测试写出自动测试代码,这样即保证了测试的客观性也可以节省大量的劳动力,大大的提高了测试效率。例如在嵌入式地理信息系统开发中我对路径搜索算法做了一个自动测试的程序,任意的两个结点(道路交叉口)之间进行路径搜索,把路径搜索失败的结点在地图上高亮度显示。这样即可以验证路网数据是否正确,同时也可以验证算法是否正确。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值