C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C语言 链接器

C语言 超详细讲解链接器

作者:鹿九丸

在C语言中,一个重要的思想就是分别编译,即若干个源程序能够在不一样的时候单独进行编译,而后在恰当的时候整合到一块儿。可是链接器通常是与C编译器分离的,链接器如何作到把若干个C源程序合并成一个总体呢,我们一起来看看

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的内存布局如下图所示:

image-20220304221554347

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

image-20220304221842175

修改方法如下图所示:

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

也可以这样进行修改:

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

6 头文件

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

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

您可能感兴趣的文章:
阅读全文