我们已经学到这里,这就是关于C语言的最后一个集中的知识点了,虽然它比较抽象,但是了解这部分知识,可以让我们对C代码有更深层次的理解,知道代码在每一个阶段发生什么样的变化。让我们开始学习吧!
目录
1.程序的翻译环境和执行环境
2.编译+链接
2.1编译环境
2.2编译本身的几个阶段
2.3运行环境
3.预处理(预编译)
3.1预定义符号(这些符号都是语言内置的)
3.2#define和#undef
3.3命令行的定义
3.4条件编译
3.5文件包含
3.5.1头文件被包含的方式
3.5.2嵌套文件包含
4.其他预处理指令
1.程序的翻译环境和执行环境
在ANSI C的任何一种实现中,存在两个不同的环境
1.第一种是翻译环境,在这个环境中,源代码被转换为可执行的机器指令。
2.第二种是执行环境,它用于实际的代码执行
我们举个例子来理解它
例子:我们在2022VS中写了一段代码,我们是放在了.c文件中,他在执行时生成解决方案会产生一个.exe文件,这个exe文件就是这段代码的翻译环境
我们的.c文件要变为.exe文件要经过编译和链接,其中这个编译就是指我们的翻译环境
我们接下来详细学习编译和链接
2.编译+链接
2.1编译环境
1.组成一个程序的每个源文件通过编译过程分别转换成目标代码
2.每个目标文件由编译器捆绑在一起,形成一个单一而完整的可执行程序
3.链接器同时也会引入标准c函数库中任何被该程序用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中
每个源文件(.c)都对应一个目标文件(.exe)
我们用一个图谱来理解它
知道这个以后,我们来了解编译本身包括的几个阶段
2.2编译本身的几个阶段
(我们用gcc编辑器语法来说明,gcc比VS效果更明显)
我们首先画一个图供大家了解
我们已经 知道编译本身几个阶段就是这四个,我已经在图中标好序号,方便我们接下来的说明:
1.预处理(预编译):
命令:gcc -E test.c -o test.i
(其中:-E代表在预处理结束后代码停止执行
-o代表指定生成的文件名是test.i)
执行的操作包括:#include头文件的包含
#define定义符号的替换和删除
注释的删除(文本删除)
2.编译:
命令:gcc -S test.i 生成了test.s文件
在这一步,它把C语言翻译成了汇编代码
执行的操作包括:语法分析
词法分析
语义分析
符号汇总(只有全局符号汇总,汇总的是全局变量)
3.汇编:
命令:gcc -c test.s 生成了test.o文件
在gcc中,这个test.o文件就是目标文件,存放的是二进制数据
执行的操作包括:把汇编代码翻译成了二进制指令(存放与test.o文件中)
行成符号表,生成对应表格(以编译的符号汇总为基础生成的)
例:假设我们有三个全局变量被符号汇总
4.链接:
在一个大型工程中,我们有多个test.c文件,这就代表我们要生成多个test.o的目标文件
执行的操作:符号表的合并和定位
在生成的符号表中查找跨文件的符号和数据
例如:
我们在这里创建两个.c文件,一个存放总体代码,一个存放函数功能的实现
2.3运行环境
也就是程序执行的过程:
1.程序必须载入内存中,在有操作系统的环境中,一般这个由操作系统完成,在独立环境中,程序的载入必须手工安排,也可能是通过可执行代码置入只读内存来完成
2.程序开始执行,接着便调用main函数
3.开始执行程序代码,这时函数程序将使用一个运行堆栈(stack),存储函数的局部变量和返回地址。程序同时可以使用静态内存(static)存储于静态,内存中的变量在程序的整个执行过程中一直保留它们的值
4.终止程序,正常终止main函数,也可能意外终止
3.预处理(预编译)
3.1预定义符号(这些符号都是语言内置的)
我们常见的预定义符号:
__FILE__ 进行编译的源文件
__LINE__ 当前文件的行号
__DATE__ 文件被编译的日期
__TIME__ 文件被编译的时间
__STDC__ 如果编译遵循ANSI C,其值为1,否则未定义
__FUNCTION__ 当前函数的名称
如果感兴趣,可以看一下它们的详细描述
我们看个代码例子:
//预定义符号 int main() { printf("文件路径为:> %s\n", __FILE__); printf("文件的对应行号是:> %d\n", __LINE__); printf("文件被编译的日期是:> %s\n", __DATE__); printf("文件被编译的时间是:> %s\n", __TIME__); //printf("%s\n", __STDC__);VS2022中已经不能使用这个预定义符号了 printf("当前执行函数的名称是:> %s\n", __FUNCTION__); return 0; }
3.2#define和#undef
由于#define还是很有说法的,所以我们把这一对放在 详解#define 这一篇博客单独讲解,这样更好理解,在这里只要知道它们都是预定义符号,以及#define是只进行对应位置的替换的,是不计算的就好
3.3命令行的定义
许多C的编译器提供了一种能力(例如gcc),允许在命令行定义中定义符号,用于启动编译过程
当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处
例如我们在gcc环境下编译以下代码
int main() { int arr[sz];//我们在这里不指定sz的大小,我们在命令行输入 int i = 0; for (i = 0; i < sz; i++) { arr[i] = i; printf("%d ", arr[i]); } printf("\n"); return 0; }
在gcc编译器命令行,我们输入
编译:gcc test.c -D sz=10 -o test.exe
其中 -D是指定sz的大小,在这里我们设置sz=10
-o是指定生成的文件名是test.exe
运行:.\test.exe
然后我们发现显示了 0 1 2 3 4 5 6 7 8 9 这几个数字
sz的值的代码的替换也是在预处理工程中完成了,我们可以输入以下命令来查看细节
gcc test.c -D sz=10 -E -o test.i
这里的-E是在预处理结束后代码停止
这时打开test.i文件中我们查看代码,我们会看到,代码sz的位置被替换为10
(这种方式就是命令行的方式,我们以后要学的linux操作系统这也这种编译方式)
3.4条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的,因为我们有条件编译指令
我们先了解一些常见的条件编译指令
1.#if 常量表达式
……
#endif
2.多个分支的条件编译
#if
……
#elif
……
#else
……
#endif
3.判断是否被定义
#if defined(symbol),如果定义了symbol就执行下面的代码,否则就跳过
这条等价于#ifdef symbol
#if !defined (symbol),如果没有定义symbol,就执行下面的代码,否则就跳过
这条等价于#ifndef symbol
4.嵌套指令
#if defined (os_UNIX)
#ifdef OPTIONI
unix _version_ option1();
#endif
#ifdef OPTION2
unix _version_ option2();
#endif
#elif defined(os_MSDOS)
#ifdef OPTION2
msdos _version_ option2();
#endif
#endif
接下来我们再来看个代码例子
#define PRINT 1 int main() { #ifdef PRINT//如果定义了PRINT,就打印hehe,否则不执行这段代码 printf("hehe\n"); #endif return 0; }
3.5文件包含
我们知道#include指令可以使另一个文件被编译,就像它实际出现于#include指令的地方一样
这种替换方式很简单:
预处理器先删除这条指令指令,并用包含文件的内容替换,这样一个源文件被包含10次,那就实际被编译了10次
我们介绍我们C语言中用到的文件包含:头文件包含和嵌套文件包含
3.5.1头文件被包含的方式
1.本地文件包含(就是指自己写出来的文件)
格式:#include“filename”
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件
2.库文件包含(我们平时用到的库函数,库里面有的文件)
格式:#include<filename.h>
查找策略:查找头文件直接去标准路径下去查找,如果找不到就提示编译错误
这时我们可以得到一个结论,库函数也可以使用“ ”的形式包含,但我们不采用,因为它效率低
3.5.2嵌套文件包含
我们用图解来说明:
解决办法:条件编译
在每个头文件开头添加条件
1.方法一:#ifndef _TEST_H_
#define _TEST_H_
……头文件的内容
#endif
2.方法二:
在开头写 #pragma once
这两种方法都可以避免头文件的重复引入,被多次包含
4.其他预处理指令
这里我们只介绍几个,如果对这部分内容感兴趣的话,可以自己去了解
#error 编译程序时,只有遇到#error就会生成一个编译错误的提示信息,并停止编译
#pragma 可以设定编译程序完成的一些特定的动作,允许向编译程序传送各种指令
#line 改变当前行数和文件名称
#pragma pack() 修改默认对齐数(这个我们用过)
好,今天的学习就到这里,我们下期再见!!!