skaiuijing

笔者在阅读一些开源代码时,发现很多头文件都会使用static inline关键字,以linux内核的list.h函数为例:

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

/**
* list_add - add a new entry
* @new: new entry to be added
* @head: list head to add it after
*
* Insert a new entry after the specified head.
* This is good for implementing stacks.
*/
static inline void list_add(struct list_head *new, struct list_head *head)
{
__list_add(new, head, head->next);
}

/**
* list_add_tail - add a new entry
* @new: new entry to be added
* @head: list head to add it before
*
* Insert a new entry before the specified head.
* This is useful for implementing queues.
*/
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
__list_add(new, head->prev, head);
}

这不禁让笔者产生了好奇,static笔者一般是用来修饰变量或函数限定作用域实现封装功能,inline一般是为了效率考虑,建议编译器选择展开函数,而不是直接ldr调用,那么static inline是什么用法呢?

先放结论:static是为了防止重定义错误,因为该函数定义在头文件,编译时会被替换到引入的源文件中,不使用static关键字,那么会出现多个函数重定义问题。至于inline,就是为了展开该函数。

现在我们知道这与编译有关,但是在讲解编译的基本知识前,读者不妨做个小实验验证一下结论:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

headfun.h

//
// Created by el on 2024/9/8.
//

#ifndef TRY_CONFIG_H
#define TRY_CONFIG_H

inline int config1()
{
int a = 1;
return a;
}


#endif //TRY_CONFIG_H

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
one.c

//
// Created by el on 2024/9/8.
//

#include "one.h"
#include "config.h"

int firsttry()
{
int a =config1();
return a;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
one.h

//
// Created by el on 2024/9/8.
//

#ifndef TRY_ONE_H
#define TRY_ONE_H
int firsttry();


#endif //TRY_ONE_H

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
two.c

//
// Created by el on 2024/9/8.
//

#include "two.h"
#include "config.h"

int twotry()
{
int a =config1();
return a;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
two.h

//
// Created by el on 2024/9/8.
//

#ifndef TRY_TWO_H
#define TRY_TWO_H


int twotry();

#endif //TRY_TWO_H

程序运行,就会报重定义错误,对headfun.h文件进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
headfun.h

//
// Created by el on 2024/9/8.
//

#ifndef TRY_CONFIG_H
#define TRY_CONFIG_H

static inline int config1()
{
int a = 1;
return a;
}


#endif //TRY_CONFIG_H

此时报错就会消失。

c程序在被编译时,会先进行预处理,把源文件中的头文件声明换成对应头文件内容。这就是static存在的意义-限制函数作用域,防止重定义。

预处理器详细流程如下:

C/C++预处理器(Preprocessor)是C/C++编译过程的第一步。它主要负责处理源代码文件中的预处理指令。预处理指令通常以#开始,例如#include#define#if等。预处理器的工作流程可以分为以下几个主要步骤:

  1. 宏定义替换:预处理器会查找所有的宏定义,然后替换程序中的宏。
  2. 头文件处理:对于#include指令,预处理器会将指定的文件内容插入到该指令所在的位置。
  3. 条件编译:预处理器会根据#if#ifdef#ifndef#else#elif#endif指令,决定是否编译代码的特定部分。
  4. 移除注释:预处理器移除源代码中的所有注释。
  5. 添加行号和文件名信息:为了在编译过程中产生准确的错误和警告信息,预处理器会添加行号和文件名信息。
  6. 生成预处理后的源代码:完成上述步骤后,预处理器会生成一个预处理后的源代码文件,这个文件将被编译器用于后续的编译过程。

至于static发挥作用,如果要详细的说,是在生成符号表时,编译器会为每一个作用域建立单独的符号来实现作用域。每一个带有声明的程序块都有自己的符号表。