本帖最后由 zxs2000 于 2012-4-11 14:27 编辑
由于昨晚听课是用手机,所以以下内容是根据微群http://q.weibo.com/316539整理的:
我们今天继续讨论链接器相关的知识
John Lee:
下面的语句: int bar; 在声明对象的同时,也“定义”了这个对象。我们就简单地说“定义(变量)”就行了
John Lee:
如果这个定义的位置在任何函数之外,那么它就定义了一个“外部对象”,对象的名称是 bar,类型是 int。
John Lee:
既然是“定义”,那么就要为这个对象分配存储空间。分配的存储空间,需要有两个基本属性才能确定: 1、起始地址 2、长度
John Lee:
这样做理由很简单:编译器被设计为可以分别编译多个源程序的,那么它就不知道最终的目标(target)系统中,到底有多少个外部对象。
John Lee:
而链接器则是被设计来将全部的目标文件(.o)以及库文件,整合输出到最终的执行文件,它自然知道构成执行文件所需的全部对象信息,那么为各个对象安排起始地址,便是很容易的事。
John Lee:
我们继续讨论外部对象,刚才说到了声明并且同时定义外部对象。
John Lee:
那么,下面的语句: extern int bar;
John Lee:
这个语句也是声明对象,但并不定义它,按语法,编译器将不会为这个对象分配空间,但程序中,访问该对象的表达式,编译器会予以承认(假设语**确的话),并且编译器会在其生成的.o文件中,说明一个未定义的符号(就是对象的名称),但没有其它相关信息,如长度、类型等等。
John Lee:
如果在所有的源程序中都没有“定义”这个对象 bar,那么链接器将会报错:“未定义的符号 bar”。
John Lee:
实际上就是要求我们在源程序中,必须定义这个外部对象,以保证通过链接。
John Lee:
假设我们定义了 bar,通过了链接,就能保证程序逻辑的正确性吗?
John Lee:
刚才说了,编译器在.o文件中说明一个未定义的符号时,没有说明长度、类型等相关信息。
John Lee:
我们来做一个实验: // a.c 文件 #include <stdio.h> extern int bar; int main(void) { printf("%d\n", bar); } ------------- // b.c 文件 char bar = -1;
John Lee:
定义了为啥 没有长度,类型的信息呢 -------------- 这个问题,要考古了。
John Lee:
目标文件格式中,就没有相关的信息。
John Lee:
上面的程序,编译链接都应该都完全通过。
John Lee:
但程序显然有很严重的错误(隐患)。
John Lee:
甚至把 b.c 文件写成这样: void bar(void) { } 也可以通过编译链接。
John Lee:
对付这种问题的正确方法是,在头文件中声明外部对象(函数和变量等),在某个.c文件中,定义这个外部对象:
John Lee:
// c.h 文件 extern int bar; // 声明全局变量 bar --------------- // a.c 文件 #include "c.h" int main(void) { printf("%d\n", bar); }
John Lee:
// b.c 文件 #include "c.h" int bar; // 定义全局变量 bar,也可以初始化, // 由于在 c.h 中已经声明了 bar,所以此处的“定义”必须完全符合声明。
John Lee:
就是为了预防声明和定义的不一致
John Lee:
某些同学可能有一个小疑问,b.c 经过编译预处理展开后,应该是这样的: extern int bar; int bar; 这样合法吗?完全合法。
John Lee:
如果同一外部对象,在多个源程序或者头文件中被定义(注意,相当多的菜鸟在头文件中定义变量),链接器又将如何处理呢?
John Lee:
例如: // c.h int bar; int foo(void); ----------- // a.c #include "c.h" int main(void) { printf("%d\n", foo() + bar); }
John Lee:
-------------- // b.c #include "c.h" int foo(void) { return bar; }
John Lee:
我们分析一下,编译器在编译 a.c 时,在生成的 a.o 里,说明了一个外部对象 bar。在编译 b.c 时,在生成的 b.o 里,也说明了一个外部对象 bar。
John Lee:
当链接时,链接器发现需要链接的两个文件(a.o 和 b.o)中,都存在外部对象 bar 的定义。
John Lee:
两个.o文件里的 bar, 都是由 c.h 引来的
John Lee:
大多数链接器会报错:“多次定义符号 bar”。
John Lee:
我们把a.c展开,得到: int bar; int foo(void); int main(void) { printf("%d\n", foo() + bar); }
John Lee:
然后把b.c展开,得到: int bar; int foo(void); int foo(void) { return bar; }
John Lee:
好,继续,刚才说到,大多数链接器会报错:“多次定义符号 bar”。
John Lee:
但有一个规则,大家要记住,库文件中的.o,如果没有没引用到的话,即使其中有重复定义的符号,也不会报错。
??
John Lee:
typedef 的,都属于“类型定义”,而不属于对象定义。
John Lee:
需要分配存储空间的,必须是对象(变量,函数等)。
John Lee:
无用的 typedef, 除了降低编译速度外,没有任何副作用
John Lee:
如果你使用了 typedef 所定义的(新)类型,来定义对象(变量)的话,那么就会分配存储空间了。
John Lee:
例如: #include <string.h> size_t strlen(const char* str) { ..... } int main() { printf("%d\n", strlen("hello, world.\n"); } strlen()是标准库函数,但并不妨碍我们自己实现它
John Lee:
链接标准库时,尽管库里已经有了strlen()函数的定义,但并不会造成符号重复定义的错误。
John Lee:
.o 文件,是有优先级的,库中的.o,比源文件(工程中的)生成的.o,优先级要低一些。
John Lee:
源程序的.o,是“必须”链接的,而库中的.o,则是“需要”才链接。
John Lee:
而且,如果库中.o的符号,与源程序.o的符号冲突,那么链接器会优先使用源程序.o中的符号。
John Lee:
问题: // a.c 工程源文件 int bar = 1; int foo(void); int main(void) { printf("%d\n", foo() + bar); } ---------------- // b.c 这个文件编译作为库。 int baz = 2; int foo(void) { return baz; } int bar; int qux(void) { return bar; } 编译链接会出问题吗?要说明原因。
John Lee:
如果某个对象与其它对象,都在同一个段中,那么当此对象被链接时,其它对象也会(必须)被链接的。
John Lee:
a.
c调用了foo() 那么foo()必须要被链接,链接器在b.c的库文件中找到了foo()函数的“定义”。
John Lee:
然后foo()函数访问了baz变量。
John Lee:
导致baz变量也必须被链接
John Lee:
而bar,则与baz处于同一个数据段中
John Lee:
导致b.c的bar也被迫链接了
John Lee:
而链接器在a.c中早已发现了bar定义,现在b.c又出现了bar定义,于是。。。。。。
John Lee:
教大家这些知识,是为了大家(水平上了台阶)以后,做自己的库文件时,需要注意的地方。
|