疑问
- inline关键字的作用是什么,大多数人的回答肯定是内联优化。以下是一个大模型的回答。
- 下面是来自
cppreference的回答:
inline关键词的本意是作为给优化器的指示器,以指示优先采用函数的内联替换,而非进行函数调用,即并不执行将控制转移到函数体内的函数调用 CPU 指令,而是代之以执行函数体的一份副本而无需生成调用。这会避免函数调用的开销(传递实参及返回结果),但它可能导致更大的可执行文件,因为函数体必须被复制多次。
因为内联替换在标准语义中不可观察,编译器拥有对任何未标记为 inline 的函数使用内联替换的自由,和对任何标记为 inline 的函数生成函数调用的自由。这些优化选择不改变上述关于多个定义和共享静态变量的规则。
现代C++中,inline已经没有内联优化的含义了。
实验
上面说了内联不是强制性的,如果函数体很大,编译器可能不会内联,那么函数体不大,内联的开销很低,inline就能使得编译器内联吗?我们直接看例子:
这里做了4组实验,围绕是否有inline修饰,是否开优化等级。可以看到,函数是否内联和inline没有关系,仅仅取决于是否开了优化等级,仅仅-O1的优化,简单函数就能达到内联的效果。所以无需担心自己的代码是否需要inline修饰来优化,真正担心的话就让编译器自己优化进行了。上面是使用C++11进行测试,事实上在现代C++中,inline早就没有了内联优化的作用。inline的真正作用是使得符号可以违反单一定义原则(One Definition Rule, ODR)。ODR是C++语言中的一个基本规则,它规定在程序的非模板函数、变量(不包括文件作用域的静态变量)、类类型(包括静态数据成员)、枚举类型等必须有单一的定义。
分析
在解释inline关键字之前,先从extern关键字说起,在main.cpp中如果想用到其他cpp文件中定义的变量或函数,那么需要extern关键字进行声明。
// other.cpp
int num = 5;
void twice(int i) {
std::cout << i * 2;
}
// main.cpp
extern int num;
extern void twice(int i);
int main() {
twice(num);
}
当然twice前面的extern可以省略,因为函数是默认具有extern属性的。如果在main.cpp中去掉num前面的extern关键字,那么编译就会报重复定义num的错误。
然后是static关键字。static除了在类中定义静态变量、静态函数,当static用于修饰一个全局函数或变量时,它的作用是限制该函数或变量的链接属性,使其只在定义它的翻译单元(translation unit)中可见。这通常被称为内部链接(internal linkage)。
// other.h
#pragma once
static void twice(int i) {
std::cout << i * 2;
}
// other.cpp
#include "other.h"
void func() {
std::cout << "other.cpp/twice() : " << (void*)&twice << '\n';
}
// main.cpp
#include "other.h"
extern void func();
int main() {
std::cout << "main.cpp/twice() : " << (void*)&twice << '\n';
func();
return 0;
}
/* @result
* main.cpp/twice() : 0x7ff7252215e0
* other.cpp/twice() : 0x7ff725221660
*/
可以看到,它们打印出来的twice函数的地址是不相同的。每个.cpp文件都会有一个独立的static属性函数实例,它们的地址是不同的。
如果将twice函数定义放在other.cpp中,仅在ohter.h中保留函数声明,那么main函数访问twice函数会报错,因为这是 undefined reference to twice(int)错误。
这里的static关键字是必要的,如果去掉twice的static修饰,那么编译会报错,因为#include只是将内容拷贝到当前cpp文件,这样一来other.cpp和main.cpp都会有twice的定义,违反了单一定义原则。
接下来,可以继续说inline关键字了。inline关键字,使得可以符号可以违反ODR原则,即可以重复定义,但需要保证这多个定义在文本上是相同的,否则是未定义行为。
让我们修改上面的代码:
// other.h
#pragma once
inline void twice(int i) {
std::cout << i * 2;
}
// other.cpp
#include "other.h"
void func() {
std::cout << "other.cpp/twice() : " << (void*)&twice << '\n';
}
// main.cpp
#include "other.h"
extern void func();
int main() {
std::cout << "main.cpp/twice() : " << (void*)&twice << '\n';
func();
return 0;
}
/* @result
* main.cpp/twice() : 0x7ff7ca8b2ed0
* other.cpp/twice() : 0x7ff7ca8b2ed0
*/
将static换成inline,可以看到函数的地址相同了,因为这两个函数的定义相同,可以随机选择一个进行链接,得到的当然也就是相同的地址。
总结
- 内联函数或变量(C++17 起)在程序中可以有多于一次定义,只要每个定义都出现在不同翻译单元中(对于非静态的内联函数和变量(C++17 起))且所有定义等同即可。例如,内联函数或内联变量(C++17 起)可以在被多个源文件包含的头文件中定义。
- 它必须在每个翻译单元中都被声明为
inline。 - 它在每个翻译单元中都拥有相同的地址。
- 在C++中,如果一个成员函数在类定义内部定义,它默认就是
inline的。
用法
在你的项目中,或许有一个common.h包含了项目中普遍需要的一些头文件或者宏定义、类型声明等。使用inline关联字,可以在单头文件中定义函数,使得在被多个.cpp文件包含时,编译不会报重定义错误。inline也可以用于修饰变量,这样就不需要在头文件使用extern声明外部变量,在common.cpp文件定义了,直接使用inline关键字定义在头文件中即可。inline非常适用于开发使用单头文件的cpp库,或者在自己的项目里少写一个common.cpp文件。