漫谈C++中的重定义:ODR,inline,static,extern
漫谈C++中的重定义:ODR,inline,static,extern
首先加了inline不会一定内敛,甚至inline原本的建议内联的作用已经没什么作用了,现在的编译器已经足够智能,能够自己决定是否内联。
先了解ODR 和 extern/static
这一节主要理解一下几个概念
- 什么是ODR
- 什么是一个翻译单元
- 定义和声明的区别
- extern关键字帮助我们把definition变为declaration
- 函数声明默认带extern
- static关键字的local definition
https://en.cppreference.com/w/cpp/language/definition
One Definition Rule意味着只能被定义一次,即任何变量、函数、类类型、枚举类型、概念 (C++20 起)或模板,在每个翻译单元中都只允许有一个定义(其中部分可以有多个声明,但只允许有一个定义)。
在C++中, 一个翻译单元由一个实现文件及其直接或间接包含的所有标头组成。 实现文件通常具有文件扩展名 .cpp
或 .cxx
。 头文件通常具有扩展名 .h
或 .hpp
。 每个翻译单元由编译器独立编译。 编译完成后,链接器会将编译后的翻译单元合并到单个程序中。
According to standard C++ (wayback machine link) : A translation unit is the basic unit of compilation in C++. It consists of the contents of a single source file, plus the contents of any header files directly or indirectly included by it, minus those lines that were ignored using conditional preprocessing statements.
A single translation unit can be compiled into an object file, library, or executable program.
The notion of a translation unit is most often mentioned in the contexts of the One Definition Rule, and templates.
首先要区分定义和声明
1 |
|
考虑这种情况
1 |
|
会出现问题,因为不符合ODR原则,两个对i的definition。
我们也许会疑惑 这不是两个cpp两个翻译单元吗? 每个翻译单元有各自的i这有什么问题呢?
我们对翻译单元的理解有误
看一下cmake
1
2
3
4
5
6
7
8
9
10
cmake_minimum_required(VERSION 3.22)
project(
untitled1
LANGUAGES CXX C
)
set(CMAKE_CXX_STANDARD 20)
add_executable(MAIN main.cpp b.h 2.cpp)
TARGET_COMPILE_FEATURES(MAIN PRIVATE cxx_std_17)这里最后把main.cpp 和2.cpp扔到一起合并到单个程序中,所以这是一个翻译单元哦
那怎么办?用extern把main里的i变成declaration即可,这下就是一个definition了
1 |
|
这就一个definition了,可以通过编译了,反过来也是可以的 main.cpp
里是int i = 3;
, 1.cpp
里是extern int i;
,不论怎样我们需要保证只有一个定义。
所以总结下extern:
1 |
|
没有函数定义会自动给函数加上extern使他成为一个声明。
我们上面都在说的都是全局的definition,那么static这个关键字是一个局部的definition,只在自己的这个.cpp里有用。这样也不会造成重定义。
1 |
|
inline变量
根据上文的知识,在C++17的inline 变量出现前,要想在多个cpp中用同一个变量,我们需要extern 声明变量+只有一个定义:
1 |
|
由于inline现在的作用其实和内敛没什么关系了,他是为了让多个翻译单元可以共用一个变量,c++17后对inline的解释是“允许重复定义”。而内敛与否完全是编译器的优化行为了。
如果有多个编译单元拒绝了内联,就会生成多份函数/变量定义,为了在链接时不报错,由inline修饰的函数会生成弱符号,所以inline帮助我们在多个翻译单元中可以重复定义,帮助我们突破了ODR规则。
因此我们可以这样:
1 |
|
但是请注意它带来的一些UB行为,常常多个inline不同的初始值会导致
1 |
|
总结一下:
突破了ODR规则,不再用多个extern+一个定义来实现,减少了思维负担。
注意inline变量的不同初始化会带来的UB行为
constexpr函数和constexpr static变量默认隐含了inline。 ([dcl.constexpr]/1).
inline函数
- 加了inline关键字这个函数不一定是内敛函数,但是可以防止odr
- 类成员函数默认是inline的
1 |
|
内敛函数(指编译器优化行为)
注意下面说的是如果一个函数被内敛了 ,就会有以下的故事,而不是一个函数加了inline关键字就会有以下的故事。
内联函数语法
inline要起作用,必须要与函数定义放在一起,而不是函数的声明
1 |
|
内联函数的作用
当编译器处理调用内联函数的语句时,不会将该语句编译成函数调用的指令,而是直接将整个函数体的代码插人调用语句处,就像整个函数体在调用处被重写了一遍一样,在执行时是顺序执行,而不会进行跳转。
内联函数的优缺点
优点:内联函数没有执行函数调用的开销,加快了程序运行时间;
缺点:内联函数是将整个函数体的代码插入到调用语句处,会增大代码的大小,增大内存的占用。不过这个缺点几乎可以忽略,代码膨胀怪不到内敛上。
类与内联函数
- 类内定义的函数都是内联函数,无论是否有inline修饰符(虚函数单独考虑)
- 类外定义的没有inline修饰的函数不是内联函数
内联函数和宏的区别
- 宏是由预处理器对宏进行替换的,而内联函数是通过编译器控制实现的。
- 宏调用并不执行类型检查甚至连正常参数也不检查,但是函数调用却要检查。
- C语言的宏使用的是文本替换,可能导致无法预料的后果
- 在宏中的编译错误很难发现,因为它们引用的是扩展的代码,而不是程序员键入的
虚函数可以标为inline吗
虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联(因为无法知道具体将哪一部分代码插入到调用位置),具体内联与否由编译器决定。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!