首页 > 技术文章 > C Program基础-宏定义

z-joshua 2017-03-21 14:16 原文

  写好c语言,漂亮的宏定义是非常重要的,我们在阅读别人工程时,往往能看到大量的宏定义,宏定义可以增加代码的可读性,也能增加代码的可移植性,一个好的宏定义甚至是一件艺术品。今天我们就来看看宏定义的方方面面。

(一) 宏 vs 函数

在软件开发中过程中,经常有很多重复使用的代码段或功能模块,这些功能既可以用函数实现,也可以用宏实现,那么究竟是用函数好还是宏定义好呢?这就要求我们对二者做合理的取舍。

我们来看下面的例子,求两个数的较大者:

(1)宏定义:

#define Max(a, b) ((a) > (b)? (a):(b))

(2)函数实现:

int max(int a, int b)
{
    return a > b ? a : b;
}

一般来说我们不会选择用函数实现,原因有两个:

首先,函数调用会带来额外的开销,调用需要开辟栈空间,将形参压栈,记录返回地址,并释放栈空间,而使用宏定义则在代码规模和速度方面都比函数更胜一筹;

其次,函数定义时需要指定参数类型,比如上面的函数只能对int运算,对于其他的类型long,float, double等,每一种都需要写一个函数,反之,上面的那个宏定义可以用于整形、长整形、单浮点型、双浮点型以及其他任何可以用“>”操作符比较值大小的类型,也就是说,宏是与类型无关的。

我们再来看一个宏定义:

#define MALLOC(type) (type *)malloc(sizeof(type))

这个宏定义是我们可以对任何类型分配堆空间,这个用函数是很难实现的。

但是,并不是说宏定义就比函数好,定义宏的时候有很多地方需要注意:

 

(二)宏定义的陷阱

(1)不能忽视宏定义中的空格

我们看宏定义: #define f(x) ((x) - 1)  假如我们不小心写成 #define f (x) ((x) - 1) ,此时编译是无法通过的,编译器把f 替换成了(x) ((x) - 1),因此宏定义时不能忽视空格,但是宏调用时却没有关系:

对于上面的宏定义 #define f(x) ((x) - 1) ,我们可以这样来调用:

void main()
{
    int a = 10;
    int result = f    (a);
    printf("result: %d\n", result);
}

(2)宏定义时不能吝啬括号

我们常常看到如下的宏定义

#define abs(a) ((a) >=0 ? (a) : -(a))

在宏定义中,使用了大量的括号,这些括号的作用是预防引起与优先级有关的问题,

假如我们把abs定义成:

1 #define abs(a) a >=0 ? a : -a

这个宏定义在运算abs(-10)时是没有问题的,但假设我们有如下宏调用:

void main()
{
    int a = 10;
    int b = 20;
    int result_abs = abs(a - b); 

    printf("result_abs: %d\n", result_abs);
}

程序输出的结果是: result_abs: -30 

我们把宏展开就能看出其中的端倪  abs(10 - 20) // 10 - 20 >=0 ? 10 - 20 : - 10 - 20  ,这里的表达式 -10 - 20并没有按照我们所期望的 - (10 - 20)展开,所以造成了上面的错误,

我们再来看另一个宏定义: #define sum(a, b) a + b 

当我们有如下宏调用时就会出错 int result_sum = sum(3, 5) * sum(2, 3); 因为宏展开变成了 3 + 5 * 2 + 3,由于*优先级高于+,所以程序输出 result_sum: 16 

我们将宏改为 #define sum(a, b) ((a) + (b)) 就可以避免出现上面的错误。

因此我们在宏定义是要做的是在每一个参数以及整体表达式都加上括号

(3) "++" "--"在宏定义时引发的错误

即使我们在每个参数及整体表达式都加上括号,"++", "--"引发的错误仍然难以避免

我们定义一个max宏: #define max(a, b) ((a) > (b) ? (a) : (b)) 

有如下的调用:

void main()
{
    int arr_number[3] = { 10, 30, 20 };
    int biggest = arr_number[0];
    int i = 1;
    while (i < 3)
    {
         biggest = max(biggest, arr_number[i++]); 
    }
    printf("The biggest number in array is %d\n", biggest);
}

最后程序的输出是: bigest number in array is 20 为什么会输出20呢?我们期望的是30。

我们来分析下程序运行的过程:

bigest初始化为10,进入while循环,i = 1,运算表达是 bigest = max(bigest, arr_number[i++]); 我们将这里的max宏展开,

上面的表达式等价于

biggest = ((biggest) > (arr_number[i++]) ? (biggest) : (arr_number[i++]))

我们发现i自增了两次,下一次循环进入时i = 3,跳出了循环,输出的20是最后一次自增前的值,即arr_number[2].

这里的解决办法有两个 (a) 将max定义为函数, (b)宏调用时不使用 i++。

 (4)宏不是类型定义

为了增加程序的可移植性,我们会如下定义宏:

#define Student1 struct student1{ \
    char id[5]; \
    char name[10];\
    int age;\
    char sex[2];\
}

我们也可以用typedef来定义一个新的类型:

typedef struct student2
{
    char id[5];
    char name[10];
    int age;
    char sex[2];
} student2;

这两种命名方式看似差不多,但是typedef的方式更通用一些,考虑下面的代码:

#define ST_P1 struct student1*
typedef student2 *ST_P2;

void main()
{
    ST_P1 stp1_a, stp1_b;
    ST_P2 stp2_a, stp2_b;
}

当我们试图用ST_P1和ST_P2来声明多个变量时,问题就来了,

stp1_b并不是指向student1结构的指针变量,而是student结构变量,我们把宏展开看看就很清楚:

struct student1 *stp1_a, stp1_b

这个语句a被定义为指向结构的指针,而b被定义为结构

因此我们最好用typedef来定义类型,而不是#define

 

(三)经典宏定义

(1)assert宏

assert宏的原型定义在<assert.h>中,其作用是如果它的条件返回错误,则终止程序执行,我们常常这样使用assert宏:

void main()
{
    int* p = NULL;
    p = (int *)malloc(sizeof(int));
    assert(p != NULL);
... }

 如果让我来定义assert宏,我可能会定义成如下的形式:

#define assert(exp) if(!exp) _assert(#exp, __FILE__, __LINE__)

上面的宏使用的是语句,在某些情形下,这个宏有一些难以察觉的错误,假如我们有如下的宏调用:

if(x > 0)
    assert(y > x);
else
    assert(x > y);
...

我们把宏展开:

if(x > 0)
    if(!(y > x))) _assert("y > x", __FILE__, __LINE__);
else
    if(!(x > y)) _assert(" x > y", __FILE__, __LINE__);
...

由于else默认是跟离它最近的if匹配,我们重新缩进下:

if(x > 0)
    if(!(y > x))) _assert("y > x", __FILE__, __LINE__);
    else
        if(!(x > y)) _assert(" x > y", __FILE__, __LINE__);
...

这和我们想要的结果是不同的,

我们再来看看系统如何定义assert宏:

#define assert(exp) (void)((exp) || (_assert(#exp, __FILE__, __LINE__), 0))

这个宏是一个表达式,不是语句,利用了||语句的短路现象,||前面的表达式为真时,后面的不再计算,当||之前为假时,才处理后面的表达式;

宏中有个#exp,这里的#号是提取exp的值;

表达式最后有一个0处于逗号运算符之后,这样处理确保整个表达式最后值为0,即为false。

推荐阅读