一、函数的缺点

在前面介绍过,当代码写复杂后,一定会封装出大量的函数,这会导致两个问题:

1. 函数越多,栈的消耗也越厉害

疑问:为什么代码复杂了、函数变多了,栈消耗的就很厉害?

答:因为这会导致函数的调用深度可能会很深,比如:

fun1 --> fun2 --> fun3 --> fun4 --> fun5 ---> ...

在这些函数都没有返回之前,所有函数所消耗的栈空间,将一直不会被释放,
如果复杂程序还有大量使用线程的话,线程函数还会占用栈空间

2. 函数的调用过程,会多花费更多额外的时间

调用函数时,除了函数代码本身执行的时间外,还需要花费额外的时间,比如:

  • (1)从调用位置跳转到函数代码处
  • (2)从栈中开辟空间,将形参、自动局部变量、返回地址压栈
  • (3)利用返回地址返回,以及返回时的弹栈

这些都是需要额外时间的,特别是如果这个函数的调用非常频繁的话,累积花费的就更多

所以,对于那些被频繁调用,而且“代码很简短”的函数来说,
我们往往就使用“带参宏”和“内联函数”代替,以减少这类函数的数量,如此一来:

  • (1)函数减少了,在一定程度上节约了栈内存
  • (2)消除了函数调用所需的额外时间,效率更高

二、使用宏来代替函数

事实上我们在第二章讲宏时就介绍过,可以通过宏的来代替简短的函数,以减少程序中的函数的数量。

参考:《C高级 - 宏深入用法例1 - 宏来代替简短函数

例子:

#include <stdio.h>

int my_max(int a, int b)
{
    a *= 2; // a = a * 2
    b /= 3;
    return (a>b) ? a : b;  //找出最大值
}

int main(void)
{
    int ret = 0;

    ret = my_max(25, 30);

    printf("ret = %d\n", ret);

    return 0;
}

如果my_max调用非常频繁的话,做成函数形式,其实是非常不划算的,所以完全可以使用带参宏来代替

#include <stdio.h>
#define MY_MAX(a, b, ret)  \
ret = ((a*2)>(b/3)) ? (a*2) :(b/3)


int main(void)
{
    int ret = 0;

    MY_MAX(25, 30, ret);
    printf("ret = %d\n", ret);

    return 0;
}

进行宏替换后,main函数就变为了

int main(void)
{
    int ret = 0;

    ret = ((25*2)>(30/3)) ? (25*2) :(30/3);  //找出最大值
    printf("ret = %d\n", ret);

    return 0;
}

使用宏来代替后,“宏体”就变成了“引用者”的一部分,
如此一来不仅节约了栈空间,也免去了函数调用时的额外开销

我们在《第二章》介绍带参宏时就说过,带参宏的参数只用于替换,不涉及参数类型的检查,
所以如果我们把参数写错了,在预编译进行宏替换时,是不会提示错误或者警告的,这不利于代码排错


三、内联函数

1. 什么是内联函数?

内联函数既不是函数也不是宏,它一个兼具宏和函数共同特点的

(1)具有宏的特点,会像宏一样进行替换

不过宏是在“预处理阶段”进行替换的,而内联函数则是在“编译、链接”阶段进行替换的,
替换的过程也被称为“内联”的过程,所以才被称为“内联函数”

(2)也具有函数的特点,会像函数一样进行参数类型检查

内联函数就是会像函数一样进行“参数类型检查”的带参宏。

2. 内联函数举例

(1)inline关键字
这个是内联函数所使用的关键字,标记了这个关键字的函数就是内联函数,
不过只有标记在“函数定义”时才是有效的,至于声明,标不标记inline都无所谓

由于内联函数和“宏”有点类似,而宏经常放在.h中,所以我们一般习惯于将“内联函数”的定义放在.h

如果将函数放在.h中,而且函数如果是extern的话,会报重复定义的错误,
所以我们往往需要将其修饰为static

(2)例子
文件:inline.h

#ifndef H_A_H
#define H_A_H

static inline int my_max(int a, int b) //inline只有修饰函数定义时才有效
{
    a *= 2;
    b /= 3;
    return (a>b) ? a : b;  //找出最大值
}

#endif

文件:a.c

#include <stdio.h>
#include "inline.h"

int main(void)
{
    int ret = 0;

    ret = my_max(25 ,30);

    printf("ret = %d\n", ret);

    return 0;
}

凡是要使用这个内联函数的.c,只需要包含这个inline.h即可

不过大家要小心,如果.c恰好有定义与“内联函数”同名的函数的话,
不管这个函数普通函数还是“内联函数”,编译会报错的,所以编程时不要编写与内联函数同名的函数

(3)检查预编译后的.i文件

gcc -E a.c -o a.i

文件:a.i

......
inline int my_max(int a, int b)  //预编译后,这个玩意任然还在
{
    a *= 2;
    b /= 3;

    return (a > b) ? a : b;
}

int main(void)
{
    int ret = 0;

    int (*funp) = my_max;

    ret = funp(25, 30);

    return 0;
}

预编译后内联函数仍然还在,所以内联函数这玩意并不在“预编译阶段”被处理


四、内联函数只是建议进行替换

指定了内联函数后,只是建议进行替换,到底会不会进行替换,这个需要看编译器,
如果编译器判断存在如下情况的话,就算指定了inline关键字,但是编译器也只当做是普通函数

  • (1)函数代码量很大,不够简短,代码超过5句以上时,就不简短了
  • (2)不是通过函数名调用的,而是通过“函数指针”来调用的
    int (*funp) = my_max;
    ret = funp(25, 30);
    
  • (3)函数是递归
  • (4)函数中使用switchwhilefordo while

五、编译时无法识别inline关键字,怎么办

在解释原因之前,先说说c标准

1. C标准

为了标准化C语法,所以制定了C标准,目前的C标准有4个版本

  • c891989 年制定
  • c901990 年制定
  • c991999 年制定
  • c112011 年制定

其实中间还有c94/c95标准,不过这两个可以忽略,
c89c90其实是一个标准,为什么一个标准有两个名字,这个是由于历史原因导致的

一般来说,制定新标准时,一些旧的语法可能会被修改或者抛弃,然后再添加一些新的语法特性,
inline则是从c99才开始支持的语法特性,所以目前只有c99/c11才支持inline

  1. 为什么有些编译器在编译内联函数时,会有问题呢

说明你的编译器版本比较老,老版本默认是按照c89/c90去编译的,自然不认识inline
所以我们需要给gcc指定-std=c99或者-std=c11选项,明确的告诉编译器,
请使用c99c11标准去编译C程序,这是就没问题了

比如:

gcc a.c -std=c99  //或者c11

如果你想查看.s的话,

gcc -S a.c -o a.s -std=c99  //或者c11