一个C程序可能是由多个分别编译的部分组成,这些不同部分通过连接器合并成一个整体。在本章中,我们将考查一个典型的连接器,注意它是如何对C程序进行处理的,从而归纳出一些由于连接器的特点而可能导致的错误。

4.1 什么是连接器

 
  在C语言中,一个重要的思想就是分别编译,即若干个源程序可以在不同的时候单独进行编译,然后通过连接器整合到一起。但是连接器一般是与C编译器分离的,连接器如何做到把若干个C源程序合并成一个整体呢?
尽管连接器并不理解C语言,但它理解机器语言和内存布局。只要编译器将C源程序“翻译”成对连接器有意义的形式,这样连接器就能够“读懂”C源程序了。
 
  典型的连接器把由编译器或汇编器生成的若干个目标模块,整合成一个被称为载入模块或可执行文件的实体,该实体能够被操作系统直接执行。其中,某些目标模块是直接作为输入提供给连接器的;而另外一些目标模块
则是根据连接过程的需要,从包括有类似printf函数的库文件中取得的。
 
  连接器通常把目标模块看成是由一组外部对象组成的。每个外部对象代表着机器内存中的某个部分,并通过一个外部名称来识别。因此,程序中的每个函数和每个外部变量,如果没有被声明为static,就都是一个外部对象。某些C编译器会对静态函数和静态变量的名称做一定改变,将它们也作为外部对象。由于经过了“名称修饰”,所以它们不会与其他源程序文件中的同名函数或同名变量发生命名冲突。
 
  大多数连接器都禁止同一个载入模块中的两个不同外部对象拥有相同的名称。然而,在多个目标模块整合成一个载入模块时,这些目标模块可能就包含了同名的外部对象。连接器的一个重要工作就是处理这类命名冲突。
 
  处理命名冲突的最简单办法就是干脆完全禁止。对于外部对象是函数的情形,这种做法是正确的。一个程序如果包括两个同名的不同函数,编译器根本就不应该接受。而对于外部对象是变量的情形,问题就变得困难了。
不同的连接器对这种情形有着不同的处理方式。
 
  现在讲讲连接器是如何工作的?
 
  连接器的输入是一组目标模块和库文件。连接器的输出是一个载入模块。连接器读入目标模块和库文件,同时生成载入模块。对每个目标模块中的每个外部对象,连接器都要检查载入模块,看是否已有同名的外部对象。
如果没有,连接器就将该外部对象添加到载入模块中;如果有,连接器就要开始处理命名冲突。
 
  除了外部对象之外,目标模块还可能包括了对其他模块中的外部对象的引用。例如:一个调用了函数printf的C程序所生成的目标模块,就包括了一个对函数printf的引用。可以推测得出,该引用指向的是一个位于某个库文件中的外部对象。
在连接器生成载入模块的过程中,它必须同时记录这些外部对象的引用。当连接器读入一个目标模块时,它必须解析出这个目标模块中定义的所有外部对象的引用,并作出标记说明这些外部对象不再是未定义的。

4.2 声明与定义

 
  下面的声明语句:

    int a;

  如果其位置出现在所有的函数体之外,那么它就将被称为外部对象a的定义。
  下面的声明语句:

    extern int a;

  并不是对 a 的定义。这个语句仍然说明了 a 是一个外部整型变量,但是因为它包括了extern关键字,这就显式地说明了a的存储空间实在程序的其它地方分配的。从连接器的角度来看,上述声明是对一个外部对象 a 的引用,而不是对 a 的定义。

4.3 命名冲突与static修饰符

 
  两个具有相同名称的外部对象实际上代表的是同一个对象,因此如果在两个不同的源文件中都定义

    int a;
那么将会造成命名冲突,程序报错。(多数情况下)

 
  static修饰符是一个能够减少此类命名冲突的有用工具。例如,以下声明语句:

    static int a;

  a 的作用域被限制在一个源文件内,对于其他源文件, a 是不可见的。static起到了“屏蔽”变量的作用。
  static不仅适用于变量,也适用于函数。 如果函数f需要调用另一个函数 g,而且只有函数 f 需要调用函数 g,我们可以把函数 f 与函数 g 都放到同一个源文件中,并且声明函数 g 为static。

    static int g(x)
    {
        /* 函数体 */
    }

    void f()
    {
        b = g(a);
    }

  我们可以在多个源文件中定义同名的函数 g ,只要所有的函数 g 都被定义为static,或者仅仅只有一个函数不是static。因此,为了避免可能出现的命名冲突,如果一个函数仅仅被同一个源文件中的其它函数调用,我们就应该声明该函数为static。

4.4 形参、实参与返回值

 
  任何C函数都有一个形参列表,列表中的每个参数都是一个变量,该变量在函数调用过程中被初始化。下面这个函数有一个整型形参:

    int    abs(int n)
    {
        return n<0 ? -n: n;
    }

 
  函数调用时,调用方将实参列表传递给被调函数。在下面的例子中, a - b 是传递给函数abs的实参:

    if( abs(a - b) > n )
        printf("difference is out of range\n");

 
  任何一个函数都有返回类型,要么是void,要么是函数生成结果的类型。
 
  因为函数printf与函数sanf在不同情形下可以接受不同类型的参数,所以它们特别容易出错。这里有一个值得注意的例子:

    #include <stdio.h>

    int i;
    char c;
    for(i=0; i<5; i++)
    {
        scanf("%d",&c);
        printf("%d",i);
    }
    printf("\n");

  表面上,这个程序从标准输入设备读入5个数,在标准输出设备上写5个数:
  0 1 2 3 4
  实际上,这个程序不一定得到上面的结果。例如,在某个编译器上,它的输出是:
  0 0 0 0 0 1 2 3 4
 
  为什么呢?问题的关键在于,这里c被声明为char类型,而不是Int类型。当程序要求scanf读入一个整数,应该传递给它一个指向整数的指针。而程序中scanf函数得到的却是一个指向字符的指针,scanf函数并不能分辨这种情况,它只是将这个指向字符的指针作为指向整数的指针而接受,并且在指针指向的位置存储一个整数。因为整数所占的存储空间要大雨字符所占的存储空间,所以字符c附近的内存将被覆盖。所以才会出现这种情况。
  字符c附近的内存中存储的内容是由编译器决定的,本例中它存放的是整数i的低端部分。因此,每次读入一个数值到c时,都会将i的低端部分覆盖为0,而i的高端部分本来就是0,相当于i每次被重新设置为0,循环将一直进行。当到达文件的结束位置后,scanf函数不再试图读入新的数值到c。这时,i才可以正常地递增,最后终止循环。

4.5 检查外部类型

  假定我们有一个C程序,它由两个源文件组成。一个文件中包含外部变量n的声明:

    extern int n;

  另一个文件中包含外部变量n的定义:

    long n;    

  这里假定两个语句都不再任何一个函数体内,因此n是外部变量。
  这是一个无效的C程序,因为同一个外部变量名在两个不同的文件中被声明为不同的类型。(在很大且开发中期很长的C项目工程中有可能出现这种情况)
  当这个程序运行时,可能发生以下情况:

  • 一、C语言编译器能检测到冲突。
  • 二、两者数值在内部表示上一样,例如都是32位,程序很可能正常工作。
  • 三、共享存储空间,long的低端部分赋给了int类型的n,能正常工作。
  • 四、共享存储空间,但是对其中一个赋值掩盖了另一个值,将不能正常工作。
     
      因此,保证一个特定名称的所有外部定义在每个目标模块中都有相同的类型,一般来说是程序员的责任。而且,“相同的类型”应该是严格意义上的相同。例如,考虑下面的程序,在一个文件中包含定义:
    char filename[] = "/etc/passwd";

  而在另一个文件中包含声明:

    extern char *filename;

  尽管在某些上下文环境中,数组与指针非常类似,但它们毕竟不同,所以上面的外部声明是非法的。
 
  有关外部类型类型(extern),另一种容易带来麻烦的方式是忽略了声明函数的返回类型,或者声明了错误的返回类型。例如下面的程序:

    main()
    {
        double s;
        s = sqrt(2);
        printf("%g\n", s);
    }

  这个源文件没有包括对函数sqrt的声明,因此函数sqrt的返回类型只能从上下文进行推断。C语言中的规则是,如果一个未声明的标识符后跟一个开括号(),那么它将被视为一个返回整型的函数。因此,这个程序完全等同于下面的程序:

    extern int sqrt();

    main()
    {
        double s;
        s = sqrt(2);
        printf("%g\n", s);
    }

  当然这种写法是错误的。函数sqrt应该返回双精度类型,而不是整型。因此这个程序的结果也是不可预测的。所以要注意外部类型的声明。

4.6 头文件

 
  有一个好办法可以避免大部分此类问题:每个外部对象只在一个头文件中进行外部声明,需要用到该外部对象的所有模块都应该包括这个头文件。另外,定义该外部对象的模块也应该包括这个头文件。
  在模块源文件中定义一个外部对象:

    char fileName[];

 
  在该模块头文件外部声明该对象:

    extern fileName[];

[C陷阱和缺陷] 第4章 连接的更多相关文章

  1. [C陷阱和缺陷] 第1章 词法“陷阱”

    有感自己的C语言在有些地方存在误区,所以重新仔细把"C陷阱和缺陷"翻出来看看,并写下这篇博客,用于读书总结以及日后方便自身复习. 第1章 词法"陷阱" 1.1 ...

  2. [C陷阱和缺陷] 第3章 语义“陷阱”

    第3章 语义"陷阱"     一个句子哪怕其中的每个单词都拼写正确,而且语法也无懈可击,仍然可能有歧义或者并非书写者希望表达的意思.程序也有可能表面上是一个意思,而实际上的意思却相 ...

  3. [C陷阱和缺陷] 第2章 语法“陷阱”

    第2章 语法陷阱 2.1 理解函数声明   当计算机启动时,硬件将调用首地址为0位置的子例程,为了模拟开机时的情形,必须设计出一个C语言,以显示调用该子例程,经过一段时间的思考,得出语句如下: ( * ...

  4. [C陷阱和缺陷] 第7章 可移植性缺陷

      C语言在许多不同的系统平台上都有实现.的确,使用C语言编写程序的一个首要原因就是,C程序能够方便地在不同的编程环境中移植.   不同的系统有不同的需求,因此我们应该能够预料到,机器不同则其上的C语 ...

  5. [C陷阱和缺陷] 第6章 预处理器

      在严格意义上的编译过程开始之前,C语言预处理器首先对程序代码作了必要的转换处理.因此,我们运行的程序实际上并不是我们所写的程序.预处理器使得编程者可以简化某些工作,它的重要性可以由两个主要的原因说 ...

  6. [C陷阱和缺陷] 第5章 库函数

      有关库函数的使用,我们能给出的最好建议是尽量使用系统头文件,当然也可以自己造轮子,随个人喜好.本章将探讨某些常用的库函数,以及编程者在使用它们的过程中可能出错之处.   5.1 返回整数的getc ...

  7. 我的《C陷阱与缺陷》读书笔记

    第一章 词法“陷阱” 1. =不同于== if(x = y) break; 实际上是将y赋给x,再检查x是否为0. 如果真的是这样预期,那么应该改为: if((x = y) != 0) break; ...

  8. 阅读《C陷阱与缺陷》的知识增量

    版权声明:本文为Focustc原创文章.转载请注明作者及出处. https://blog.csdn.net/caozhankui/article/details/35925939 看完<C陷阱与 ...

  9. C语言学习书籍推荐《C陷阱与缺陷》下载

    下载地址:点我 凯尼格 (作者), 高巍 (译者) <C和C++经典著作:C陷阱与缺陷>适合有一定经验的C程序员阅读学习,即便你是C编程高手,<C和C++经典著作:C陷阱与缺陷> ...

随机推荐

  1. 前端开发:JavaScript---DOM & BOM

    DOM:Document Object Model  文档对象类型 模态框案例 <!DOCTYPE html> <html lang="en"> <h ...

  2. apple网址

    https://developer.apple.com/downloads/index.action#   开发工具下载

  3. 2017 CCPC 杭州 HDU6273J 区间修改(线段树&差分数组)

    http://acm.hdu.edu.cn/downloads/CCPC2018-Hangzhou-ProblemSet.pdf 解析 线段树区间延迟更新 或 差分数组 两个数   统计2和3的最少的 ...

  4. poj-1979 && hdoj - 1312 Red and Black (简单dfs)

    http://poj.org/problem?id=1979 基础搜索. #include <iostream> #include <cstdio> #include < ...

  5. Servlet的会话(Session)跟踪

    以下内容引用自http://wiki.jikexueyuan.com/project/servlet/session-tracking.html: HTTP是一种“无状态”协议,这意味着每次客户端检索 ...

  6. openstack setup demo Image service

    Image service (glance)是openstack中管理vm image的service.本文包含以下内容: overview install overview glance包含以下部分 ...

  7. 使用百度网盘实现自动备份VPS

    http://ju.outofmemory.cn/entry/51536 经过轰轰烈烈的一轮网盘大战,百度网盘的容量已经接近无限(比如我的是3000多G ),而且百度网盘已经开放API,所以用来备份V ...

  8. Word Break II 求把字符串拆分为字典里的单词的全部方案 @LeetCode

    这道题相似  Word Break 推断能否把字符串拆分为字典里的单词 @LeetCode 只不过要求计算的并不不过能否拆分,而是要求出全部的拆分方案. 因此用递归. 可是直接递归做会超时,原因是Le ...

  9. webpack-输出

    输出(Output) 配置 output 选项可以控制 webpack 如何向硬盘写入编译文件. 注意,即使可以存在多个入口起点,但只指定一个输出配置. 用法(Usage) 在 webpack 中配置 ...

  10. 秒懂C#通过Emit动态生成代码 C#使用Emit构造拦截器动态代理类

    秒懂C#通过Emit动态生成代码   首先需要声明一个程序集名称, 1 // specify a new assembly name 2 var assemblyName = new Assembly ...