文章目录
1.目标
我们所认为的优秀程序员应该具备的能力是什么?高效的算法,优良的架构,设计模式,面向对象等等,这些我们每天挂在嘴上,喊着要学习的技能。确实,能很好的掌握这些技能是成为优秀程序员的条件,但是作为程序员最根本,最基础的是代码编写,如何编写高质量,可靠的,规范的代码,是作为一名合格程序员的基础和根本,本规范规定了我们编写代码的格式,排版,注释,函数,变量,命名等,从这些最基础的编码规范来纠正我们的一些编码错误,要写出高质量的代码这些都是第一步,只有把这个基础打扎实,我们才可能“建设”稳健的“代码大厦”。
2.排版格式
程序排版格式虽然不是十分严格的规范要求,但整个项目都服从统一的编程风格,这样使所有人在阅读和理解代码时更加容易。格式虽然不会影响程序的功能,但会影响可读性。程序的排版格式追求清晰、美观,是程序风格的重要构成因素。可以把程序的排版格式比喻为“书法”。好的“书法”可让人对程序一目了然,看得兴致勃勃。差的程序“书法”如螃蟹爬行,让人看得索然无味,更令维护者烦恼有加。请程序员们学习程序的“书法”,弥补大学计算机教育的漏洞,实在很有必要。
2.1 类及其排版格式
- 声明属性依次序是public:、protected:、private:。
- 关键字public,protected,private不要缩进,声明的函数和变量缩进一个制表符。
- 类声明前应加上注释,注明该类的主要作用。
- 不改变对象成员变量的类成员函数都应标记为const,可以预防意外的变动,提高程序的健壮性。
- 类中成员必须进行初始化,可以通过构造函数的初始化列表初始化成员或者写一个专门初始化成员的函数(如init())。
- 有继承关系的基类中析构函数一定要声明为虚函数。
- 为了防止头文件重复包含,应在头文件处加上#ifndef/#define/#endif宏。
- 函数和成员变量的声明分开。
类声明的基本格式如下:class MyClass : public OtherClass { public: MyClass(); MyClass(int var); ~MyClass() {} void someFunction(); void someFunctionThatDoesNothing() void set_some_var(int var) int some_var() const private: bool someInternalFunction(); private: int some_var_; int some_other_var_; };
2.2 函数的声明与定义
返回类型和函数名在同一行,合适的话,参数也放在同一行。
返回值总是和函数名在同一行;参数列表的左圆括号总是和函数名在同一行。
函数名和左圆括号间没有空格;圆括号与参数间没有空格。
左大括号总是新起一行;右大括号总是单独位于函数最后一行。
函数的声明(头文件)和实现处(CPP)的所有形参名称必须保持一致。
函数的内容总与左括号保持一个制表符的缩进。
参数间的逗号总加一个空格。
函数的大小一般不要超过50行,函数越小,代码越容易维护。
函数声明前应加上注释,注明该函数的作用,如果该函数有比较多的参数,还应该加上参数含义和返回值的注释。
如果函数的参数是类对象,应使用对象的指针或引用来传递,以提高效率。
ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) { DoSomething(); ... } 如果同行一行文本较多,容不下所有参数时: ReturnType ClassName::FunctionName(Type p_name1, Type p_name2, Type p_name3) { DoSomething(); ... }
如果函数为const的,关键字const应与最后一个参数位于同一行。
ReturnType FunctionName(Type par) const { ... }
2.3 空行
空行起着分隔程序段落的作用。空行得体(不过多也不过少)将使程序的布局更加清晰。空行不会浪费内存,虽然打印含有空行的程序是会多消耗一些纸张,但是值得。所以不要舍不得用空行。
- 在每个类声明之后、每个函数定义结束之后都要加空行。
// 空行
void Function1(…)
{
…
}
// 空行
void Function2(…)
{
…
}
- 在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。
// 空行
while (condition)
{
statement1;
// 空行
if (condition)
{
statement2;
}
else
{
statement3;
}
// 空行
statement4;
}
2.4 代码行
- 一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样的代码容易阅读,并且方便于写注释。
- if、for、while、do等语句自占一行,执行语句不得紧跟其后。不论执行语句有多少都要加{}。这样可以防止书写失误。
示例2-4(a)为风格良好的代码行,示例2-4(b)为风格不良的代码行。
int width = 0; // 宽度
int height = 0; // 高度
int depth = 0; // 深度
2-4(a)
int width, height, depth; // 宽度高度深度
2-4(b)
x = a + b;
y = c + d;
z = e + f;
2-4(a)
X = a + b; y = c + d; z = e + f;
2-4(b)
if (width < height)
{
dosomething();
}
for (initialization; condition; update)
{
dosomething();
}
// 空行
other();
2-4(a)
if (width < height) dosomething();
for (initialization; condition; update)
dosomething();
other();
2-4(b)
示例2-4(a) 风格良好的代码行 示例2-4(b) 风格不良的代码行
- 在定义变量的同时必须初始化该变量。
如果变量的引用处和其定义处相隔比较远,变量的初始化很容易被忘记。如果引用了未被初始化的变量,可能会导致程序错误。例如
int width = 10; // 定义并初绐化width
int height = 10; // 定义并初绐化height
int depth = 10; // 定义并初绐化depth
2.5 代码行内的空格
- ‘,’之后要留空格,如Function(x, y, z)。如果‘;’不是一行的结束符号,其后要留空格,如for (initialization; condition; update)。
- 赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符,如“=”、“+=” “>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”,“^”等二元操作符的前后应当加空格。
- 一元操作符如“!”、“~”、“++”、“–”、“&”(地址运算符)等前后不加空格。
- 像“[]”、“.”、“->”这类操作符前后不加空格。
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();
示例2-5 代码行内的空格
2.6 对齐
- 程序的分界符‘{’和‘}’应独占一行并且位于同一列,同时与引用它们的语句左对齐。
- { }之内的代码块在‘{’右边一个制表符左对齐。
示例2-6(a)为风格良好的对齐,示例2-6(b)为风格不良的对齐。
void Function(int x)
{
… // program code
}
2-6(a)
void Function(int x){
… // program code
}
2-6(b)
if (condition)
{
… // program code
}
else
{
… // program code
}
2-6(a)
if (condition){
… // program code
}
else {
… // program code
}
2-6(b)
for (initialization; condition; update)
{
… // program code
}
2-6(a)
for (initialization; condition; update){
… // program code
}
2-6(b)
While (condition)
{
… // program code
}
2-6(a)
while (condition){
… // program code
}
2-6(b)
如果出现嵌套的{},则使用缩进对齐,如:
{
…
{
…
}
…
}
示例2-6(a) 风格良好的对齐 示例2-6(b) 风格不良的对齐
2.7 长行拆分
- 代码行最大长度宜控制在70至80个字符以内。代码行不要过长,否则眼睛看不过来,也不便于打印。
- 长表达式要在低优先级操作符处拆分成新行,操作符放在新行之首(以便突出操作符)。拆分出的新行要进行适当的缩进,使排版整齐,语句可读。
if ((very_longer_variable1 >= very_longer_variable12)
&& (very_longer_variable3 <= very_longer_variable14)
&& (very_longer_variable5 <= very_longer_variable16))
{
dosomething();
}
virtual CMatrix CMultiplyMatrix (CMatrix leftMatrix,
CMatrix rightMatrix);
for (very_longer_initialization;
very_longer_condition;
very_longer_update)
{
dosomething();
}
示例2-7 长行的拆分
2.8 修饰符的位置
修饰符 * 和 & 应该靠近数据类型还是该靠近变量名,是个有争议的活题。
若将修饰符 * 靠近数据类型,例如:int* x; 从语义上讲此写法比较直观,即x是int 类型的指针。
上述写法的弊端是容易引起误解,例如:int* x, y; 此处y容易被误解为指针变量。虽然将x和y分行定义可以避免误解,但并不是人人都愿意这样做。
- 应当将修饰符 * 和 & 紧靠变量名
例如:
char *name;
int *x, y; // 此处y不会被误解为指针
2.9 代码段中的预处理格式
- 预处理指令不要缩进,从行首开始。即使预处理指令位于缩进代码块中,指令也应从行首开始。
例如:
if(lopsided_score)
{
#if DISASTER_PENDING // 正确 – 预处理从行首开始
dropEverything();
#endif
BackToNormal();
}
2.10 被注释的代码
- 在代码中经常残留一下被注释的代码,如果这段代码还有价值,必须对该段代码加上被注释的原因,或者不需要有的就直接删除。
2.11 注释
注释虽然写起来很痛苦,但对保证代码可读性至为重要。当然,注释的确很重要,但最好的代码本身就是文档,类名和变量名意义明确要比通过注释解释模糊的命名要好的多。注释是为别人(下一个需要理解你代码的人)而写的,认真点吧,下一个人可能就是你!如何写注释请详见《合约编程》
- 注释是对代码的“提示”,而不是文档。程序中的注释不可喧宾夺主,注释太多了会让人眼花缭乱。注释的花样要少。
- 如果代码本来就是清楚的,则不必加注释。否则多此一举,令人厌烦。单行注释用//,多行注释用/* */。
例如 i++; // i 加 1,多余的注释 - 边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。
- 注释应当准确、易懂,防止注释有二义性。错误的注释不但无益反而有害。
- 尽量避免在注释中使用缩写,特别是不常用缩写。
- 注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,不可放在下方。
3.命名规则
命名规则是软件编程中重要的规则之一,规范命名风格会使代码风格保持一致,更容易被团队中其他伙伴理解。
总体原则是所有命名必须做到见名知意。
标识符应当直观且可以拼读,可见名知意,不必进行“解码”。
标识符最好采用英文单词或其组合,便于记忆和阅读。切忌使用汉语拼音来命名。程序中的英文单词一般不会太复杂,用词应当准确。例如不要把currentValue写成nowValue。命名规则要求符合匈牙利命名法。
所有的变量,函数,类的命名,若需要多个单词时,每个单词直接连写,不要用下划线(“_”)或横线(“-”)分开。如:DeviceInfo,RemoteCamera。
类的名字应当使用“名词”。能准确表示被抽象的事物。首字母以大写开头。
例如:要重新一个摄像机,可以定义一个Camera类。函数的名字应当使用“动词”或者“动词+名词”(动宾词组)。类的成员函数应当只使用“动词”,被省略掉的名词就是对象本身。
例如:
DrawBox(); // 全局函数
box->Draw(); // 类的成员函数变量的名字应当使用“名词”或者“形容词+名词”。首字母以小写开头。
例如:
float value;
float oldValue;
float newValue;对于变量命名,禁止取单个字符(如i、j、k…),建议除了要有具体含义外,还能表明其变量类型、数据类型等,但i、j、k作局部循环变量是允许的。
变量名字需要加上作用域的前缀,类的成员变量加上“m_”前缀,局部变量不加作用域前缀,全局变量加上“g_”前缀。
变量型前缀见下表:
|前缀 | 类型说明 |
| —— | —— |
|n |整型变量(int/short/long)|
|c |字符型变量(char)|
|psz| 字符型变量指针char*|
|b |布尔型变量(bool)|
|w |整型变量word|
|dw |整型变量DWORD|
|bt |字节变量byte|
|e |枚举型变量(enum)|
|d |浮点型变量(double)|
|sz |字符型变量(char[n])|
|str | 字符型变量(string)|
|obj | 类实例变量|
|pobj | 类实例变量指针|
|iter | STL迭代器变量(iterator)|
|m_ |类成员变量|
|p_ |函数参数|
|g_ |全局变量|
|h |句柄|常量命名,枚举命名,宏命名都用大写,每个单词直接用下划线(“_”)分开。如:ENCODE_TYPE_H264,ENCODE_TYPE_MPEG2。
项目的名字都使用“名词”,首字母以大写开头。
用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。
例如:
int minValue;
int maxValue;
int setValue(…);
int getValue(…);
避免名字中出现数字编号,如Value1,Value2等,除非逻辑上的确需要编号。这是为了防止程序员偷懒,不肯为命名动脑筋而导致产生无意义的名字(因为用数字编号最省事)。
程序中不要出现仅靠大小写区分的相似的标识符。
例如:
int x, X; // 变量x 与 X 容易混淆
void foo(int x); // 函数foo 与FOO容易混淆
void FOO(float x);
程序中不要出现标识符完全相同的局部变量和全局变量,尽管两者的作用域不同而不会发生语法错误,但会使人误解。
命名风格保持一致,自己特有的命名风格,要自始至终保持一致,不可来回变化。项目组或产品组对命名有统一的规定,个人服从团队。说明:个人的命名风格,在符合所在项目组或产品组的命名规则的前提下,才可使用。(即命名规则中没有规定到的地方才可有个人命名风格)。
命名中若使用特殊约定或缩写,则要有注释说明。
说明:应该在源文件的开始之处,对文件中所使用的缩写或约定,特别是特殊的缩写进行必要的注释说明。避免命名空间与类名冲突,命名空间的定义以NS_为前缀。
4.表达式和基本语句
4.1 运算符的优先级
C++中的运算符有很多,并且有严格的优先级和结合律,记忆起来比较困难。
- 如果代码行中的运算符比较多,使用用括号明确表达式的操作顺序,避免使用默认优先级。
示例:下列语句中的表达式
word = (high << 8) | low (1)
if ((a | b) && (a & c)) (2)
if ((a | b) < (c & d)) (3)
如果书写为
high << 8 | low
a | b && a & c
a | b < c & d
由于
high << 8 | low = ( high << 8) | low,
a | b && a & c = (a | b) && (a & c),
(1)(2)不会出错,但语句不易理解;
a | b < c & d = a | (b < c) & d,(3)造成了判断条件出错
4.2 复合表达式
如 a = b = c = 0这样的表达式称为复合表达式。允许复合表达式存在的理由是:(1)书写简洁;(2)可以提高编译效率。但要防止滥用复合表达式。
不要编写太复杂的复合表达式。
例如:
i = a >= b && c < d && c + f <= g + h ; // 复合表达式过于复杂不要有多用途的复合表达式。
例如:
d = (a = b + c) + r ;
该表达式既求a值又求d值。应该拆分为两个独立的语句:
a = b + c;
d = a + r;不要把程序中的复合表达式与“真正的数学表达式”混淆。
例如:
if (a < b < c) // a < b < c是数学表达式而不是程序表达式
并不表示
if ((a<b) && (b<c))
而是成了令人费解的if ( (a<b)<c )
4.3 避免直接使用数字作为标识符
- 避免使用不易理解的数字,用有意义的标识来替代。涉及物理状态或者含有物理意义的常量,不应直接使用数字,必须用有意义的枚举或常量来代替。
示例:如下的程序可读性差。
if (Trunk[index].trunk_state == 0)
{
Trunk[index].trunk_state = 1;
... // program code
}
应改为如下形式。
const int TRUNK_IDLE = 0;
const int TRUNK_BUSY = 1;
if (Trunk[index].trunk_state == TRUNK_IDLE)
{
Trunk[index].trunk_state = TRUNK_BUSY;
... // program code
}
4.4 if 语句
if语句是C++/C语言中最简单、最常用的语句,然而很多程序员用隐含错误的方式写if语句。本节以“与零值比较”为例,展开讨论。
不可将布尔变量直接与TRUE、FALSE或者1、0进行比较。
根据布尔类型的语义,零值为“假”(记为FALSE),任何非零值都是“真”(记为TRUE)。TRUE的值究竟是什么并没有统一的标准。例如Visual C++ 将TRUE定义为1,而Visual Basic则将TRUE定义为-1。
假设布尔变量名字为flag,它与零值比较的标准if语句如下:
if (flag) // 表示flag为真
if (!flag) // 表示flag为假
其它的用法都属于不良风格,例如:
if (flag == TRUE)
if (flag == 1 )
if (flag == FALSE)
if (flag == 0)应当将整型变量用“==”或“!=”直接与0比较。
假设整型变量的名字为value,它与零值比较的标准if语句如下:
if (value == 0)
if (value != 0)
不可模仿布尔变量的风格而写成
if (value) // 会让人误解 value是布尔变量
if (!value)不可将浮点变量用“” 或 “!=”与任何数字比较。
千万要留意,无论是float还是double类型的变量,都有精度限制。所以一定要避免将浮点变量用“”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。
假设浮点变量的名字为x,应当将
if (x == 0.0) // 隐含错误的比较
转化为
if ((x>=-EPSINON) && (x<=EPSINON))
其中EPSINON是允许的误差(即精度)。应当将指针变量用“==”或“!=”与NULL比较。
指针变量的零值是“空”(记为NULL)。尽管NULL的值与0相同,但是两者意义不同。假设指针变量的名字为p,它与零值比较的标准if语句如下:
if (p == NULL) // p与NULL显式比较,强调p是指针变量
if (p != NULL)
不要写成
if (p == 0) // 容易让人误解p是整型变量
if (p != 0)
或者
if § // 容易让人误解p是布尔变量
if (!p)有时候我们可能会看到 if (NULL == p) 这样古怪的格式。不是程序写错了,是程序员为了防止将 if (p == NULL) 误写成 if (p = NULL),而有意把p和NULL颠倒。编译器认为 if (p = NULL) 是合法的,但是会指出 if (NULL = p)是错误的,因为NULL不能被赋值。
4.5 循环语句的效率
C++/C循环语句中,for语句使用频率最高,while语句其次,do语句很少用。本节重点论述循环体的效率。提高循环体效率的基本办法是降低循环体的复杂性。
- 在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU跨切循环层的次数。例如示例4-5(b)的效率比示例4-5(a)的高。
for (row=0; row<100; row++)
{
for ( col=0; col<5; col++ )
{
sum = sum + a[row][col];
}
}
4-5(a)
for (col=0; col<5; col++ )
{
for (row=0; row<100; row++)
{
sum = sum + a[row][col];
}
}
4-5(b)
示例4-5(a) 低效率:长循环在最外层 示例4-5(b) 高效率:长循环在最内层
- 如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。示例4-4©的程序比示例4-4(d)多执行了N-1次逻辑判断。并且由于前者老要进行逻辑判断,打断了循环“流水线”作业,使得编译器不能对循环进行优化处理,降低了效率。如果N非常大,最好采用示例4-4(d)的写法,可以提高效率。如果N非常小,两者效率差别并不明显,采用示例4-4©的写法比较好,因为程序更加简洁。
for (i=0; i<N; i++)
{
if (condition)
DoSomething();
else
DoOtherthing();
}
4-4(c)
if (condition)
{
for (i=0; i<N; i++)
DoSomething();
}
else
{
for (i=0; i<N; i++)
DoOtherthing();
}
4-4(d)
表4-4(c) 效率低但程序简洁 表4-4(d) 效率高但程序不简洁
4.6 new的使用
- new后的指针在使用前也需要进行非空判断,delete指针后应将指针置为空。
5.常量
5.1 为什么需要常量
如果不使用常量,直接在程序中填写数字或字符串,将会有什么麻烦?
- 程序的可读性(可理解性)变差。程序员自己会忘记那些数字或字符串是什么意思,用户则更加不知它们从何处来、表示什么。
- 在程序的很多地方输入同样的数字或字符串,难保不发生书写错误。
- 如果要修改数字或字符串,则会在很多地方改动,既麻烦又容易出错。
- 尽量使用含义直观的常量来表示那些将在程序中多次出现的数字或字符串。
例如:
#define MAX 100 /* C语言的宏常量 */
const int MAX = 100; // C++ 语言的const常量
const float PI = 3.14159; // C++ 语言的const常量
5.2 const 与 #define的比较
C++ 语言可以用const来定义常量,也可以用 #define来定义常量。但是前者比后者有更多的优点:
- const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。
- 有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。
- 在C++ 程序中只使用const常量而不使用宏常量,即const常量完全取代宏常量。
5.3 常量定义规则
- 需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部。为便于管理,可以把不同模块的常量集中存放在一个公共的头文件中。
- 如果某一常量与其它常量密切相关,应在定义中包含这种关系,而不应给出一些孤立的值。
例如:
const float RADIUS = 100;
const float DIAMETER = RADIUS * 2;
6.函数
根据《合约编程》中对函数的定义:一个正确的方法应该“不多不少”正好完成要求它完成的功能。该部分的基础理论是《TDD与单元测试》、《合约编程》。
6.1 参数的规则
参数的书写要完整,不要贪图省事只写参数的类型而省略参数名字。如果函数没有参数,则用void填充。
例如:void setValue(int p_nWidth, int p_nHeight); // 良好的风格 void setValue(int, int); // 不良的风格
参数命名要恰当,顺序要合理。
例如编写字符串拷贝函数StringCopy,它有两个参数。如果把参数名字起为str1和str2,例如void stringCopy(char * p_str1, char * p_str2);那么我们很难搞清楚究竟是把str1拷贝到str2中,
还是刚好倒过来。可以把参数名字起得更有意义,如叫strSource和strDestination。这样从名字上就可以看出应该把strSource拷贝到strDestination。还有一个问题,这两个参数那一个该在前那一个该在后?
参数的顺序要遵循程序员的习惯。一般地,应将目的参数放在前面,源参数放在后面。
如果将函数声明为:
void stringCopy(char * p_strSource, char * p_strDestination);
别人在使用时可能会不假思索地写成如下形式:
char str[20];
stringCopy(str, “Hello World”); // 参数顺序颠倒如果参数是指针,且仅作输入用,则应在类型前加const,以防止该指针在函数体内被意外修改。
例如:
void stringCopy(char * p_strDestination,const char * p_strSource);
函数中的指针入参必须进行非空判断如果输入参数以值传递的方式传递对象,则宜改用“const &”方式来传递,
这样可以省去临时对象的构造和析构过程,从而提高效率。避免函数有太多的参数,参数个数尽量控制在5个以内。如果参数太多,在使用时容易将参数类型或顺序搞错。
尽量不要使用类型和数目不确定的参数。
C标准库函数printf是采用不确定参数的典型代表,其原型为:int printf(const chat *format[, argument]…); 这种风格的函数在编译时丧失了严格的类型安全检查。
6.2 返回值的规则
不要省略返回值的类型。
C语言中,凡不加类型说明的函数,一律自动按整型处理。这样做不会有什么好处,却容易被误解为void类型。
C++语言有很严格的类型安全检查,不允许上述情况发生。由于C++程序可以调用C函数,为了避免混乱,规定任何C++/ C函数都必须有类型。如果函数没有返回值,那么应声明为void类型。函数名字与返回值类型在语义上不可冲突。
违反这条规则的典型代表是C标准库函数getchar。
例如:
char c;
c = getchar();
if (c == EOF)
…
按照getchar名字的意思,将变量c声明为char类型是很自然的事情。但不幸的是getchar的确不是char类型,而是int类型,其原型如下:
int getchar(void);
由于c是char类型,取值范围是[-128,127],如果宏EOF的值在char的取值范围之外,那么if语句将总是失败,这种“危险”人们一般哪里料得到!导致本例错误的责任并不在用户,是函数getchar误导了使用者。不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用return语句返回。
回顾上例,C标准库函数的设计者为什么要将getchar声明为令人迷糊的int类型呢?他会那么傻吗?
在正常情况下,getchar的确返回单个字符。但如果getchar碰到文件结束标志或发生读错误,它必须返回一个标志EOF。为了区别于正常的字符,只好将EOF定义为负数(通常为负1)。因此函数getchar就成了int类型。
我们在实际工作中,经常会碰到上述令人为难的问题。为了避免出现误解,我们应该将正常值和错误标志分开。即:正常值用输出参数获得,而错误标志用return语句返回。
函数getchar可以改写成 BOOL GetChar(char *c);
虽然gechar比GetChar灵活,例如 putchar(getchar()); 但是如果getchar用错了,它的灵活性又有什么用呢?如果函数的返回值是一个对象,有些场合用“引用传递”替换“值传递”可以提高效率。而有些场合只能用“值传递”而不能用“引用传递”,否则会出错。
例如:class String {… // 赋值函数 String & operate=(const String &other); // 相加函数,如果没有friend修饰则只许有一个右侧参数 friend String operate+( const String &s1, const String &s2); private: char *m_data; } String的赋值函数operate = 的实现如下: String & String::operate=(const String &other) { if (this == &other) return *this; delete m_data; m_data = new char[strlen(other.data)+1]; strcpy(m_data, other.data); return *this; // 返回的是 *this的引用,无需拷贝过程 }
对于赋值函数,应当用“引用传递”的方式返回String对象。如果用“值传递”的方式,虽然功能仍然正确,但由于return语句要把 *this拷贝到保存返回值的外部存储单元之中,增加了不必要的开销,降低了赋值函数的效率。例如:
String a,b,c; … a = b; // 如果用“值传递”,将产生一次 *this 拷贝 a = b = c; // 如果用“值传递”,将产生两次 *this 拷贝 String的相加函数operate + 的实现如下: String operate+(const String &s1, const String &s2) { String temp; delete temp.data; // temp.data是仅含‘\0’的字符串 temp.data = new char[strlen(s1.data) + strlen(s2.data) +1]; strcpy(temp.data, s1.data); strcat(temp.data, s2.data); return temp; }
对于相加函数,应当用“值传递”的方式返回String对象。如果改用“引用传递”,那么函数返回值是一个指向局部对象temp的“引用”。由于temp在函数结束时被自动销毁,将导致返回的“引用”无效。例如:
c = a + b;
此时 a + b 并不返回期望值,c什么也得不到,留下了隐患。对所调用函数的错误返回值要仔细、全面地处理。
7.建议
7.1 使用const
看到const关键字,C++程序员首先想到的可能是const常量。这可不是良好的条件反射。如果只知道用const定义常量,那么相当于把火药仅用于制作鞭炮。const更大的魅力是它可以修饰函数的参数、返回值,甚至函数的定义体。const是constant的缩写,“恒定不变”的意思。被const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。所以很多C++程序设计书籍建议:“Use const whenever you need”。
用const修饰函数的参数
如果参数作输出用,不论它是什么数据类型,也不论它采用“指针传递”还是“引用传递”,都不能加const修饰,否则该参数将失去输出功能。
const修饰输入参数:如果输入参数采用“指针传递”,那么加const修饰可以防止意外地改动该指针,起到保护作用。例如stringCopy函数:void stringCopy(char * p_strDestination, const char * p_strSource); 其中p_strSource是输入参数,p_strDestination是输出参数。给p_strSource加上const修饰后,如果函数体内的语句试图改动p_strSource的内容,编译器将指出错误。
如果输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,所以不要加const修饰。用法总结一下1.对于非内部数据类型的输入参数,应该将“值传递”的方式改为“const引用传递”,目的是提高效率。 例如将void Func(A a) 改为void Func(const A &a)。 因为函数体内将产生A类型的临时对象用于复制参数a,而临时对象的构造、复制、析构过程都将消耗时间。为了提高效率,可以将函数声明改为void Func(A &a),因为“引用传递”仅借用一下参数的别名而已, 不需要产生临时对象。但是函数void Func(A &a)存在一个缺点:“引用传递”有可能改变参数a, 这是我们不期望的。解决这个问题很容易,加const修饰即可,因此函数最终成为void Func(const A &a)。 2.对于内部数据类型的输入参数,不要将“值传递”的方式改为“const引用传递”。否则既达不到提高效率的目的,又降低了函数的可理解性。 例如void Func(int x) 不应该改为void Func(const int &x)。 以上此类推,是否应将void Func(int x) 改写为void Func(const int&x),以便提高效率? 完全没有必要,因为内部数据类型的参数不存在构造、析构的过程,而复制也非常快, “值传递”和“引用传递”的效率几乎相当
用const修饰函数的返回值
如果给以“指针传递”方式的函数返回值加const修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const修饰的同类型指针。
例如 函数 const char * getString();
如下语句将出现编译错误:
char *str = getString();
正确的用法是
const char *str = getString();
如果函数返回值采用“值传递方式”,由于函数会把返回值复制到外部临时的存储单元中,加const修饰没有任何价值。
例如不要把函数int getInt() 写成const int getInt(void)。
同理不要把函数A getA() 写成const A getA(),其中A为用户自定义的数据类型。
如果返回值不是内部数据类型,将函数A getA() 改写为const A & getA()的确能提高效率。但此时千万千万要小心,一定要搞清楚函数究竟是想返回一个对象的“拷贝”还是仅返回“别名”就可以了,否则程序会出错。见6.2节“返回值的规则”。const成员函数
任何不会修改数据成员的函数都应该声明为const类型。如果在编写const成员函数时,不慎修改了数据成员,或者调用了其它非const成员函数,编译器将指出错误,这无疑会提高程序的健壮性。以下程序中,类stack的成员函数getCount仅用于计数,从逻辑上讲getCount应当为const函数。编译器将指出getCount函数中的错误。class Stack { public: void push(int elem); int pop(); int getCount() const; // const成员函数 private: int m_num; int m_data[100]; }; int Stack::getCount() const { ++ m_num; // 编译错误,企图修改数据成员m_num pop(); // 编译错误,企图调用非const函数 return m_num; }
const成员函数的声明看起来怪怪的:const关键字只能放在函数声明的尾部,大概是因为其它地方都已经被占用了。
7.2 其他建议
- 当心那些视觉上不易分辨的操作符发生书写错误。
我们经常会把“==”误写成“=”,象“||”、“&&”、“<=”、“>=”这类符号也很容易发生“丢1”失误。然而编译器却不一定能自动指出这类错误。
变量(指针、数组)被创建之后应当及时把它们初始化,以防止把未被初始化的变量当成右值使用。
当心变量的初值、缺省值错误,或者精度不够。
当心数据类型转换发生错误。尽量使用显式的数据类型转换(让人们知道发生了什么事),避免让编译器轻悄悄地进行隐式的数据类型转换。
当心变量发生上溢或下溢,数组的下标越界。
当心忘记编写错误处理程序,当心错误处理程序本身有误。
当心文件I/O有错误。
避免编写技巧性很高代码。
不要设计面面俱到、非常灵活的数据结构。
如果原有的代码质量比较好,尽量复用它。但是不要修补很差劲的代码,应当重新编写。
尽量使用标准库函数,不要“发明”已经存在的库函数。
尽量不要使用与具体硬件或软件环境关系密切的变量。
把编译器的选择项设置为最严格状态。
如果可能的话,使用PC-Lint、LogiScope等工具进行代码审查。
业务逻辑等容易出错的地方建议加上错误处理,并写入日志,以便快速定位出错位置。
条件语句中变量与常量比较时尽量将常量放在前面,出错时编译器会直接报错。例如,不小心写成if(true = bRet),编译时会直接报错,但是如果写成if(bRet = true),那么相对没那么容易找出错误了。
在QT中,由于更多用来设计界面,为了更好地区分控件类型,成员变量的命名需要突出控件类型,建议在变量名的后面加上控件的标识,如QLineEdit* m_pIPLineEdit,QComboBox* m_pTimeIntervalComboBox,QPushButton* m_pQueryPushButton。
在QT中,为了区分信号和槽函数,信号函数名建议用前缀signal_,如signal_currentTaskInfo;槽函数名建议用前缀slot_,如slot_queryIpInfo。
在QT中,有继承关系的类若存在信号和槽函数,必须要在类体中加入Q_OBJECT宏,否则加入的信号和槽将会没有响应。
在QT中,需要注意槽函数的参数与信号参数的匹配问题,信号的参数必须要大于等于槽函数的参数;自定义信号和槽函数若存在自定义参数,必须要用qRegisterMetaType(“”)进行注册,否则信号和槽将没有响应。例如
#include <QMetaType> //注册自定义数据类型DeviceTime qRegisterMetaType<DeviceTime>("DeviceTime");
在QT中,new时没有指定parent的指针用完后必须要释放,释放后应该置为空;而new时指定parent的指针由系统自动回收,不需要程序员释放。