January 12th, 2013

本文为原创,首发于我的cnblog:.NET程序员的C情结(二)

C多文件编译、作用域和存储周期

所谓的编译,分为两个步骤:编译和链接

编译有两个过程:

  1. 预编译:处理#…的语句。#define的宏替换、#if条件编译、#include只是简单的把对应的文件内容复制到#include语句的位置
  2. 单元源代码编译:随后编译器对每个cpp文件(在预编译阶段已经将#include的文件复制完成)单独编译成模块(.obj/.o等),在这个过程中除了语法检查外,还要在本cpp文件中检查调用函数或引用变量是否声明过。最后生成的模块开头会有一个符号表,其中包括了本模块定义的函数或变量在本模块中的偏移量;以及本模块引用的外部变量或函数(称为unsolved symbol)。

链接就是将多个模块文件链接成最后的目标程序。链接过程中需要检查每个模块中的函数声明或变量声明的实际位置。比如模块A声明了一个全局变量global其实是在模块B中定义的,那么链 接器需要把模块A对global的引用地址替换成模块B定义的global的地址。

链接过程又分为符号解析和重定位:

  1. 符号解析:解决各个模块符号表中的unsolved symbol。
  2. 重定位:模块被拼接起来之后,模块符号表中的符号地址将不再正确(因为模块拼接起来后,本来的0偏移,变成了有偏移量)。重定位的任务就是将这些模块的导出符号和导入符号的引用地址最终填写完整。

在链接阶段出现最多的错误主要有:引用未找到、重复定义。

  • 引用未找到:就是在符号解析阶段没有找到unsolved symbol。针对上面global的例子,这个错误时因为链接器无法在其他任何模块中找到global这个变量的定义。
  • 重复定义:对于全局级别的变量,无论程序由多少个模块组成,对同一个变量可以由多个声明但只能有一个定义。重复定义的情况通常是因为不同模块定义了相同的变量。

另外需要补充的是:在编译模块的过程中,C++编译器为了支持重载,会对变量名或函数名进行“重命名”,比如会把fun这个名字变得面目全非,可能是fun@aBc_int_int#%$也可能是别的。extern "C" void fun(int a, int b); 则告诉编译器在编译fun这个函数名时按着C的规则去翻译相应的函数名而不是C++的。

变量

变量按照存储周期(生命周期)可以分为:应用程序级和代码块级。在函数外定义的变量都是应用程序级的,因为他们是在编译阶段被作为代码的一部分写入到符号表中的,在模块中有固定的偏移量;代码块级变量是在函数中定义的,运行时动态在栈上创建和销毁的。

变量按照作用域分为:外部变量、自动变量、静态变量。外部变量是程序每个模块在任何时候都能访问;自动变量与代码块级变量一样;静态变量是在本模块的任何时候都能访问的,特殊的情况是在函数里面定义的静态变量,这种变量的生命周期是整个应用程序级,但是只有定义它的函数可以访问。

补充:

  • 有些人喜欢把全局变量的声明和定义放在一起,这样做,如果这个头文件被include的超过1次,在链接阶段会出现重复定义的错误。
extern char g_str[] = "123456";
  • 用static修饰的全局变量,链接器对其是“不可见”的。
  • 单独const修饰变量与static一样,具有模块级别的作用域;但static不能与extern共用,const可以与extern公用使得变量变成全模块可见。

一些原则:

  • 头文件不要放定义,只放声明,定义放在cpp文件中
  • static变量最好放在.cpp中,防止变量被其他模块重复定义,浪费空间

参考资源

http://www.diybl.com/course/3_program/c++/cppsl/20071119/86983.html http://hi.baidu.com/zengzhaonong/blog/item/30a47460eb8b0048eaf8f822.html http://www.cppblog.com/woaidongmao/archive/2008/11/07/66254.aspx


1块2块也是钱,小额赞助