最近在集中面试,因为招的是cpp开发人员,所以对语言这块的面试在所难免,尤其是对于新特性这块。可能有的人会问,我使用C++03还不是一样可以完成工作,为什么要了解新特性?
其实,项目用C++03完全可以胜任,之所以问新特性,是因为作为一个技术开发人员,要时刻了解技术最前沿,哪怕不用研究的太底层,基础的功能也还是要知道,可惜的是,面了好多人,也就那么一两个通过了技术面试,虽然最终因为种种原因,没能加入😭。
当然了,inline
也是我经常问的一个问题,往往以聊聊你对inline的看法
为题,开始展开~
基本上所有人都会围绕着优化函数调用来回答,即当编译器遇到一个 inline 函数调用时,它会尝试用函数体的代码替换函数调用的地方。这意味着函数调用的开销(例如堆栈操作和跳转)被消除,代码执行可能会变得更高效
。这个回答没错,但却不是我心里想要的答案,于是我会接着问:然后呢?
嗯,然后候选人就卡壳了,这个问题最终结束~
在某次群里聊天的时候,将这个问题在群里抛出来了,然后大家的回答仍然是跟很多候选人回答的一样,虽然我在一年半以前在公众号发过这篇文章inline: 我的理解还停留在20年前,甚至通子出来的某个佬也不清楚~
其实,候选人对inline的回答并没有错,编译器用来优化,但是对于现在的编译器来说,能否优化或者是否会进行优化,已经完全不依赖我们代码中的inline
关键字来告诉编译器了(虽然编译器不一定听),编译器有自己的优化策略。
其实,inline
还有另外一个Modern赋予其的非常重要的作用,在编译和链接阶段,尤其是针对ODR方面,这也就是作为面试官,最想听到答案~
ODR
在C++中,ODR是One Definition Rule
的缩写,即单一定义规则。
cppreference中的定义如下:
Only one definition of any variable, function, class type, enumeration type, concept (since C++20) or template is allowed in any one translation unit (some of these may have multiple declarations, but only one definition is allowed).
One and only one definition of every non-inline function or variable that is odr-used (see below) is required to appear in the entire program (including any standard and user-defined libraries). The compiler is not required to diagnose this violation, but the behavior of the program that violates it is undefined.
这一规则是C++语言的核心原则之一,它规定了在同一个程序的不同翻译单元(Translation Units)中,任何命名实体(如变量、函数、类型别名、类模板实例等)都只能有一个定义(Definition),可以有多个声明(Declaration)。换句话说,对于全局作用域的同一实体,其定义在整个程序中只能出现一次,但可以被不同翻译单元通过声明来引用。
违反ODR可能导致未定义行为(Undefined Behavior),例如链接错误或者运行时错误。
在链接阶段,如果链接器可以找到多个同一个符号的定义,则认为是错误的,因为其不知道使用哪个,这个时候,就会出现链接器报错,如下这种:
error: redefinition of 'xxx'
而这个报错原因,就是因为没有遵循ODR原则,下图易于理解:
在我们开发项目的过程中,有一个不成文的规定,即声明和实现分离(模板除外),其实声明和实现分离的另外一个原因是为了遵守ODR原则
。
这就往往使得我们像如下这样实现代码:
// fun.h
int fun() {
return 0;
}
// test.h
int test();
// test.cc
#include "fun.h"
int test() {
return fun();
}
// test1.h
int test();
// test1.cc
#include "fun.h"
int test1() {
return fun();
}
// main.cc
#include "fun.h"
int main() {
return 0;
}
编译命令如下:
g++ -g test1.cc test.cc main.cc -I . -o odr
报错如下:
in function `fun()':
/test/odr/fun.h:1: multiple definition of `fun()';
/tmp/cc5YLpsp.o:/test/odr/fun.h:1: first defined here
collect2: error: ld returned 1 exit status
上述报错即违反了ODR
原则,即:
•fun.h中定义了函数fun()•test.cc和test1.cc包含了头文件fun.h,利用头文件展开的原理,在编译器生成的test.o和test1.o中会分别存在一份fun()定义•main.cc中包含了头文件test.h和test1.h•在main.cc中分别调用test()和test1()•链接器在链接test.o和test1.o时候,发现fun()有两个定义,不确定要使用哪个,因此就报了前面的错误
看了上面的报错,我们第一时间会考虑如何解决它,后面几节中,将列举几种修改方式,最后从编译的角度去分析这几种方式的区别。
TU
既然在前面一节中已经知道了报错原因,那么就从根上解决这个问题,即重新生成一个文件fun.cc,将fun()函数放入其中,而fun.h则只放fun()函数的声明
。
// fun.h
// fun.h
int fun();
// fun.cc
#include "fun.h"
int fun() {
return 0;
}
嗯,编译&链接正常,因为fun()定义只存在于fun.cc中,所以不存在重复定义的问题,这也就解决了一开始的报错问题~
static
另外一种解决方案是通知链接器,我知道有两个定义,但是我只使用自己编译单元中存在的那个
,因此可以在该函数前加上static
即可:
// fun.h
static int fun() {
return 0;
}
在编译完成后,每个编译单元有一个fun()定义,但是因为其被声明为static,因此不会被export
,因此就避免了一开始的报错。
Modern inline
如果把之前编译器优化的inline称之为传统inline的话,那么C++17起,对inline赋予的新角色,我暂且称之为Modern inline
。
自C++17起,使用inline,允许在多个TU中存在相同的定义而不违背链接器的ODR规则,虽然某个符号出现在多个TU中,但是最终只会有一个被链接,当然了链接哪个,这个依赖于编译器或者编译器链接TU的顺序~
如果使用inline修改前面的例子,如下:
// fun.h
inline int fun() { // 此处加inline关键字
return 0;
}
编译&运行,成功~
虽然会有多个fun()符号,但是最终链接器只会选择其中一个,这样同样避免了一开始的报错。
PS:模板函数默认情况下也是内联的,但完整的模板特化则不是,也就是说特化模板如果在多个翻译单元中,将因为违反ODR原则,而导致报错。
区别
好了,在前面内容中,使用了3中方式来解决链接过程中的ODR问题,那么问题来了,这几种有什么区别?建议用哪个呢?
当然了,单纯从源码角度看区别,这种只能看个表面,所以,还是建议你从编译的角度来分析,会理解的更为透彻。
使用g++ -c
生成.o文件,然后通过nm查看其内容。
test.o | test1.o | |
origin error | 0000000000000000 T fun | 0000000000000000 T fun |
TU | U fun | U fun |
static | 0000000000000000 t fun | 0000000000000000 t fun |
inline | 0000000000000000 W fun | 0000000000000000 W fun |
emm,下面是关于上面表格中fun前面的字符说明:
T | 这个符号表示一个全局或静态函数,其定义在代码段中。也就是说,符号 T 表示这是一个在目标文件中定义的可执行代码部分 |
U | 这个符号表示一个未定义的外部符号。也就是说,目标文件中引用了这个符号,但它的定义位于其他文件或目标文件中,通常在链接阶段解析 |
t | 这个符号表示一个局部符号,在代码段中定义。也就是说,符号 t 表示这是一个仅在目标文件内部有效的局部函数(或局部变量),不会被其他文件引用 |
W | 这个符号表示一个弱符号。弱符号在链接时如果有其他强符号定义,弱符号会被忽略。弱符号通常用于提供默认实现,但允许在其他文件中进行覆盖 |
static和inline解决方式的区别在于它们处理符号重复定义的方式不同。
嗯,此时,可能有人要问如何选用上面哪种解决方案?答案是具体问题具体分析
,使用TU的方式是最差的一种,不过,一般来说建议使用inline,如果你的编译器支持c++17及以上的话~
inline 变量
C++17 引入了inline
变量,其语义与inline
函数类似,只是它适用于变量。
最常见的是用于类内static变量的初始化:
class Test {
private:
inline static int value_ = 0; // 等同于类外执行 int Test::value = 0;
};
与inline function一样,inline variable也允许在多个编译单元对同一个变量进行定义,并且在链接时只保留其中的一份作为该变量的定义。当然,同时在多个源文件中定义同一个inline变量必须保证它们的定义都相同,否则和inline函数一样,你没办法保证链接器最终采用的是哪个定义。
inline variable除了支持类内静态成员初始化外,也支持头文件中定义全局变量,这样不会违反ODR规则。
注意
需要注意的是,由于inline
关键字有效地忽略了链接器对ODR的检查,这允许它在所有定义的编译单元中选择该符号的任何一个定义,而忽略其他定义。
// test.cc
inline int fun() {
return 0;
}
int call() {
return Foo();
}
// main.cc
#include <iostream>
int call();
inline int fun() {
return1;
}
int main() {
std::cout <<call()<< std::endl;
std::cout <<fun()<< std::endl;
return0;
}
嗯,上面这个代码可能输出的结果不一样:
g++ main.cc test.cc -o test
// 输出 1 1
g++ test.cc main.cc -o test
// 输出 0 0
也就是说输出结果依赖于编译文件的顺序,emm~
好了,本文内容到此结束,希望你下次再遇到类似问题时能够参考这些信息,找到解决方案。
1、比“千千静听”还老的装机必备播放器——Winamp公开源代码,但“白嫖”条款惹恼众人