问题:
1、为什么外部变量的定义性声明只能在一个编译单元中出现?
2、为什么同一个函数的定义不能出现在多个编译单元中?
3、为什么类定义应当写在头文件中,可被多个源文件包含?
一、编译
源文件(如:.cpp或.c文件)经过编译处理后生成目标文件(如:.obj文件)的过程称为编译。编译是对一个个单独的源文件进行处理,每个文件都对应生成一个目标文件或构成一个编译单元,不同的编译单元是互不影响的。
目标文件是用来描述程序在运行中需要调入内存中的内容。其中内容分:代码和数据。目标文件中包含代码段和数据段。
代码段(.text)是源文件中函数编译后生成的目标代码,普通函数和类成员函数的目标代码都在代码段中。
数据段是源文件中具有静态生存期的变量和对象的描述,它分成初始化数据段(.data)和未初始化数据段(.bss)。定义同时又赋初值的静态生存期变量和对象(通过构造函数赋初值的不属于此)的值就存放在初始化数据段。这些变量和对象在运行时占多少内存空间,目标文件中就要提供多大空间存放它们的初值。其它未初始化的变量和对象都放在未初始化的数据段中。目标文件中不需要为它们提供空间存储信息,只要记录这个段的大小。
对于只声明而没有定义的全局变量和函数则不会出现在代码段或数据段中,它们均存放在目标代码的符号表中。
符号表是标识符名称和它们在段中对应的地址关联表,对定义的和未定义的变量、对象及函数在符号表中都可以找到。已定义的地址用它所在的段及相对于该段的首地址偏移来表示。未定义的只有符号名而没有对应地址。对于静态生存期的引用及函数调用所使用的地址都是未定义的,它们要到连接阶段才能确定。连接时确定需要相关的信息来关联,这些信息称为重定位信息。
二、连接
将各编译单元的目标文件和运行库中被调用的单元合并在一起的过程称为连接。此时不同编译单元的代码段和数据段都合并,运行时代码和静态数据占据的内存空间全部知道,地址也均分配。
符号表也合并起来,利用重定位信息,未定义地址的则可分配有效地址。连接生成的可执行文件,也是有各段的信息,此时所有的指令地址都是有效地址,符号表可以出现在可执行文件中也可以不出现,如果出现对调试程序是有帮助的。
三、执行
程序的执行是以进程为单位,程序一次动态执行过程称为一个进程。
现在可以对文章前面的三个问题进行答复了,这三个问题的根源都一样:对同一个符号,合并时的地址,是要在有定义的编译单元中的相对地址来确定的,如果在多个编译单元都有定义的话,它的地址就无所适从,此时会出现符号定义冲突的连接错误!如果所有的编译单元都没有定义的话,则会出现符号未定义连接错误!
附:需要构造函数初始化的静态生存期变量和对象,它们的初始化需要编译器生成专门的代码来调用构造函数,何时调用也由编译器控制。命名空间作用域中的此类变量和对象,在执行主函数之前由引导代码调用。局部作用域,初始化代码会内嵌在函数体中,并用静态标志标识出已初始化,以确保初始化代码只执行一次。
《C++语言程序设计》 郑莉 清华大学出版社