C/C++链接属性以及static和匿名namespace的区别

链接

首先,对源代码进行编译,是对各个cpp文件单独进行的。对于每一次编译,如果排除在cpp文件里include别的cpp文件的情况(这是C++代码编写中极其错误的写法,如果有include,会递归包含头文件,形成一个含有所有必要信息的单个源文件,这个源文件就是一个编译单元),那么编译器仅仅知道当前要编译的那一个cpp文件,对其他的cpp文件的存在完全不知情。
其次,每个cpp文件编译后,产生的.o文件,要被一个链接器(link)所读入,才能最终生成可执行文件。

  • 编译:编译器对源代码进行编译,是将以文本形式存在的源代码翻译为机器语言形式的目标文件的过程。
  • 目标文件:由编译所生成的文件,以机器码的形式包含了编译单元里所有的代码和数据,以及一些其他的信息。

编译器把一个cpp编译为目标文件的时候,除了要在目标文件里写入cpp里包含的数据和代码,还要至少提供3个表:未解决符号表,导出符号表和地址重定向表。

  • 未解决符号表提供了所有在该编译单元里引用但是定义并不在本编译单元里的符号及其出现的地址。
  • 导出符号表提供了本编译单元具有定义,并且愿意提供给其他编译单元使用的符号及其地址。
  • 地址重定向表提供了本编译单元所有对自身地址的引用的记录。

链接器进行链接的时候,首先决定各个目标文件在最终可执行文件里的位置。然后访问所有目标文件的地址重定向表,对其中记录的地址进行重定向(即加上该编译单元实际在可执行文件里的起始地址)。然后遍历所有目标文件的未解决符号表,并且在所有的导出符号表里查找匹配的符号,并在未解决符号表中所记录的位置上填写实际的地址(也要加上拥有该符号定义的编译单元实际在可执行文件里的起始地址)。最后把所有的目标文件的内容写在各自的位置上,再作一些别的工作,一个可执行文件就出炉了。

现在我们可以来看看几个经典的链接错误了:

  • unresolved external link…
    这个很显然,是链接器发现一个未解决符号,但是在导出符号表里没有找到对应的項。解决方案么,当然就是在某个编译单元里提供这个符号的定义就行了。(注意,这个符号可以是一个变量,也可以是一个函数),也可以看看是不是有什么该链接的文件没有链接。
  • duplicated external simbols…
    这个则是导出符号表里出现了重复项,因此链接器无法确定应该使用哪一个。这可能是使用了重复的名称,也可能有别的原因。

我们再来看看C/C++语言里针对这一些而提供的特性:

  • extern:这是告诉编译器,这个符号在别的编译单元里定义,也就是要把这个符号放到未解决符号表里去。(外部链接)
  • static:如果该关键字位于全局函数或者变量的声明的前面,表明该编译单元不导出这个函数/变量的符号。因此无法在别的编译单元里使用。(内部链接)。如果是static局部变量,则该变量的存储方式和全局变量一样,但是仍然不导出符号。

默认链接属性:对于函数和变量,模认外部链接,对于const变量,默认内部链接。(可以通过添加extern和static改变链接属性)

  • 外部链接的利弊:外部链接的符号,可以在整个程序范围内使用(因为导出了符号)。但是同时要求其他的编译单元不能导出相同的符号(不然就是duplicated external simbols)
  • 内部链接的利弊:内部链接的符号,不能在别的编译单元内使用。但是不同的编译单元可以拥有同样名称的内部链接符号。
  • 为什么头文件里一般只可以有声明不能有定义:头文件可以被多个编译单元包含,如果头文件里有定义,那么每个包含这个头文件的编译单元就都会对同一个符号进行定义,如果该符号为外部链接,则会导致duplicated external simbols。因此如果头文件里要定义,必须保证定义的符号只能具有内部链接。
  • 为什么常量默认为内部链接,而变量不是:
    这就是为了能够在头文件里如const int n = 0这样的定义常量。由于常量是只读的,因此即使每个编译单元都拥有一份定义也没有关系。如果一个定义于头文件里的变量拥有内部链接,那么如果出现多个编译单元都定义该变量,则其中一个编译单元对该变量进行修改,不会影响其他单元的同一变量,会产生意想不到的后果。
  • 为什么函数默认是外部链接:
    虽然函数是只读的,但是和变量不同,函数在代码编写的时候非常容易变化,如果函数默认具有内部链接,则人们会倾向于把函数定义在头文件里,那么一旦函数被修改,所有包含了该头文件的编译单元都要被重新编译。另外,函数里定义的静态局部变量也将被定义在头文件里。
  • 为什么类的静态变量不可以就地初始化:所谓就地初始化就是类似于这样的情况:
    class A
    {
    static char msg[] = “aha”;
    };
    不允许这样做得原因是,由于class的声明通常是在头文件里,如果允许这样做,其实就相当于在头文件里定义了一个非const变量。
  • 在C++里,头文件定义一个const对象会怎么样:
    一般不会怎么样,这个和C里的在头文件里定义const int一样,每一个包含了这个头文件的编译单元都会定义这个对象。但由于该对象是const的,所以没什么影响。但是:有2种情况可能破坏这个局面:
    1。如果涉及到对这个const对象取地址并且依赖于这个地址的唯一性,那么在不同的编译单元里,取到的地址可以不同。(但一般很少这么做)
    2。如果这个对象具有mutable的变量,某个编译单元对其进行修改,则同样不会影响到别的编译单元。
  • 为什么类的静态常量也不可以就地初始化:
    因为这相当于在头文件里定义了const对象。作为例外,int/char等可以进行就地初始化,是因为这些变量可以直接被优化为立即数,就和宏一样。
  • 内联函数:
    C++里的内联函数由于类似于一个宏,因此不存在链接属性问题。
  • 为什么公共使用的内联函数要定义于头文件里:
    因为编译时编译单元之间互相不知道,如果内联函数被定义于.cpp文件中,编译其他使用该函数的编译单元的时候没有办法找到函数的定义,因此无法对函数进行展开。所以说如果内联函数定义于.cpp文件里,那么就只有这个cpp文件可以是用这个函数。
  • 头文件里内联函数被拒绝会怎样:
    如果定义于头文件里的内联函数被拒绝,那么编译器会自动在每个包含了该头文件的编译单元里定义这个函数并且不导出符号。
  • 如果被拒绝的内联函数里定义了静态局部变量,这个变量会被定义于何处:
    早期的编译器会在每个编译单元里定义一个,并因此产生错误的结果,较新的编译器会解决这个问题,手段未知。
  • 为什么export关键字没人实现:
    export要求编译器跨编译单元查找函数定义,使得编译器实现非常困难。

编译单元

当一个c或cpp文件在编译时,预处理器首先递归包含头文件,形成一个含有所有必要信息的单个源文件,这个源文件就是一个编译单元。这个编译单元会被编译成为一个与cpp 文件名同名的目标文件(.o或是.obj) 。连接程序把不同编译单元中产生的符号联系起来,构成一个可执行程序。

  • 一個translation unit由"一個"source file,及其引入的數個header files,減去那些因預處理而被忽略的代碼。
  • 一個translation unit會被編譯成一個object file,library,或是可執行檔。

链接属性

链接属性针对标识符而言,对象通常是变量、函数名、形参。一共分三种-none(无)、external(外部)和 internal(内部)。链接属性不仅意味着能不能访问变量,也意味着能不能在文件中定义同名变量或函数名。

  • none:函数形式参数和代码块内声明的变量在缺省情况下具有none链接属性 。
  • external:并非声明于代码块内的变量,缺省情况下具有external链接属性,(函数名也不包含于任何代码块)。
  • internal:缺省情况下,除了上述说明的external属性以外,其余均为none。因此internal属性出现在static修饰之后。

匿名 Namespace和static 的区别

  • 在C中,给全局变量,函数,加上 static,表示当前函数和变量的linkage 为 internal,这样,就可以在不同的unit 中定义同名的函数和变量了。但是,加了static 的函数和变量就不用作为模板费类型参数了。(模板的非类型参数,必须是编译期确定的,exterl 性质的,整形常量或者指针常量)
  • 在C++中,我们希望同样解决这种在不同的编译unit中定义同名的函数和变量的,这样就有了匿名的namespace, C++会为每个匿名的namespace生成一个唯一的 名字: __UNITQUE_NAMESPACE_NAME,并会再定义时后,加上 using namespace __UNITQUE_NAMESPACE_NAME,这样也就就解决了变量和函数名字冲突问题。这里解决冲突的同时,并不会给函数和变量限定为 internal linkage。

在阐述了上面的static 和 nick name namespace 后,

  • 需要备注第一点是,C++规定,字符常量默认是 internal 的,这个和全局函数和其他全局变量有点点不一样。
  • 这里需要备注的第二点是:编译单元,给函数和变量加上 static ,并不是说,头文件中的函数和变量#include 到其他文件后,就不能使用了,这里的单元和文件作用域是不同的,编译单元,可以理解为生成的一个obj 文件。内部链接指的是只能在一个obj文件中使用,外部链接指的是,可以跨obj使用。同一个obj内的函数和变量不管是什么链接性质,都必须名字惟一。引用其他obj的函数和变量,只需要在 使用前用 extern 声明指定,在链接阶段,连接器会去其他的obj中进行查找外部链接符号。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值