程序环境和预处理(详解版)

我们已经学到这里,这就是关于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() 修改默认对齐数(这个我们用过)


好,今天的学习就到这里,我们下期再见!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

月亮夹馍干

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值