c进阶篇(九)——inline函数用法详解

前言:
  内敛函数可有效降低调用函数的资源开销,堆栈开销和时间开销都能得到降低,但会增加Flash的消耗。本篇文章详细讲解内敛函数在c语言中的运用。

1 什么是内敛函数

  在c/c++中,为了消除一些小函数(处理任务少,运算时间较短的函数),特别是一些调用频次很高的函数调用时带来的大量堆栈空间和进退栈时间的消耗,特别的引入 inline 修饰符在函数定义时放置在返回值关键字之前,请求编译器将其作为内敛函数处理,它对编译器只是提出一种建议,编译器将最终决定是否将其作为内敛函数或普通函数。被作为内敛的函数,编译器在调用处直接将其展开,嵌入到调用处,这样就省去了参数传递、对栈的操作和释放,从而节省了栈和时间开销。由于在每个调用处都会展开和嵌入内敛函数的代码,因此掉调用的地方越多Flash开销越大,对于多处调用的函数作为内敛将会快速消耗Flash空间,这一点与宏函数有些类似。
   inline 是c99标准时加入的关键字。

2 内敛函数和宏函数的区别

2.1 相同

  被定义为内敛的函数将在调用处内敛展开,通过避免函数被调用的开销来提高执行函数的效率,实现类似宏展开的作用,代价是代码迅速的膨胀,更快的消耗Flash空间,因为函数在所有调用处均有一个“备份”,因此内敛函数不应过大。省去了调用函数进出堆栈的时间开销,因此“小函数”调用越频繁作为内敛越“划算”。

2.2 不同

  编译器会对内敛函数进行参数类型检查(做参数类型检查和自动类型转换),而宏只是简单的字符替换,因此使用内敛函数比使用宏函数更加安全。宏函数不能用于调试运行,特别是无法打断点,而内敛函数可以。内敛函数具有返回值,而宏函数没有返回值。内敛函数传参没有宏替换带来的副作用。

3 定义内敛函数

  我们通过实际写代码和调试来探究内敛的作用,对以下代码进行测试。测试平台为MDK-ARM,为了排除编译器自动优化干扰观察结果,将优化等级设置为最低等级 -O0 ,我们进行手动优化。

  • main.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#define LST_LEN 5u

/**
* @fn word CalcTriAr(word wBase, word wHgt)
* @brief 计算三角形面试。
* @details 三角形面积 = 底 * 高 / 2
* @param [in] wBase 底。
* @param [in] wHgt 高。
* @return 三角形面积,只保留整数。
* @note 无
* @attention 无舍弃小数部分。
*/
word CalcTriAr(word wBase, word wHgt)
{
return (wBase * wHgt) / 2u;
}

int32_t main(void)
{
byte byIdx = 0u;
word awLst[LST_LEN][3u] = {{0u, 1u, 0u}, //col0: Num1, col1: Num2, col2: Sum.
{0u, 3u, 0u},
{2u, 8u, 0u},
{5u, 2u, 0u},
{3u, 1u, 0u}};

for(byIdx = 0u; byIdx < LST_LEN; byIdx++)
{
awLst[byIdx][2u] = CalcTriAr(awLst[byIdx][0u], awLst[byIdx][1u]);
}

while(1u);
}

  这段代码内 main 函数在循环内调用了函数 CalcTriAr ,并且循环调用了5次,而 CalcTriAr 函数是一个非常精简的 “小函数“,假设它还在其他地方被频繁调用,如果在所有调用的地方都写一遍三角形公式就显得很臃肿也不利于修改维护,封装为函数又会觉得这样简单的一行表达式每次调用都要进出栈降低执行效率,最合适的方式是将其定义为内敛函数,即保证封装性又保证效率,缺点是增加了Flash消耗,但由于函数本身比较小这个空间消耗是可接受的。
  连接开发板进行调试,观察反汇编代码。

Code.png

  从调试模式可以发现, CalcTriAr 此时作为一个 “普通” 的非内敛函数,对齐调用时需要进行 PUSH 入栈操作, -O0 优化等级下可以在入函数的 { 处打上断点。
  我们将其改为内敛函数再次调试,代码入下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define LST_LEN 5u

__attribute__((always_inline)) word CalcTriAr(word wBase, word wHgt)
{
return (wBase * wHgt) / 2u;
}

int32_t main(void)
{
byte byIdx = 0u;
word awLst[LST_LEN][3u] = {{0u, 1u, 0u}, //col0: Num1, col1: Num2, col2: Sum.
{0u, 3u, 0u},
{2u, 8u, 0u},
{5u, 2u, 0u},
{3u, 1u, 0u}};

for(byIdx = 0u; byIdx < LST_LEN; byIdx++)
{
awLst[byIdx][2u] = CalcTriAr(awLst[byIdx][0u], awLst[byIdx][1u]);
}

while(1u);
}

  这段代码通过使用 __attribute__((always_inline)) 修饰向编译器表达强烈的“建议”,希望编译器将 CalcTriAr 函数作为内敛函数。该指令的作用是强制内敛,但仍然不是绝对的,最终是否将函数作为内敛函数还是要取决于编译器的处理,只是其”建议“的程度比 inline 关键词高。上面的代码为了演示内敛函数,确保内敛成功因此使用了 __attribute__((always_inline)) 修饰而不是 inline 修饰,因为编译器大多数时候都会忽略 inline 关键词,特别是编译器优化等级设置较低时往往不会将函数处理为内敛函数。__attribute__((always_inline)) 只能用于ARM环境下。

attribute.png

  通过调试可以发现,内敛函数的执行不需要进行进出栈操作,断点也不可打在 { 处。

4 参数检查和返回值

  内敛函数和普通函数一样,会对形参列表和返回值进行参数检查和自动类型转换,因此这一点也使得内敛函数的使用笔宏函数更加安全。内敛函数传参没有宏替换带来的副作用,我们都知道宏替换时是有潜在风险的,比如宏函数的参数传入的是表达式时需要加上括号,而内敛函数的参数可以放心的使用表达式。

  • 宏函数
1
2
3
#define Calc(Num1, Num2)             ((Num1) * (Num2)) //宏参数要加括号,防止表达式优先级被篡改。

Add(2u + 3u, 3u);
  • 内敛函数
1
2
3
4
5
6
inline word Calc(word wNum1, word wNum1)
{
return wNum1 * Num2;
}

Add(2u + 3u, 3u);

5 内敛函数的声明和定义位置

  一个全局普通函数可由函数定义和函数声明构成,在编译时编译器只会去查找函数声明,在链接时才会去找函数定义。但对于内敛函数,由于其在编译阶段就将其进行内敛扩展,因此链接阶段不需要找内敛函数的定义,因为此时不存在内敛函数的定义。因此当使用关键字 inline 修饰的函数必须对该函数进行声明。

5.1 .c中定义.h中声明

  • hdr.h
1
2
3
#include "other.h"

extern inline word CalcTriAr(word wBase, word wHgt);
  • main.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "hdr.h"

#define LST_LEN 5u

inline word CalcTriAr(word wBase, word wHgt)
{
return (wBase * wHgt) / 2u;
}

int32_t main(void)
{
CalcTriAr(1u, 2u);

while(1u);
}

  以上代码可成功通过编译,在内敛函数的定义和调用都在main.c源文件中,而声明在hdr.h头文件中。编译阶段成功找到了内敛函数的声明,并且完成了对内敛函数的展开,随后成功通过链接。
  这种方式有缺点,假设此时再增加文件other.c,该文件内也调用了这个内涵函数,代码如下。

  • other.c
1
2
3
4
5
6
#include "hdr.h"

word Fun(void)
{
return CalcTriAr(1u, 2u);
}
  • other.h
1
extern word Fun(void);
  • hdr.h
1
2
3
#include "other.h"

extern inline word CalcTriAr(word wBase, word wHgt);
  • main.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "hdr.h"

inline word CalcTriAr(word wBase, word wHgt)
{
return (wBase * wHgt) / 2u;
}

int32_t main(void)
{
Fun();
CalcTriAr(1u, 2u);

while(1u);
}

  以上代码会报警,提示内敛函数 CalcTriAr 未定义,虽然在头文件进行了声明,但main.c和other.c对对齐进行了调用,而其定义放于源文件main.c中,这时other.c文件内直接是找不到该内敛函数定义的。如果在other.c中也增加 CalcTriAr 内敛函数的定义,就会报重复定义的错。因此.c中定义.h声明的方式,实际外部并不能调用到声明的内敛函数。

5.2 .c中定义和声明

  .c中定义.h中声明并不能让其他文件调用到内敛函数,如果内敛函数只限于当前.c使用,可以将定义和声明都在.c文件内进行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
extern inline word CalcTriAr(word wBase, word wHgt); //在定义的.c中声明。这里的inline可以省略。

inline word CalcTriAr(word wBase, word wHgt)
{
return (wBase * wHgt) / 2u;
}

int32_t main(void)
{
//Fun();
CalcTriAr(1u, 2u);

while(1u);
}

  这种方式仅适用于内敛的定义、声明、使用都在同一个.c文件中,这样设计虽然语法上没有错误,但不符合模块设计的思想。

5.3 .c中定义且static修饰

  按照模块化的设计思想,仅在模块内使用的函数应该限制其作用域为该模块范围,作为静态局部函数。内敛函数若只在某个.c文件中使用,也应该使用 static 关键字进行修饰,这种方式不需要声明(static函数不需要声明)。

  • main.c
1
2
3
4
5
6
7
8
9
10
11
static inline word CalcTriAr(word wBase, word wHgt)
{
return (wBase * wHgt) / 2u;
}

int32_t main(void)
{
CalcTriAr(1u, 2u);

while(1u);
}
  • com.c
1
2
3
4
5
6
7
8
9
static inline word CalcTriAr(word wBase, word wHgt)
{
return (wBase * wHgt) / 2u;
}

word Fun(void)
{
return CalcTriAr(1u, 2u);
}

  以上代码在main.c和com.c中都调用了内敛函数 CalcTriAr ,每个.c中分别定义并且使用 static 修饰为静态内敛函数。这种方式各个模块独立定义和使用自己.c内的内敛函数,内敛函数的作用域被限制仅在该.c内,因此允许.c之间同名。此种方式适用于各个模块独立定义自己的内敛函数(可以同名),对作用域进行限制因此具有更好的封装性。

5.4 .h中定义且static修饰

  上面介绍的几种形式都只能使定义的内敛函数被一个.c使用,如果要一次定义在多个.c中使用,必须将内敛函数定义在头文件中,内敛函数定义在.h中必须使用 static 进行修饰,但其作用域是 include 该.h文件的所有.c文件。在.h文件中定义不需要声明(static函数不需要声明)。

  • hdr.h
1
2
3
4
5
6
#include "other.h"

static inline word CalcTriAr(word wBase, word wHgt)
{
return (wBase * wHgt) / 2u;
}
  • main.c
1
2
3
4
5
6
7
8
9
#include "hdr.h"

int32_t main(void)
{
Fun();
CalcTriAr(1u, 2u);

while(1u);
}
  • other.c
1
2
3
4
5
6
#include "hdr.h"

word Fun(void)
{
return CalcTriAr(1u, 2u);
}

  总结一下,c中使用内敛函数,如果限制为模块内使用则将其定义在.c文件中,这有利于封装性也保密性(如果将.c编译为库文件),如果需要多个.c文件同时调用则将内敛定义在.h中,不论定义在哪种.c还是.h,建议都使用 static 对内敛函数进行修饰,并且不需要声明(static函数不需要声明)。如果要保证保密性但同时要被多个.c调用,那只能在每个.c都定义一次相同的静态内敛函数(允许同名)。