static、inline 关键字在C语言中的作用分析
static、inline
1. static
关键字
- 作用:限制函数的作用域仅限于定义它的源文件(即内部链接)。这意味着每个包含该头文件的源文件都会生成该函数的一个独立实例,避免了多个定义导致的链接错误。
- 在头文件中使用:适用于小型、频繁调用的函数,允许每个源文件拥有自己的函数副本,避免链接器的重复定义错误。
2. inline
关键字
- 作用:建议编译器将函数调用替换为函数体本身,以减少函数调用的开销,提高性能。
- 在头文件中使用:结合
static
使用时,可以在多个源文件中安全地定义内联函数,而不会导致链接冲突。 - 链接行为:
- C99 标准:
inline
函数具有外部链接,但需要在某个翻译单元中提供一个非inline
的定义,供链接器使用。 - GNU 扩展:GNU C 允许
inline
函数在头文件中被多次定义,只要它们是inline
的,并且链接器能够处理这些定义。
- C99 标准:
组合使用 static inline
- 消除重复定义:在头文件中使用
static inline
,每个包含该头文件的源文件都会有自己的函数副本,避免了链接器在合并多个目标文件时因重复定义同名函数而报错。 - 优化性能:编译器可能将这些函数内联展开,减少函数调用的开销。
一个例子
test.h:
1 |
|
test2.c:
1 |
|
test3.c:
1 |
|
main.c:
1 |
|
Makefile:
1 |
|
{static(Y) inline(Y)]、[static(Y) inline(N)}
这两种情况下都可以编译成功,这很正常:
1 |
|
可以看到,虽然是 inline,但是编译器没有听从建议,全部用的是内部链接。
{static(N) inline(Y)]
这两种情况下编译不成功了:
1 |
|
可以看到它报的错误是 undefined reference to func1
,并且 test2.o
、test3.o
中有未定义的 func1
,这是为什么?这是因为编译器期望存在一个外部定义,但是并没有外部定义,因此C99要求:inline 函数具有外部链接,但需要在某个翻译单元中提供一个非 inline 的声明,供链接器使用。比如:
1 |
|
就可以通过编译。
另外,如果强制内联,那么内联函数就自然不存在什么外部链接了,直接在预处理阶段就处理好了:
1 |
|
或者,直接在变异的时候,最大概率内联(不一定会内联,但是概率较大,最好的方法就是强制内联):
1 |
|
总结
1. 函数的链接属性
在C语言中,函数有两种主要的链接属性:
内部链接(Internal Linkage):函数只能在定义它的源文件内部使用。使用
static
关键字可以实现这一点。外部链接(External Linkage):函数可以被整个程序中的其他源文件访问。默认情况下,函数具有外部链接,除非使用
static
限制其作用域。
2. inline
关键字的作用
inline
关键字是对编译器的一个建议,表示希望将函数调用替换为函数体本身,以减少函数调用的开销,提高执行效率。然而,inline
并不保证编译器一定会内联所有的函数调用。编译器会根据函数的复杂性、调用频率以及优化级别等因素决定是否内联。
3. 仅移除 static
,保留 inline
的影响
当您在头文件中定义一个函数并仅移除 static
,保留 inline
,如下:
1 |
|
这会导致以下情况发生:
外部链接:函数
inst_fetch
现在具有外部链接,这意味着它在整个程序中都是一个全局符号,可以被其他源文件引用。多重定义的风险:如果多个源文件包含这个头文件,每个源文件都会尝试定义一个名为
inst_fetch
的全局符号。这违反了C语言的“一定义规则”(One Definition Rule),即每个全局符号只能有一个定义。编译器的内联行为:
- 部分内联:编译器可能会在某些调用点内联
inst_fetch
,将函数体直接插入到调用处,从而不生成对该函数的外部调用。 - 未内联的调用:对于未被内联的调用,编译器会生成对
inst_fetch
的函数调用指令,期望在链接阶段找到该函数的定义。
- 部分内联:编译器可能会在某些调用点内联
链接器的期望:
- 链接器在处理这些调用时,期望在某处找到
inst_fetch
的唯一定义。 - 由于头文件中的
inline
定义被多个源文件包含,并且没有提供一个统一的外部定义,链接器无法找到一个确定的inst_fetch
实现,这也是为什么 C99 要求必须提供一个外部实现或者定义。 - 结果,链接器报告“未定义引用”(undefined reference)错误,因为它找不到函数的实际实现。
- 链接器在处理这些调用时,期望在某处找到
4. 为什么会看到未定义引用
尽管 inline
关键字建议编译器内联展开函数,编译器并不保证所有的调用都会被内联。对于那些未被内联的调用,编译器生成了对 inst_fetch
的外部调用指令,但由于缺乏一个统一的外部定义,链接器无法解析这些调用,导致未定义引用错误。
5. nm
工具显示未定义符号的原因
当使用 nm
工具查看对象文件时,看到类似以下的输出:
1 |
|
这里的 U
表示“未定义”(Undefined)符号,意味着这个对象文件引用了 func1
,但没有在该文件中找到它的定义。具体原因如下:
- 未内联的调用:某些调用点未被内联展开,编译器生成了对
func1
的调用,但并未在当前源文件中提供其实现。 - 缺乏统一的外部定义:由于头文件中定义的
inline
函数被多个源文件包含,且没有一个源文件提供一个非inline
的外部定义,导致链接器找不到func1
的实际实现。
6. 为什么同时去掉 static
和 inline
会导致多重定义错误
当同时去掉 static
和 inline
,函数定义变为一个普通的全局函数,具有外部链接:
1 |
|
这种情况下:
- 外部链接:函数
inst_fetch
具有外部链接,可以被整个程序中的其他源文件访问。 - 多重定义:多个源文件包含这个头文件,每个源文件都会生成一个名为
inst_fetch
的全局符号。 - 链接器冲突:链接器在合并这些目标文件时,发现多个同名的全局符号,导致多重定义错误。
7. 为什么仅移除 static
会导致未定义引用,而去掉 static
和 inline
会导致多重定义
**仅移除
static
,保留inline
**:- 部分内联:编译器可能内联一些调用,但未内联的调用需要一个外部定义。
- 缺少外部定义:由于没有提供一个统一的外部定义,链接器无法找到
inst_fetch
,导致未定义引用错误。
**同时去掉
static
和inline
**:- 全部外部定义:每个包含头文件的源文件都定义了一个全局的
inst_fetch
,导致链接器发现多个定义,报出多重定义错误。
- 全部外部定义:每个包含头文件的源文件都定义了一个全局的
声明和定义:static
第一问:
在 nemu/include/common.h
中添加一行 volatile static int dummy;
后重新编译 NEMU。由于 common.h
被多个源文件包含,而 static
关键字在文件作用域内使变量具有内部链接,每个源文件都会有自己独立的 dummy
变量。
因此,NEMU 中的 dummy
变量实体数量等于包含了 common.h
的源文件数量。
第二问:
在 nemu/include/debug.h
中也添加一行 volatile static int dummy;
,然后重新编译 NEMU。
比较和解释:
实体数量: NEMU 中的
dummy
变量实体数量仍然等于源文件的数量,因为每个源文件中,dummy
变量只会被定义一次。原因: 即使某个源文件同时包含了
common.h
和debug.h
,由于两个头文件中对dummy
的声明都是volatile static int dummy;
,这在 C 语言中属于多次声明同一个具有内部链接的变量,是被允许的。结论: 与第一问相比,
dummy
变量的实体数量不变,仍然等于源文件的数量。
第三问:
将两处 dummy
变量修改为初始化形式:volatile static int dummy = 0;
,然后重新编译 NEMU。
发现的问题:
- 编译器会在包含了 同时包含
common.h
和debug.h
的源文件中报错,提示重复定义了dummy
变量。
原因分析:
定义与声明的区别: 在 C 语言中,未初始化的文件作用域的
static
变量(如static int dummy;
)被视为声明,可多次声明。但带有初始化的static
变量(如static int dummy = 0;
)被视为定义,在同一作用域内只能定义一次。重复定义错误: 当一个源文件同时包含了
common.h
和debug.h
,并且这两个头文件中都对dummy
进行了初始化定义,编译器会检测到在同一作用域内对具有内部链接的变量进行了多次定义,从而报错。
为什么之前没有出现这样的问题?
未初始化时的行为: 未初始化的
static
变量在同一作用域内可以多次声明,编译器会将其视为对同一变量的多次声明,不会报错。初始化后的行为: 一旦对
static
变量进行了初始化,就变成了定义。在同一作用域内多次定义同名的具有内部链接的变量是非法的,编译器会报重复定义错误。
总结:
在头文件中声明未初始化的
static
变量是安全的,即使多个头文件中有相同的声明,编译器也不会报错。但在头文件中定义(初始化)具有内部链接的
static
变量时,需要确保这些头文件不会被同一源文件多次包含,否则会导致重复定义错误。建议: 为避免此类问题,最好避免在头文件中对
static
变量进行初始化。如果需要初始化,应该在源文件中进行,或者采取其他设计方式。