C语言 超详细讲解链接器

目录
  • 1 什么是链接器
  • 2 声明与定义
  • 3 命名冲突
    • 3.1 命名冲突
    • 3.2 static修饰符
  • 4 形参、实参、返回值
  • 5 检查外部类型
  • 6 头文件

1 什么是链接器

典型的链接器把由编译器或汇编器生成的若干个目标模块,整合成一个被称为载入模块或可执行文件的实体–该实体能够被操作系统直接执行。

链接器通常把目标模块看成是由一组外部对象组成的。每个外部对象代表着机器内存中的某个部分,并通过一个外部名称来识别。因此,==程序中的每个函数和每个外部变量,如果没有被声明为static,就都是一个外部对象。==某些C编译器会对静态函数和静态变量的名称做一定改变,将它们也作为外部对象。由于经过了“名称修饰”,因此它们不会与其它源程序文件中的同名函数或同名变量发生命名冲突。

2 声明与定义

extern int a;

上面的这段代码并不是对a的定义,而是说明a是一个外部整型变量。

注意:引入之后,假如引入的位置在函数之外,就相当于在那个位置定义了全局变量,同样遵循局部变量优先原则,如果引入位置在某个函数之内,就相当于是一个局部变量,作用域与那个地方定义的局部变量相类似,此处讨论声明周期没有任何意义。

int a;
extern int a;

上面的这两条语句既可以是在同一个源文件中,也可以位于程序的不同源文件之中。

==注意:每个外部变量只能定义一次。==如果外部变量的多个定义各指定一个初始值,例如:

int a = 7;

出现在一个源文件中,而

int a = 9;

出现在另一个源文件中,大多数系统都会拒绝接收该程序。但是,如果一个外部变量在多个源文件中定义却没有指定初始值,那么**某些系统会接受这个程序,而另外一些系统则不会接受。**所以,每个外部变量必须只定义一次。

3 命名冲突

3.1 命名冲突

如果在两个不同的源文件中都包括了定义

int a;

那么它要么表示程序错误(如果链接器进制外部变量重复命名的话),要么在两个源文件中共享a的同一个实例(无论两个源文件中的外部变量是否应该被共享)。即使其中a的一个定义是出现在系统提供的库文件中,也仍然进行同样的处理。

3.2 static修饰符

static int a;

static修饰a之后,a的作用域将被限制在一个源文件中,对于其它源文件,a是不可见的,且无法再被extern所引用,当然,static也适用于函数。使用static之后,我们就可以在其它的源文件中定义和这个已经被static修饰后的同名的变量或者函数。

4 形参、实参、返回值

如果我们使用的函数并未进行声明,但是已经在后面进行了定义,此时会默认函数返回类型为int型,这会造成极其严重的后果。

使用的函数如果在使用之前并未定义或者可能在其他的文件中,那么就要进行声明,函数声明的目的就是告知编译器函数的返回值的类型。

注意:如果一个函数没有float、short、或者char类型的参数,在函数声明中完全可以省略掉参数类型的说明(注意,函数定义中不能省略参数类型的说明)。这种做法依赖于调用者能够提供数目正确且类型恰当的实参。这里,“恰当”并不意味着“等同”:float类型的参数会自动转换为double类型,short或者char类型的参数会自动转换为int类型。

在ANSI C标准发布之前,常常会有下面的这种声明和定义函数的方式:

int isvowel();//声明函数的方式
int isvowel(c)
		char c;
{
	return c =='a' ;
}

实际上,上面这种写法与下面这种写法是等价的:

int isvowel(int i)
{
	char c;
	return c=='a';
}

上述两种方式在VS2019中都是支持的。

看下面的例子:

#include<stdio.h>
int main()
{
	int i;
	char c;
	for (i = 0; i < 5; i++)
	{
		scanf("%d", &c);
		printf("%d ", i);
	}
	printf("\n");
	return 0;
}

表面上,这个程序从标准输入设备读入5个数,在标准输出设备设备上写5个数:

0 1 2 3 4

实际上,这个程序并不一定得到上面的结果。例如,在某个编译器上,它的输出是(当然,在VS2019环境下程序会崩溃,因为非法修改了内存空间)

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才可以正常的运行,最后终止循环。

5 检查外部类型

注意:保证一个特定类型的所有外部定义在每个目标模块中都有相同的类型,“相同的类型”也应该是严格意义上的相同。

例如,在一个文件中包含定义:

char filename[] = "/etc/passwd";

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

extern char *filename;

在定义时,filename是一个字符数组的名称。尽管在一个语句中引用filename的值将得到指向该数组起始元素的指针,但是filename的类型是”字符数组“,而不是字符指针。在第二个声明中,filename被确定为一个指针。这两种方式使用存储空间的方式是不同的,它们无法以一种合乎情理的方式共存。第一个例子字符数组filename的内存布局如下图所示:

第二种方式字符指针filename的内存布局如下图所示:

修改方法如下图所示:

char filename[] = "/etc/passwd";
extern char filename[];

也可以这样进行修改:

char*filename = "/etc/passwd";
extern char *filename;

6 头文件

注意:每个外部对象只在一个地方声明,这个声明的地方一般就在头文件种,需要用到该外部对象的所有模块也应该 包括在这个头文件。特别指出的是,定义该外部对象的模块也应该包括这个头文件。

到此这篇关于C语言 超详细讲解链接器的文章就介绍到这了,更多相关C语言 链接器内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 举例讲解C语言链接器的符号解析机制

    1. 符号分类 (1)全局符号:非静态全局变量,非静态函数 (2)外部符号:定义于其它模块,而被本模块引用的全局变量和函数 (3)本地符号:静态变量(包括全局和局部),静态函数 对于静态局部变量,编译器会为其生成唯一的名字.如x.fun1,x.fun2.本地符号对链接器来说是不可见的. 2. 符号决议 当编译器遇到一个不是本模块定义的符号时,会假设该函数由其它模块定义,并生成一个链接器符号表条目,交由链接器处理.如果链接器在它的任何输入模块都没有找到该符号,会给出一个类似undefined re

  • C语言 超详细讲解链接器

    目录 1 什么是链接器 2 声明与定义 3 命名冲突 3.1 命名冲突 3.2 static修饰符 4 形参.实参.返回值 5 检查外部类型 6 头文件 1 什么是链接器 典型的链接器把由编译器或汇编器生成的若干个目标模块,整合成一个被称为载入模块或可执行文件的实体–该实体能够被操作系统直接执行. 链接器通常把目标模块看成是由一组外部对象组成的.每个外部对象代表着机器内存中的某个部分,并通过一个外部名称来识别.因此,==程序中的每个函数和每个外部变量,如果没有被声明为static,就都是一个外部

  • C语言超详细讲解轮转数组

    目录 题目描述 实例 解题思路 1. 先整体逆转 2.逆转子数组[0, k - 1] 3.逆转子数组[k, numsSize - 1] 易错点 代码 题目描述 给你一个数组,将数组中的元素向右轮转 k 个位置,其中 k 是非负数.OJ链接 实例 1.实例1 输入: nums = [1,2,3,4,5,6,7], k = 3输出: [5,6,7,1,2,3,4]解释:向右轮转 1 步: [7,1,2,3,4,5,6]向右轮转 2 步: [6,7,1,2,3,4,5]向右轮转 3 步: [5,6,7

  • C语言超详细讲解数据结构中双向带头循环链表

    目录 一.概念 二.必备工作 2.1.创建双向链表结构 2.2.初始化链表 2.3.动态申请节点 2.4.打印链表 2.5.销毁链表 三.主要功能 3.1.在pos节点前插入数据 尾插 头插 3.2.删除pos处节点数据 尾删 头删 3.3.查找数据 四.总代码 List.h 文件 List.c 文件 Test.c 文件 五.拓展 一.概念 前文我们已经学习了单向链表,并通过oj题目深入了解了带头节点的链表以及带环链表,来画张图总体回顾下: 在我们学习的链表中,其实总共有8种,都是单双向和带不带

  • C语言 超详细讲解库函数

    目录 1 返回整数的getchar函数 2 更新顺序文件 3 缓冲输出与内存分配 4 库函数 练习 1 返回整数的getchar函数 代码: #include<stdio.h> int main() { char c; while((c = getchar())!=EOF)//getchar函数的返回值为整型 putchar(c); return 0; } 上述代码有三种可能: 某些合法的输入字符在被"截断"后使得c的取值与EOF相同,程序将在复制的中途停止. c根本不可能

  • C语言 超详细讲解算法的时间复杂度和空间复杂度

    目录 1.前言 1.1 什么是数据结构? 1.2 什么是算法? 2.算法效率 2.1 如何衡量一个算法的好坏 2.2 算法的复杂度 2.3 复杂度在校招中的考察 3.时间复杂度 3.1 时间复杂度的概念 3.2 大O的渐进表示法 3.3 常见时间复杂度计算举例 4.空间复杂度 5. 常见复杂度对比 1.前言 1.1 什么是数据结构? 数据结构(Data Structure)是计算机存储.组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合. 1.2 什么是算法? 算法(Algorit

  • C语言超详细讲解字符串相乘

    目录 前言 一. 分析思路 二.使用步骤 1.代码如下 2.memset函数 三.总结 前言 我们已经知道,正常的两位整形数据通过*相乘,C语言中int为4字节,32bit(字节),其机器码第一位为符号位,余下31位表示数字,表示范围:-2^31(-2147483648)~2^31-1(2147483647),但超过了这个范围我们该如何做呢? 提示:将数字以字符串的形式进行操作 一. 分析思路 示例: 我们把每一个数都看成是一个字符串,每一个元素为十进制数字所对应的字 符,由于是后面的元素先进行

  • C语言超详细讲解排序算法上篇

    目录 1.直接插入排序 2.希尔排序(缩小增量排序) 3.直接选择排序 4.堆排序 进入正式内容之前,我们先了解下初阶常见的排序分类 :我们今天讲前四个! 1.直接插入排序 基本思想:当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排 序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移! 直接插入排序的特性总结: 1. 元素集

  • C语言超详细讲解栈与队列实现实例

    目录 1.思考-1 2.栈基本操作的实现 2.1 初始化栈 2.2 入栈 2.3 出栈 2.4 获取栈顶数据 2.5 获取栈中有效元素个数 2.6 判断栈是否为空 2.7 销毁栈 3.测试 3.1测试 3.2测试结果 4.思考-2 5.队列的基本操作实现 5.1 初始化队列 5.2 队尾入队列 5.3 队头出队列 5.4 队列中有效元素的个数 5.5 判断队列是否为空 5.6 获取队头数据 5.7 获取队尾的数据 5.8 销毁队列 6.测试 6.1测试 6.2 测试结果 1.思考-1 为什么栈用

  • C语言超详细讲解栈的实现及代码

    目录 前言 栈的概念 栈的结构 栈的实现 创建栈结构 初始化栈 销毁栈 入栈 出栈 获取栈顶元素 获取栈中有效元素个数 检测栈是否为空 总代码 Stack.h 文件 Stack.c 文件 Test.c 文件 前言 栈的概念 栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作.进行数据插入和删除操作的一端称为栈顶,另一端称为栈底.栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则.有点类似于手枪弹夹,后压进去的子弹总是最先打出,除非枪坏了. 压栈:栈的插入

  • C语言超详细讲解队列的实现及代码

    目录 前言 队列的概念 队列的结构 队列的应用场景 队列的实现 创建队列结构 队列初始化 队列销毁 入队列 出队列 队列判空 获取队列元素个数 获取队列头部元素 获取队列尾部元素 总代码 Queue.h 文件 Queue.c 文件 Test.c 文件 前言 队列的概念 队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 入队列:进行插入操作的一端称为队尾 出队列:进行删除操作的一端称为队头 队列和前文所学的栈

随机推荐