1源代码完成后,就可以编译生成可执行文件了。负责实现该功能的是编译器。本章围绕编译器的功能,详细介绍从程序编写到运行为止的流程。首先,看一下源文件是如何通过编译转化为可执行文件的。接下来,我们会继续关注可执行文件被加载到内存后的运行机制。此外,还会对程序运行时内存中的栈及堆进行说明。由于篇幅有限,本章只介绍了C语言编译器来编写Windows用的可执行文件(EXE文件),不过其他环境及编程语言基本上是同样的机制。
8.1 计算机只能运行本地代码
# include <windows.h>
# include <stdio.h>
//消息框的标题
char* title = "示例程序1"
// 返回两个参数的平均值函数
double Average (double a, double b) {----------------------------------------(1)
return (a + b) / 2
}
//程序启动位置的函数
int WINAPI WinMain (HINSTANCE h, HINSTACE d, LPSTR s, int m) {--------------(2)
double ave;
char buff[80];
//求解123,456的平均值
ave = Average(123, 456);
//编写显示在消息框中的字符串
sprintf(buff, "平均值 = %f", ave);---------------------------------------(3)
//打开消息框
MessageBox(NULL, buff, title, MB_OK);-----------------------------------(4)
return 0;
}
类似于代码清单8-1这样,用某种编程语言编写得程序就是源代码,保存源代码的文件称为源文件。源代码是无法直接运行的,因为CPU能直接解析并运行的不是源代码而是本地代码的程序。用任何编程语言编写的代码,最后都要翻译成本地代码(见下图)。
8.2 本地代码的内容
Windows中EXE文件的控制内容,使用的就是本地代码。本地代码的内容就是各种数值的罗列,而计算机就是把所有的信息作为数值的集合来处理的。于此同时,计算机指令也是数值的罗列。
8.3 编译器负责转换源代码
能够把C语言等高级编程语言编写的源代码转换成本地代码的程序称为 编译器。每个编写源代码的编程语言都需要其专用的编译器。编译器首先读入代码的内容,然后再把源代码转换成本地代码。编译器中就好像有一个源代码同本地代码的对应表。
根据CPU不同,本地代码的类型也不同。因而,编译器不仅和编程语言的种类有关,和CPU的类型也是相关的。编译器的选择有三大关键词:
- 何种语言
- 用于哪种CPU
- 什么环境下使用
8.4 仅靠编译无法得到可执行文件
编译器转换为源代码后,就会生成本地文件。不过本地文件是无法直接运行的。为了得到可以运行的EXE文件,编译之后还需要进行“链接”处理。Borland C++编译器是bcc32.exe
bcc32 -W -c Sample1.c
上面这条命令就是编译Sample1.c的命令行命令。“-W -c”是用来指定编译Windows用的程序的选项。编译后生成的不是EXE文件,而是扩展名为“.obj”的目标文件。虽然目标文件的内容是本地代码,但却无法直接运行。原因就是当前程序还处于未完成状态。
(3)处sprintf()函数和(4)处的MessageBox()函数在源代码中没有记述这两个函数的处理内容。sprintf()是通过指定格式把数值变换成字符串的函数,MessageBox()是消息框函数。源代码中没有记述这些函数的处理内容。因此,必须将这两个函数的处理内容同Sample1.obj结合。
把多个目标文件结合,生成1个EXE文件的处理就是链接,运行链接的程序就称为链接器。Borland C++的链接器就是ilink32.exe工具。在Windows命令提示符下运行以下命令后,程序所需的目标文件就会被全部链接生成Sample1.exe文件。
ilink32 -Tpe -c -x -aa c0w32.obj Sample1.obj, Sample1.exe,,
import32.lib cw32.lib
8.5 启动及库文件
链接选项“-Tpe -c -x -aa”是指定生成Windows用的EXE文件的选项。在这些选项之后,会指定结合的目标文件。而该命令行中就指定了c0w32.obj、Sample1.obj两个目标文件。c0w32.obj这个目标文件记述的是同所有程序起始位置相结合的处理内容,称为程序的启动。因而,即使程序不调用其他目标文件的函数,也必须要进行链接,并和启动结合起来。
在链接的命令行末尾,存在着扩展名是“.lib”的import32.lib和cw32.lib这两个文件。这是因为sprintf()的目标文件在cw32.lib中,MessageBox()的目标文件在import32.lib中(实际上,MessageBox()的目标文件在user32.dll这个DLL文件中。)
像import32.lib及cw32.lib这样的文件称为库文件。库文件指的是把多个目标文件集成保存到一个文件中的格式。
sprintf()等函数,不是通过源代码形式而是库文件形式和编译器一起提供的。这样的函数称为标准函数。之所以是使用库文件,是为了简化为链接器的参数指定多个目标文件这一过程。
通过以目标文件的形式或集合多个目标文件的库文件形式来提供函数,就可以不同公开标准函数的源代码内容。
8.6 DLL文件及导入库
Windows以函数的形式为应用提供了各种功能。这些形式的函数称为API。例如,Sample1.c中调用的MessageBox()是Windows提供的API的一种。
Windows中API的目标文件,并不是存储在通常的库文件中,而是存储在名为DLL(Dynamic Link Library)文件的特殊库文件中。在前面的介绍中,我们提到MessageBox()的目标文件是存储在import32.lib中的。实际上,import32.lib中仅仅存储着两个信息,一是MessageBox()在user32.dll这个DLL文件中,另一个是存储着DLL文件的文件夹信息,MessageBox()的目标文件的实体并不存在。我们把类似于import32.lib这样的库文件称为导入库。
与此相反,存储着目标文件的实体,并直接和EXE文件结合的库文件形式称为静态链接库。存储着sprintf()的目标文件的cw32.lib就是静态链接库。
8.7 可执行文件运行时的必要条件
本地代码在对程序中记述的变量进行读写时,是参照数据存储的内存地址来运行命令的。在调用函数时,程序的处理流程就会跳转到存储着函数处理内容的内存地址上。EXE文件作为本地代码的程序,并没有指定变量及函数的实际内存地址。那么在EXE文件中,变量和函数的内存地址的值是如何来表示的呢?
EXE文件中给变量及函数分配了虚拟的内存地址。在程序运行时,虚拟的内存地址会转化成实际的内存地址。链接器会在EXE文件的开头,追加转换内存地址所需的必要信息。这个信息称为再配置信息。
8.8 程序加载时会生成栈和堆
EXE文件的内容分为再配置信息、变量组和函数组。不过,当程序加载到内存后,除此以外还会额外生成两个组,那就是栈和堆。栈是用来存储函数内部临时使用的变量(局部变量),以及函数调用时所用的参数的内存区域。堆是用来存储程序运行时的任意数据及对象的内存领域。
栈和堆的相似之处在于,他们的内存空间都是在程序运行时得到申请分配的。栈中对数据进行存储和舍弃的代码,是由编译器自动生成的,因此不需要程序员的参与。于此相对,堆的内存空间,则要根据程序员编写的程序,来明确进行申请分配或释放。
C语言是通过malloc()函数来进行申请分配、通过free()函数来释放的。而C++中则是通过new运算符来申请分配、通过delete运算符来释放的。无论是C语言还是C++,如果没有在程序中明确释放堆的内存空间,那么即使在处理完毕后,该内存空间仍会一直残留,这个现象称为内存泄漏。