分享

《高效编程十八式》(5/13)宏思想与语法糖

 叹落花 2015-07-23

 

宏思想与语法糖

王伟冰

    在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

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多