大家可能感觉宏比较简单,所以大家往往容易忽略宏的存在,实际上在很多的复杂C源码中,
往往可以经常看到各种宏的比较深入的用法,所以我们这里举一些宏的比较高级的、比较深入的用法的例子

举这些例子的目的有三个:

  1. 提高对宏的认识,希望大家能够适应C源码中宏的深入用法
  2. 向大家如何分析这些比较复杂的宏,当自己遇到类似比较复杂的后,能够自己去分析这些宏
  3. 希望大家也能够学会在自己的代码中,按照类似的方式去模仿宏的比较深入的用法,提高自己的代码质量

一、简短函数

#include <stdio.h>

void exchange(int *p1, int *p2)
{
    int tmp = 0;

    tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}

int main(void)
{
    int a = 10;
    int b = 30;

    exchange(&a, &b);

    printf("a=%d, b=%d\n", a, b);

    return 0;
}

1. 例子中 exchange 函数就是一个简短函数

那么什么是简短函数呢,如何判断一个函数是不是简短函数呢?

1) 代码只有1~5行左右

2) 函数中没有循环
因为如果有循环的话,也相当于有很多的代码,不过如果你的循环要是非常短的话,比如只循环3~4次,
累计的代码量也就只有5行左右的话,也算是简短函数,不过一般来说,我们并不把有循环的函数算作是就简短函数

2. 简短函数缺点:调用开销比较大

1)时间开销:调用时跳转到被调函数处执行,函数执行完毕后,返回到调用处,这些都是需要时间的

2)空间开销:调用函数时,往往需要在栈中为形参开辟空间,所以有空间开销
而且开辟和释放形参的空间,也是需要时间的,也有时间开销。

所以对于简短函数来说,函数调用的开销甚至都有可能大于那1~5行代码的运行开销,
所以说如果你在程序中有大量的简短函数的话,会非常影响你的程序质量,
特别是当这个简单函数会被频繁调用时,累积的开销就更大了,所以这个时候就可以使用“带参宏”来代替了


二、使用带参宏来代替简短函数

文件:helloworld.c

#include <stdio.h>

#define EXCHANGE(p1, p2) \
        int tmp = 0;\
        tmp = *p1;\
        *p1 = *p2;\
        *p2 = tmp;\


int main(void)
{
    int a = 10;
    int b = 30;

    EXCHANGE(&a, &b);

    printf("a=%d, b=%d\n", a, b);

    return 0;
}

查看预编译结果:gcc -E helloworld.c -o helloworld.i

......
# 10 "main.c"
int main(void)
{
    int a = 10;
    int b = 30;

    int tmp = 0; tmp = *&a; *&a = *&b; *&b = tmp;;

    printf("a=%d, b=%d\n", a, b);

    return 0;
}

注意看替换结果:

EXCHANGE(&a, &b) ————>  int tmp = 0; tmp = *&a; *&a = *&b; *&b = tmp;;

宏展开后,代码直接成为了main函数的一部分,不存在函数调用的过程,省去了函数的调用开销

使用宏来实现时可以不使用指针,不过用了也没错

总之为了效率着想,大家完全可以使用宏来代替简短函数,特别是当程序非常大时,这是很有意义的


三、带参宏缺点

注意:我们说的只是简短函数使用宏代替,不要什么函数都使用宏来代替,如果都使用宏来代替的话,
会导致程序的代码量急剧上升,代码变大了自然就需要更多的内存空间来存储,这也会带来很大的空间开销

1. 为什么代码量会急剧上升?

因为所有引用这个宏的地方都会进行宏展开,每个引用宏的地方都会重复包含一份完全相同的代码,
程序的代码量自然会急剧上升,所以什么事都不能走极端,走了极端就出麻烦

2. 使用宏来代替简短函数,参数类型不检查

预编译时,宏的参数只是做简单的替换,而不做类型检查,也就是不检查实参类型与形参类型对不对

为什么宏不做类型检查?

因为宏的形参就没有类型,自然没办法进行类型检查,假如你引用EXCHANGE时,你写成了EXCHANGE(100, 100)
此时实参的类型是int,并不是宏体所需要的指针类型,这显然是有问题的,但是预编译时不会进行类型检查,只是简单替换

但是函数的形参有类型说明,所以编译时会检查函数的实参与形参的类型是否匹配,类型检查其实是很有用的,
因为编译时的类型不匹配的提示信息,非常有利于我们排查编译错误


四、内联函数

宏只做替换,不做类型检查,函数会做类型检查,但是不做替换(函数只能调用),为了将二者的特点融合下,
后来就有了“内联函数”,内联函数的特点是

  • (a)有函数的特性:内联函数的形参有类型,会进行类型检查
  • (b)有宏的特点:内联函数和宏一样,也是一个替换的过程,不存在函数调用

说白了内联函数就是一个宏和函数的特点相综合后的产物,所以对于简短函数来说,最好的方式是使用内联函数来实现

Linux内核源码中,会经常看见内联函数这个东西,因为Linux内核必须考虑效率问题,
所以几乎所有会被频繁调用的简短函数,都使用内联函数来实现,不过有关内联函数这个东西,我们讲函数时在详细介绍

实际上你只要注意不要把参数类型弄错了,使用带参宏来替代简短函数,其实也是很好的,在很多源码中,
经常会看到这样的用法,因为内联函数也有一个问题,那就是它依赖于编译器的支持

因为内联函数相对来说算是一个比较新的C语法特性,有些老旧的编译器不一定支持,
但是你使用带参宏肯定是没问题的,因为宏是一个老的C语法特性

不过内联函数的出现也有好些年了,所以现在的编译器应该几乎都支持了