链接性 杂谈

来源:互联网 发布:mysql数据库忘记密码 编辑:程序博客网 时间:2024/06/07 03:26

即使是使用 __forceinline参数, 编译器也不是在所有的情况下都能把函数成功内联。在以下任何一种情况下,函数内联是会失败的:

  • 函数或它的调用者被以 /Ob0 参数进行编译,该参数也就debug模式的默认参数。(也就是说,debug版本不会内联任何函数)
  • 函数和它的调用者使用了不同类型的异常处理。(The function and the caller use different types of exception handling (C++ exception handling in one, structured exception handling in the other).)
  • 函数有一个可变形参列表。
  • 函数中使用了内联的汇编代码,不过如果编译时加上 /Og, /Ox, /O1, or /O2参数,还是会成功内联的。
  • 函数中使用了递归调用,并且没有使用 #pragma inline_recursion(on)。如果使用了 #pragma inline_recursion(on),那么递归函数也是可以被成功内联的,不过只会具有16层的深度。如果想减少深度,可以使用inline_depth pragma。
  • 函数是虚函数并且被动态的调用。但是,如果是以对象的方式直接调用,虚函数也是可以内联的。
  • 程序获取的是函数的地址,通过一个函数指针来调用的函数。(The program takes the address of the function and the call is made via the pointer to the function. Direct calls to functions that have had their address taken can be inlined.)
  • 函数被 __declspec 修饰符所标记。
====================================================================================================

引子 定义不一样的同名inline函数

在开始我们的讨论之前先看一下简单的小程序。这个小程序是由三个.cpp文件组成的:test1.cpp、test2.cpp、testMain.cpp 。

//test1.cpp#include <iostream>inline void show(){    std::cout << "test1.cpp" << std::endl;}void CallShow1(){    show();}
//test2.cpp#include <iostream>inline void show(){    std::cout << "test2.cpp" << std::endl;}void CallShow2(){    show();}
//testMain.cpp#include <iostream>void CallShow1();void CallShow2();int main(){    CallShow1();    CallShow2();    return 0;}

大家可以猜一下输出结果是什么?如果你只是在课堂上学过C++,那你一定天真地认为:因为inline函数具有内部链接性,所以肯定是本文件中的函数,都用本文件中的内联函数定义。所以你会猜测输出结果一个是test1.cpp,一个是test2.cpp。如果这样猜测,那你就错了。还是揭晓答案吧。结果是不定的

如果没有开编译器优化选项,那么:

如果链接的顺序为   mingw32-g++.exe  -o hello.exe test1.o test2.o testMain.o  

则输出结果为

test1.cpptest1.cpp

如果链接的顺序为   mingw32-g++.exe  -o hello.exe test2.o test1.o testMain.o 

则输出结果为

test2.cpptest2.cpp
我还做了一张图,形象地展示了上面讲的内容。


通过上面的展示,可以看出,链接的顺序决定了使用哪个内联函数定义来进行展开。也就是说,如果先碰到了test1.o,则程序中所有文件中的同名内联函数,全都使用test1.o中的定义。即先碰到哪个定义,那么所有文件中的同名内联函数都使用这个定义。也就是说,在没有开编译器优化选项的情况下,inline函数是在链接期展开的。

我们可以通过nm命令看到,未经优化的.o文件中,是存在内联函数的符号的。


正因为如此,对于新入行的程序员们,老手总是谆谆教诲:“对于同一程序的不同文件,如果同名inline函数出现的话,其定义必须相同,否则会出现不定的结果”。正是由于先碰到哪个.o文件是不定的,所以说输出结果是不定的。

不过,开过编译器优化选项后,输出结果就正常了。



下面我们进入正题,浅谈一下内部链接性与外部链接性。

对于刚刚的那个问题,inline的内部链接性只能保证在编译的时候,不同编译单元中即使出现同名内联函数,也会通过编译。这仅仅只是保证可以通过编译。但具体展开哪个内联函数定义,是由链接器决定的。

为了可以表达的更加清楚,我们先讲几个术语。

  • 编译单元(翻译单元)

当然,简单地我们可以理解为单个的.cpp文件。更具体地讲,是让预处理器作用于该文件,处理后所获得的结果。这个处理是指:按照条件编译命令((#if,#ifdef等等)去除掉未 被选中的代码块,去除注释,递归的插入"#include"指令所引用的文件,并将宏展开。

  • 作用范围、作用域

//myClass.hclass A{    static  int a;};

对于class A而言,假如某个.cpp文件include了myClass.h,那么此时,class A就属于那个.cpp文件对应的匿名命名空间。那个匿名命名空间就是类A的作用域。

而对于static int a 而言,a的作用域为class A,也就是类A的内部。

  • 内部链接性、外部链接性

下面不论是变量、函数还是类对象,我们统称为对象。

内部链接性是指,如果该对象只在本作用范围可见,则表明该对象具有内部链接性。同理,如果该对象在所有作用范围都可见,则表明该对象具有外部链接性。内部链接性和外部链接性是互斥的,只能居于其一。

  • 声明和定义

声明是一种“把一个C++名称引入或者重新引入到你的程序”的构造。声明也可以是定义,这依赖于它所引入的实体和引入的方式。

1)命名空间和命名空间的别名

命名空间的声明和命名空间的别名都是定义。尽管名字空间的成员可以在这之后被扩展(这点于类和枚举类型不一样),即尽管命名空间具有断续性或接续性。


2)枚举、类、类模板、函数、函数模板、成员函数和成员函数模板

当且仅当后面有一对花括号出现时,声明才是定义。否则只是声明。


3)typedef和using(C++11)

它们只是声明,不能成为定义


4)ODR(One-Definition Rule)

这就是大名鼎鼎的“只能定义一次”规则。简单的讲,在该对象能作用到的编译单元内,只能定义一次,但可以有多次声明,但多次声明必须一致。

声明仅仅是将一个符号引入到一个作用域。而定义提供了一个实体在程序中的唯一描述。在一个给定的定义域中重复声明一个符号是可以的 , 但是却不能重复定义 , 否则将会引起编译错误。但是,对于类定义中的函数声明,只能声明一次。


作为本文的结尾,我们来谈谈一个没人提及的问题。

即然只能定义一次,那为什么要把类的定义放在头文件中。也就是说,为什么可以把类的定义要放在.h文件中?

我们知道;将具有外部链接性的定义放在头文件中,只要该头文件中被两个或两个以上的源文件包含,那么就会导致编译错误,因为存在多重定义,链接时就会出错。

但 是,如果在头文件中放置内部链接的定义却是合法的,但不推荐过度地使用。因为头文件被包含到多个源文件中时,每个编译单元中都会有自己的实体存在。


类的定义具有内部链接,由于它是定义,因此在同一编译单元中不能重复出现。而由于它的内部链接性,如果需要在其他编译单元使用,类的定义可以出现在不同的.cpp文件中,类的作用域只为对应.cpp的匿名命名空间。

PS.

如果在别的文件中只是需要使用类的类型,即并不需要涉及到类中具体的函数,那么仅仅在其他文件中使用 class a; 声明就可以了。但是,一旦该文件需要使用到类中的方法,那只使用 class a; 声明是不行的,而需要include进来整个类的完整定义。原因就是类的定义是内部链接,不会在目标文件导出符号。也就不会被其他单元解析它们的未定义符号。 理解这一点很重要。

正是因为类的定义具有内部链接性,所以对于在类内部定义的函数,编译器会尽量使其成为内联函数,从而具有内部链接性。但如果在类内部定义的函数很复杂,则编译器还是把它做为普通类成员函数,具有外部链接性。此时,如果有两个外部文件同时include了该头文件,则会有重定义错误。也就是说,普通的类成员函数具有外部链接性


但是,但是,从编译器的实际实现上来讲,内联失败的函数与普通函数是有区别的。若一个函数被声明成内联函数,若在调用该内联函数的地方发现该内联函数的不适合展开时怎么办?一种选择是在调用该内联函数的目标文件中为该内联函数生成一个具有内部链接的代码。这么做的直接后果是:若在多个文件调用了内联失败的函数,其中每个文件对应的目标文件中都会包含一份该内联函数的目标代码。

如果编译器真的选择了上面的做法对待内联失败的函数,那么最好的情况是:没吃到羊肉,反惹了一身骚。即内联的好处没享受到,缺点却承担了:目标代码的体积膨胀得与成功内联的目标代码一样,但目标代码的效率确和没内联一样。同时,同时,内联失败的函数内的静态变量实际上就不在只有一份,而是有若干份。这显然是个错误,但是如果不了解内幕就很难找到原因。



Done!






0 0
原创粉丝点击