C++ 头文件中定义的函数一定是内联的吗?

C++ 头文件中定义的函数一定是内联的吗?如果不一定,编译器是怎样把多个.o文件中的同名函数链接到同一个函数上?
关注者
47
被浏览
32,194

6 个回答

每次谈到 C++ 里的内联的时候,都是得要我出来辟谣的时候。— — 一个无奈的题注


大兄弟,#include 的作用只是一个高级点的 copy+paste 而已。你写的头文件和 .txt 比没有一点特殊性。所以 头文件中定义的函数并不天然具备内联属性

如果你按下面方式组织项目结构:

// h.hpp
#ifndef _H_HPP
#define _H_HPP
void h()
#endif
-----------------------------------------------------
// a.cpp
#include "h.hpp"
int main()
	h();
	return 0;
-----------------------------------------------------
// b.cpp
#include "h.hpp"
void b()
    h();

执行下面的编译语句:

g++ a.cpp b.cpp -o main

那么你会得到一个链接错误:

/usr/bin/ld: /tmp/ccvTrGvf.o: in function `h()':
b.cpp:(.text+0x0): multiple definition of `h()'; /tmp/ccLEccIg.o:a.cpp:(.text+0x0): first defined here
collect2: 错误:ld 返回 1

而且这个问题不是我们在编译阶段讲的重复 include 导致的多次定义的问题 ,你是没法通过

#ifndef FILE_NAME
#define FILE_NAME
#endif

经典保护三段论,或者 #pragma once 解决这个报错的。

这个时候,通过给 h 函数加上 inline 或者 static 修饰,可以解决这个链接报错。


但是,inline 和 static 解决这个问题的技术原理是不同的。

在 C 语言的基础部分我们学过,用 static 关键字修饰的函数,其作用域范围被限制在了文件作用域一级。

我们看下,对上面的 h 函数加上 static 修饰(static void h())之后,最终生成的二进制是什么样的:

我们看到,最终结果中有地址分别为 1129 和 1140 的两个同名标识 _ZL1hv(红框标注部分,这是 h() 函数经过 mangle 之后的名字),而 main 函数和 b() 函数,其内部调用的是地址不同的 h() 函数!(蓝框标注部分)。

而通过 nm 命令,导出程序中的所有符号,我们也可以看出些端倪:

这也意味着,我们在头文件中只编写了一次的 static void h() 函数,在被分别 include 进两个不同的编译单元 a.cpp 和 b.cpp 之后, 已经分化为了两个长得一样的分身 ,一个是 a.cpp 中的 h() 函数,一个是 b.cpp 中的 h() 函数。它们被链接在一起之后,分得了不同的地址,以在全局区分这两个分身。

@孔乙己 这里说的会被 mangle 成不同的名字,与实际不相符。实际结果是它们在全局有着相同的符号。(尽管之前我也是这么认为的,认为编译器会给不同的符号,比如 A_foo, B_foo。所以我也对实验结果也十分震惊,不清楚链接器是怎么区分的。还请有清楚的读者评论区告知!)


我们再来看看,用 inline 修饰有何不同(将 h() 函数改为:inline void h()):

首先,nm 导出的符号表,我们可以通过仔细核对后发现, h 函数 mangle 后符号 _Z1hv 在全程序中只有唯一一个:

而看汇编,这次,main 函数和 b 函数内部调用的则是 同一个函数 ,即地址位于 1139 的 _Z1hv 了。

而这就是 inline 和 static 有所区别的地方。

inline 的作用机理是,被 include 进多个编译单元的 h 函数,在多个编译单元中分别编译,得到了多个副本;在链接的时候,链接器随便选取其中的一个副本保留,其余的被丢弃。

static 的作用机理是,被 include 进多个编译单元的 h 函数,在多个编译单元中分别编译,得到了多个副本;在链接的时候,链接器认为它们是其所属的编译单元私有的函数,所有的副本都被保留。

而原来的 inline static 都不加,出错的机理是,链接器发现有两个同名的函数符号,对不起,我分不清你们两个,于是报符号冲突。


小结一下,static 关键字应修饰的是那些 “只供同文件的其他函数使用的函数”, 将 static 函数置于头文件中,进而被多个源文件 include,企图使用这种方式绕过链接符号冲突错误,则是对 static 的误用、乱用。

因为加 inline,可以保证你在不同的编译单元中调用的是同一个函数;而加 static,你在不同编译单元中调的是不同的分身!

具体例子是什么呢?

// head.hpp
#ifndef _HEAD_HPP
#define _HEAD_HPP
inline/static int h()
	static int a = 0;
	return a++;
#endif
---------------------------------
// b.cpp
#include "head.hpp"
int b()
	return h();
---------------------------------
// a.cpp
#include "head.hpp"
#include <iostream>
int b();
using namespace std;
int main()