在C中,我们一开始学习的内容大多是如何定义一个变量,我们还会学习初始化和定义的区别,在我们学习到多模块编程的时候,我们还会学习声明变量,并进一步比较变量声明,变量定义,变量初始化的区别。

在进一步学习C的编译时,我们在学习多个C文件生成的多个可重定向目标文件链接的时候,又会学习到相关符号的内容,有关符号的划分又会划分成“全局符号,外部符号,局部符号”以及“强符号,弱符号”。那这些名词之间又有什么区别和联系呢?

3.1 变量声明,变量定义,变量初始化的区别

3.1.1 变量声明

简单来说,用extern 修饰的且没有显式赋值的为声明,如下:

  1. extern int a; //the statement of int type a

为什么要有变量的声明?

变量的声明其实是为了告诉编译器,在链接阶段一定会在其他的模块存在一个名为a的int类型的变量定义,到时候这个a就是指那个变量,这相当于是一种对于编译器的“承诺”,即如若当链接阶段没有找到所链接的其他模块中存在类型为int且名为a的变量的定义,那链接器就会报错。

这样做的好处很明显,就是为了多个模块之间共享数据。本模块中可以使用其他模块的全局变量。

注意:声明可以多次声明,因为其最终只会指向一个定义的变量。

3.1.2 变量定义

如下:

  1. int a;//the definiton of int a

这里声明并定义了一个变量,类型为int ,名字为a。

与声明不同,一个变量在同一个模块中只能定义一次。

3.1.3 变量初始化

如下:

  1. int a =1;

这里定义并初始化了一个int类型的变量a。

将变量区分为声明和定义似乎很好理解,但将变量区分为初始化和未初始化的这一语法在初学时似乎有些困惑,似乎这样的语法没有什么意义,但其实这一特点在ELF格式的可重定向目标文件存储以及链接中都发挥着作用。

在ELF格式的可重定向目标文件中,甚至专门将未初始化的全局变量和初始化的全局变量放在两个不同的节中,对于未初始化的全局变量(包括声明和定义)会将其放在.bss节中,而初始化的变量则放在.data中,这时因为对于未初始化的全局变量在目标文件中是不占用存储空间的,因为其没有显式地赋初值,所以不需要将其初值进行保存。因此,在目标文件中区分全局变量是否初始化可以节省磁盘空间。如下图:
3.C中的变量以及链接过程中的符号 - 图1
注意x(初始化全局变量)与y(未初始化全局变量)所在的节

其次,在链接过程中,全局变量是否初始化将进一步导致其变量名代表的符号是是强符号还是弱符号,这也会导致不同的链接结果,这在后续讨论。

3.2 链接过程中的符号(强弱符号,全局,外部,局部符号)

对符号的讨论似乎没有对于变量的讨论多,因为当我们在讨论变量时,我们往往是站在编译器的角度来看待,例如,只有先定义变量,才能使用变量,这是从语法层面来说的,违反这一语法在编译阶段就会报错。

但当我们讨论符号时,我们往往站在链接器的角度,因为符号解析正是在这一过程中完成。

当C文件的编译过程走到最后一个阶段——链接时,实际上主要的工作已经差不多完成了,但当我们采取多模块编程时,多个模块之间的数据传递还没有完成,前面的直到编译阶段的工作只着眼于单个C文件并成功生成了每个C文件对应的可重定向目标文件,链接的工作就是符号解析和重定位,走完这最后一公里。

首先,链接器需要区分不同的符号,在C中,符号有三种:
1.全局符号:非静态函数名以及非static修饰的变量名。
2.外部符号:其他模块定义的外部函数名和外部变量名。(这里注意,在本模块看来,该符号为外部符号,但在定义该函数或者全局变量的模块来说,该符号也是全局符号
3.带static属性的函数名和变量名,为本地符号。

上述符号均会在上图中的.symtab节中进行存储。

强弱符号的讨论只包括每个模块的全局符号

那什么样的符号是强符号,什么样的符号是弱符号呢?

其实很简单:初始化的全局变量名和函数名为强符号,未初始化的全局变量名为弱符号。

其实这样的划分意味很明显了,就是要区分初始化的全局变量名和未初始化的全局变量名。

在语法层面来看,为什么要区分?

因为在链接过程中,只允许有一个强符号,即对于多个全局变量定义,只允许存在一个初始化,其余的符号以强符号为准,即链接之后的可执行目标文件,会按照强符号的类型进行分配空间等。

对于多个弱符号,则会随机以其中一个弱符号为准。

从语法层面上讲,确实这样的划分是有意义的,但抛开语法,这样做有什么意义呢?

实际上,这么费劲地区分强弱符号还会引入一种常见的错误:多重定义。

即假设a模块中定义一个int a; b模块中定义并初始化 double a = 1.0,实际上,a与b链接后a的类型就变成了double,但a模块并不知情,如若c模块用了a模块中的a全局变量,只阅读a模块的代码的话c模块会以为a类型为int,但实际上并不是,这就可能导致错误。

实际编程中,我们甚至会用编程规范来避免这样的多重定义,也就是避免出现弱符号,甚至避免使用全局变量。即我们尽量遵循如下的编程规范:
1.尽量使用本地变量,因为不向外暴露的变量不太会因为链接出错。
2.全局变量尽量赋初值使之成为强符号,这样出现多重定义时可以及时报错。
3.外部全局变量使用extern来声明,而尽量不要利用强弱符号性质来引用外部模块的全局变量。

综上,强弱符号并非一种用于简便编程的好的性质,事实上,这个性质往往会带来错误,因此我们在编程时尽量避免出现强弱符号之分而尽量使用强符号或者本地符号。