王伟冰
在C程序里我们会常用到宏定义,比如导言中PI的例子,也可以写成宏定义:
#define
PI 3.14159265
如同导言所讨论的,宏定义也有同样的好处:简化书写、便于修改、便于理解等。然而,正如许多C/C++书籍所强调的,宏定义不安全,因为它只是执行简单的文本替换。比如#define
f(x)
x*x,那么f(1+2)会变成1+2*1+2而不是(1+2)*(1+2)。所以我们不提倡在代码中随便使用宏。但是,我们依然需要运用宏的思想,来做到简化书写、便于修改、便于理解的目的。C++的许多语言特性运用到了宏的思想,同时又避免了宏的不安全性:
1.
const常量
这个我们在导言中已经讨论过。
2.
typedef
比如我们要使用C++内置的复数类,完整的名称应该是std::complex<double>,这样写显然太麻烦,或许可以用宏:
#define
comp std::complex<double>
然而这样可能不太安全,更保险的是用typedef:
typedef
std::complex<double> comp;
表明comp类型是std::complex<double>的别名。
3.
内联函数
比如我们在代码中多次用到三次方运算,我们可以定义一个宏:
#define
cube(x) ( (x)*(x)*(x) )
为了避免上面所提到的安全问题,我们加了好几个括号。但是这样并不代表着就完全没问题了,因为宏不会对输入参数进行类型检查,更好的方法是定义内联函数:
inline
double cube(double x){
return x*x*x;
}
内联函数和函数有什么区别?看起来就是多了个inline修饰符而已。但是内联函数在运行时是不存在的,它只是在编译的时候把调用cube(x)的地方替换成x*x*x而已,跟宏一样,只不过它会进行参数的类型检查,而且它保证正确运算次序,不需要像宏一样加括号才能保证。
内联函数比起函数的好处就是性能,因为函数的调用需要进行参数的复制、控制的跳转、返回值的复制等多余操作,而内联函数只是简单的语句替换,所以它的性能会更好。
4.
sizeof
sizeof语句常用于内存分配的语句中:
int*
p=(int*)malloc(sizeof(int)*10); //分配10个整数的空间
我们明明知道sizeof(int)是4,但还是要写成sizeof(int),一方面是使表意清晰,另一方面是使代码可移植,如果我们换了一台机器来编译,说不定sizeof(int)不再是4了,而我们用sizeof不需要做任何更改。如果我们直接写成4,那么移植的时候就要把每个4都改成别的,麻烦。
5.
枚举
枚举常用于描述选项之类的信息,比如第四节的立方体与球体求体积,也可以这样实现:
enum
shape{ cube,sphere };
class
object{
public:
shape type; //标志物体属于哪种形状,有两个选项:cube和sphere
double length; //如果是立方体则表示边长,是球体则表示半径
double volume(){
switch(type){
case cube: //如果是立方体
return length*length*length;
case sphere: //如果是球体
return 4*PI*length*length*length/3;
}
}
};
枚举值本质上就是整数值,上面的cube实际上就是0,而sphere就是1。下面的代码和上面的本质上是一样的:
int type;
double volume(){
switch(type){
case 0: //如果是立方体
return length*length*length;
case 1: //如果是球体
return 4*PI*length*length*length/3;
}
}
既然直接写0和1就可以解决问题,那为什么还要使用枚举呢?因为枚举含义清晰,比较好记,上例中的枚举只有两个可能值,假如说有10个可能值,那你就得时时刻刻记住哪个数对应哪个意思,稍有不慎就会弄错。还有,枚举可以随意排列,把上面的定义语句改成enum
shape{ sphere,cube
},其它代码都不需要修改;但是如果直接用数字,你想让0表示球体,让1表示立方体,那就所有代码都要改。
6.
条件编译
条件编译常用于程序版本的控制,比如说,你希望你的程序在测试版本中输出一些中间信息,以检查程序运行过程是否有问题,而在正式版本中你不想要这些信息,你可以一条一条手工地删除这些代码,但是这样做太麻烦,所以可以用条件编译:
#define
TEST
……
#ifdef
TEST
cout<<信息1;
#endif
……
#ifdef
TEST
cout<<信息2;
#endif
当你不想输出这些信息的时候,只要把#define TEST这一句注释掉就可以了。
综上所述,我们得到了宏思想原则:运用宏、常量、typedef、内联函数、sizeof、枚举、条件编译之类的语言特性,可以使程序便于书写,便于修改,便于理解。
而从另外一个角度来看,宏思想体现了一个原则:将需要经常改动或将来有可能改动的因素所对应的代码量减到最少。(灵活原则7)
然后再来讲语法糖。什么是语法糖?就是那些没有为计算机语言增加新功能,而只是用来简化代码书写的语法。比如说,我们可以用p[i]来表示*(p+i),这样的写法更为简洁和清晰,但并没有增加什么实际的功能。又如运算符重载,c=a+b和c=add(a,b)并没有本质区别,只是看起来更清晰明了而已。
这一次我不用C++作为例子,而用C#,因为C#有很多语法糖。但是在这里我只是展示一下这些语法糖的效果,而不会具体地讲它们如何实现。如果你对它感兴趣可以自己去查MSDN。
1.
属性
假设你在一个类中储存有月份的信息,你不能让外部代码直接去设它的值,因为你要确保月份的值在1到12之间,那么在C++里你可以这样做:
int
month;
public:
int getMonth(){
return month;
}
void setMonth(int m){
assert(m>0 &&
m<13);
month=m;
}
那么你在用的时候就必须这样:
int
m=a.getMonth(); //获取月份
a.setMonth(10); //设置月份
这时我们称Month为一个属性,属性就是提供一个get函数和一个set函数去约束对一个私有变量的访问。如果提供get函数,则称为只读属性。在C#里,提供了用于专门定义属性的语法,使得你可以这样使用属性:
int
m=a.Month;
a.Month=10;
看起来Month好像是一个成员变量,但它实际上还是一个get函数和一个set函数。
2.
索引器
假设Student类用来储存一个学生的信息,而Class类用来储存一个班的信息,class1是Class类的一个实例。那么Class类里应该有一个Student数组,假设数组名为students,为了获取这个班的第一个学生,我们应该这样做:
Student
stu=class1.students[0];
C#提供了一索引器的语法,定义了索引器之后你可以这样做:
Student
stu=class1[0];
就好像重载[]运算符一样。还可以这样做:
Student
stu=class1[“张三”];
可以找出名为张三的学生。而且索引器参数还可以不止一个:
Student
stu=class1[1,3];
找出坐在第1排第3列的学生(假如我们记录了每个学生的座位)。
索引器本质上也是一个get函数和set函数:
Student
stu=class1[1,3]; //相当于class1.get(1,3)
class1[1,3]=stu; //相当于class1.set(1,3,stu)
3.
扩展方法
比如在C++中,你定义了一个print函数来输出一个整数:
void
print(int x){
cout<<x<<endl;
}
然后你使用这个函数:
print(10);
C#提供了一种定义函数的语法,使得你可以这样调用它:
10.print();
仿佛print是int的成员函数,但本质上并不是。
4.
可变个数参数
考虑一个求和函数,在C++里你可以这样:
int
sum(int a[],int n){
int s=0;
for(int i=0;i<n;i++)
s+=a[i];
return s;
}
然后你在代码中使用这个函数:
int
a[3]={1,2,3};
int
s=sum(a,2); //得到3
s=sum(a,3); //得到6
C#提供了一种语法,能够定义参数个数可变的函数,然后你就可以这样:
int
s=sum(1,2); //得到3
s=sum(1,2,3); //得到6
传任意多个参数都可以。这种函数本质上是按照输入的参数生成一个数组,然后把这个数组传给函数。
5.
var关键字
var关键字可以使你定义变量的时候不需要写类型。比如:
var x=10;
//x就是int型
var
x=”abcd”; //x就是string型
有些类型的全称很长,用var就可以简化书写。
C++新标准提出auto关键字,和var有同样的功能,比如:
vector<int> vec;
auto
iter=vec.begin(); //相当于vector<int>::iterator
iter=vec.begin();
除了这里提及的,语法糖还有很多,包括其它语言里的。语法糖可以简化书写,这自然的好事,但是请不要太过依赖语法糖,真正决定编程质量和效率的,是整个程序的架构设计是否合理。也不要凭语法糖多少去评价一种编程语言的好坏。最后总结一下,应当充分利用语法糖来简化书写,但也不要太过依赖。(简洁原则5)
|