王伟冰
命名
命名其实包括两方面的内容,一个是起什么样的名字,一个是在什么地方定义。通常我们认为,一个变量、函数或类的名字应该能够清晰地表达出它的作用,但是有时候要想出一个合适的名字就很费时间,所以比较实际的做法是,一个变量、函数或类的重要性越大,它的名字就越应该清晰地表达出它的作用。(清晰原则4)衡量重要性有两个方面,一方面是本征重要性:类>函数>变量;另一方面是使用次数,一个变量、函数或类在代码中出现的次数越多,它的重要性就越大。所以从某种角度来读,一种语言内置的库函数的重要性是最大的,因为有无数人的无数代码要使用它。所以像scanf、printf这样的名字简直罪大恶极,每一个学C语言的同学都必须被迫接受这样一些不是单词的单词……呵呵开玩笑啦。
大型项目需要避免的一个重要问题就是同名问题。解决同名的一个方法就是减少全局变量。比如你在程序中用到100个坐标,你可以这样定义:
int x[100];
int y[100];
也可以这样定义:
struct point{ int x,y; };
point p[100];
把关联密切的一组变量组合到一个结构或类里面去,这样就减少了全局变量。
有些变量根本没必要是全局变量,比如这样:
int t;
void swap(int& a,int& b){
t=a,a=b,b=t;
}
完全可以把t放到swap里定义。
另一个方法是使用命名空间。比如:
namespace A{
int x;
void f(){}
}
namespace B{
int x;
void f(){}
}
那么A::x和B::x就代表不同的变量,A::f()和B::f()代表不同的函数。在A中使用x变量就是使用A::x变量。通常,我们把一组功能较为接近,关联较为密切的变量和函数放到同一个命名空间里。我本人喜欢把我自己写的代码放在一个以我的名字缩写为名的命名空间里。
还有一种方法是使用静态变量和静态函数。有些全局变量(或全局函数)只和某个类有关,可以把它定义为这个类的静态变量(或静态函数)。比如第四节的sphere类中,用到了PI常量,我们发现这个PI常量只在sphere类用到,在其它代码中不会用到。为了防止其它代码中也定义了这个变量而产生冲突(尤其是在合作编程中,每个人定义自己要的东西,合到一起编译时就冲突了),可以把这个量定义为sphere类的静态变量:
class sphere{
public:
static const double PI; //声明
……
}
const double sphere::PI=3.14159265; //定义
静态变量在类中声明之后,还需要在类外定义。
声明为静态变量后,外界要访问PI,就必须写成sphere::PI。如果PI在类中是声明为private的,那么外界根本就无法访问PI。
但是,静态变量本质上还是一个全局变量。无论这个类创建了多少个实例,静态变量都只有一个。只不过是它的可访问性与全局变量不同而已。
静态变量不必要是const的,上面只是一个例子而已。还可以定义静态的函数:
class sphere{
public:
static double PI(){
return 3.14159265;
}
……
}
同样,静态函数本质上还是全局函数,只是要访问它得用sphere::PI()而已。
综上可看出,通过减少全局变量,使用命名空间和静态成员,可以避免命名冲突,同时使代码之间的关系更为清晰。(清晰原则5)
可能你注意到了,上面我多次地强调“功能相近”,“关联密切”之类的词语,把功能相近、关联密切、调用层次相近的函数放到同一个类、同一个命名空间或同一个文件里,可以使整个程序的架构清晰。(清晰原则6)相反,如果关联不太密切的函数被乱七八糟地放在一起,那程序就显得混乱。调用层次可以这样理解:A调用B,那么A的调用层次比B高。所以在一个程序里,main函数的层次最高,汇编指令的层次最低。
陷阱
陷阱就是指那些容易引起错误的语言细节。下面列举一些常见的C++陷阱:
1. 把“==”误写成“=”。
2. 某些运算符的优先级,如*p++到底是(*p)++还是*(p++)?没把握时还是加括号吧。
3. 运算可能会溢出,比如int型20亿加20亿会得到一个负数。
4. 在switch里一个case完了忘了加break,结果继续到下一个case。
……
总之,写代码时注意避免语言陷阱。(安全原则7)
有些错误其实不是语言的问题,而是程序员自己的疏忽,像这个求和函数:
int s=0;
int sum(int a[],int n){
for(int i=0;i<n;i++)
s+=a[i];
return s;
}
每次调用sum的时候没有对s进行重新初始化,导致每一次求和的结果都积累到下一次。所以,每次进入函数调用或进入循环体的时候,要记得对相关的变量重新初始化。(安全原则8)对于上面的例子,把s声明为局部变量就可以解决问题,但有时有些变量必须是全局变量,所以就要重新给它们赋初值。
异常
异常通常是指一些程序无法控制的意外事件,比如用户指定打开一个文件,但是磁盘上却没有这个文件;用户指定要访问某个网络上的资源,但是断网了。一个健壮的程序应该能够对这些意外事件做出恰当的处理。
考虑一个简单的程序:输入一个数,输出它的倒数。那么我们得处理用户输入是0的情况,可能有三种处理方式,一种是断言(assert):
double reciprocal(double x){
assert(x!=0);
return 1/x;
}
void main(){
while(true){
double x;
cin>>x;
cout<<reciprocal(x)<<endl;
}
}
一种是异常机制(try-catch):
double reciprocal(double x){
if(x==0)throw 1; //抛出变量1
return 1/x;
}
void main(){
while(true){
double x;
cin>>x;
try{
cout<<reciprocal(x);
}
catch(int flag){ //捕获变量1
if(flag==1)cout<<”0没有倒数!”;
}
}
}
一种是返回错误代码:
bool reciprocal(double x,double& y){
if(x==0)return 1; //失败返回1
y=1/x;
return 0; //成功返回0
}
void main(){
while(true){
double x,y;
cin>>x;
if(reciprocal(x,y))cout<<”0没有倒数!”;
else cout<<y;
}
}
我们看到,用断言最简单,但是最不负责,因为一输入0程序就崩溃了。断言适合用来检查程序潜在的bug,但是不适合用来处理异常情况。异常情况并不是bug,而是由于程序无法控制的外部因素引起的,比如用户的非法输入,比如操作系统或硬件的不支持,等等。所以处理异常情况还是要用异常机制或错误代码的方式。
异常机制的好处是,不破坏原有函数的结构,而且可以跨越多层函数调用,而缺点是try-catch这样的语句写起来比较烦琐,而且你在一个函数里throw就要保证上层代码要有try-catch,如果上层没有的话,程序还是会崩溃。尤其是合作编程时,如果没有事先约定,有的人在自己的代码里throw,其它人不知道,没有用try-catch处理,那就会出问题。
返回错误代码的好处就是处理起来比较方便,而且你不处理的话程序也还照样运行。但是返回错误代码破坏了原有函数的结构,为了返回一个bool值,原来的返回值只能变成一个引用参数double& y。还有就是无法跨越多层调用,假设A调用B,B调用C,C调用D,然后D中检测到一个异常情况,为了在A中处理这个情况,必须把错误代码一层一层传上去,这就太麻烦了。
TopLanguage上有过不少关于这两者的争论,我觉得两种方式各有优缺点,选择哪一种就取决于个人喜好了。不过,在一个团队里一定要统一,不能有的人用这种,有的人用那种。总之,要运用异常机制或错误代码的方式,来处理可能遇到的异常情况。(安全原则9)