程序环境和预处理

    大家好我是小锋我们来开始新一节的内容——程序环境和预处理。

我们知道我们创建的.c文件经过编译链接变成可执行程序那么我们写的.c的文本文件怎么变成可执行程序?它的过程是什么?大家别急我为大家细细说明

程序的翻译环境和执行环境

在ANSI C的任何一种实现中,存在两个不同的环境。

第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。

第2种是执行环境(运行环境),它用于实际执行代码。

我们的源文件在运行之前会经过翻译环境变成以.exe为后缀的可执行程序进入运行环境

那翻译环境中到底有哪些操作呢?

详解编译+链接

组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人 的程序库,将其需要的函数也链接到程序中。

我们知道一个工程会有多个源文件当它们进入翻译环境后会单独的进入编译器,转换成后缀为.obj的目标文件,然后目标文件和链接库中的内容一起进入链接器形成单一完整的可执行程序。

接下来我用gcc编译器为大家演示翻译环境的每个过程

vs code的下载使用教程可以在b站上学习

http://【VScode配置C/C++开发环境,安装/环境配置/编译/调试/汉化/编码问题】https://www.bilibili.com/video/BV1UK411C7xi?vd_source=e88e3818e94b2cbe77cfa5460db4931f

大家看现象就行了

预编译

我们在test.c文件中敲下这样一段代码

在终端窗口输入 gcc -E test.c -o test.i

它就会生成一个test.i的文件而这个文件中的代码就是预处理的代码了

我们看见红色框中的代码有很多,这里其实在把#include<stdio.h>中的内容包含到test.c中来。

我们修改一下代码在测试一下

开始测试

大家对比一下是不是发现我们的注释没有了,还有我们的#define替换也没有了。

总结

在预处理是我们主要进行

1,头文件的包含

2,#define定义的符号的替换

3,注释的删除

我们发现预编译进行的都是文本操作,都是于预处理指令相关的操作

编译

我们在终端进行如下操作

. 编译 选项 gcc -S test.c

编译完成之后就停下来,结果保存在test.s中。

test.s的内容

这里都是汇编代码

有此我们可以知道

总结

在进行编译操作主要进行

1,词法分析

2,语法分析

3,语义分析

4,符号汇总

5,并且将源代码转换为汇编代码

汇编

我们在终端进行如下操作

汇编 gcc -c test.c

汇编完成之后就停下来,结果保存在test.o中。

我们生成了一个test.o的文件这个文件就是目标文件,这时有点朋友就要问了目标文件的后缀不是.obj吗?

其实在vs中的目标文件后缀是.obj

          gcc中的目标文件后缀是.o

那test.o文件中的是什么呢?

这里面是二进制文件。

总结

汇编操作是

把汇编代码转换成二进制指令

并且形成符号表

链接

我们知道链接是将多个目标文件一起转换为可执行程序

我们接着操作

gcc test.o

我们看他这里是不是生成了.exe的文件了?

我也可以执行这个程序

. \test.exe

是不是就执行了这个程序了

总结

所以我们链接要做的事情是

1,合并段表

2,符号表的汇总和重定位

那什么是合并段表呢?

我们用图来说话

所以合并段表就是将多个目标文件中相同的段以相同的格式合并在一起

段表实际上是记录了该文件中所有段的偏移位置和属性,存储的是像代码段 数据段等的位置偏移或者属性
 

符号表

我们经过观察不难看出编译,汇编,链接都提到了符号表

那这个操作具体是什么呢?

我们还是用图说话

从图中我们可以看出符号表的变化过程

那图中的链接库是什么呢?

接下来我们从宏观的角度解读一下c语言

运行环境

程序执行的过程:

1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序 的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。

2. 程序的执行便开始。接着便调用main函数。

3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回 地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程 一直保留他们的值。

4. 终止程序。正常终止main函数;也有可能是意外终止。

预处理详解

预定义符号

这些预定义符号都是语言内置好的可以直接拿来用

# include<stdio.h>


int main() {
	int a[10] = { 1,2,3,4,5,6,7,8,9,10 };
	for (int i = 0; i < 10; i++) {
		printf("%d  %s  %s  %s  %d\n", a[i], __FILE__, __DATE__, __TIME__, __LINE__);
	}
	return 0;
}

#define定义的标识符

我们来举些例子

这里我们用gcc来观察预处理之后的现象

我们可以看到语句也可以替换,

(注)

define定义标识符的时候,尽量不要在最后加上

因为在替换时;也会替换

#define 定义宏

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义
宏(define macro
#define     name( parament-list )    stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff 的一部分
举个例子

大家看,参数也替换进去了这样就类似一个函数了

但是这这样有可能会出现问题

#define ADD(x,y)  x*y
int main() {
    int x = 3, y = 4;
    int c = ADD(x+1, y+1);
    printf("%d", c);
    return 0;
}

大家觉得这段代码输出结果时多少?

我们运行试试

怎么会这样呢,我们预期不是应该等于20吗?

我们在gcc中预处理看看

原来它是这样调换的;

我们要怎么避免这种情况呢?

加了括号后是不是明了了许多。

所以宏定义时应多用括号。

用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中
的操作符或邻近操作符之间不可预料的相互作用。

#define 替换规则

在程序中扩展 #define 定义符号和宏时,需要涉及几个步骤。
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先 被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上 述处理过程。
注意:
1. 宏参数和 #define 定义中可以出现其他 #define 定义的符号。但是对于宏,不能出现递归。
2. 当预处理器搜索 #define 定义的符号的时候,字符串常量的内容并不被搜索。

#和##

从这里我们可以总结 字符串是有自动连接的特点的
大家来看这两段代码
int main() {
	int add = 0, bdd = 1, cdd = 2, daa = 3;
	printf("the value is add   %d\n", add);
	printf("the value is bdd   %d\n", bdd);
	printf("the value is cdd   %d\n", cdd);
	printf("the value is daa   %d\n", daa);
	return 0;
}

#define PRINT(a) printf("the value is "#a"   %d\n", a)
int main() {
	int add = 0, bdd = 1, cdd = 2, daa = 3;
	PRINT(add);
	PRINT(bdd);
	PRINT(cdd);
	PRINT(daa);
	return 0;
}

第二个代码是不是比第一个要简洁很多
这就是# 的作用
使用 # ,把一个宏参数变成对应的字符串
我们在看看接下来的代码
## 的作用
##可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符。
注:
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。
最后我们要注意
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。
副作用就是表达式求值的时候出现的永久性效果。

宏和函数对比

我们来看看这段代码

#define ATT(x,y) ((x>y)?(x):(y))
int main() {
	int a = 3, b = 9;
	int c = ATT(a, b);
	printf("%d", c);
	return 0;
}

这里我们定义了一个宏用它来比较大小

那同学们我们这里能不能用函数来实现?

int att(int x, int y) {
	return x > y ? x : y;
}

int main() {
	int a = 3, b = 9;
	int c = att(a, b);
	printf("%d", c);
	return 0;
}

那这里我们为什么用宏定义而不用函数?

原因有二:
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。
所以宏比函数在程序的规模和速度方面更胜一筹
2. 更为重要的是函数的参数必须声明为特定的类型。
所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以
用于 > 来比较的类型。 宏是类型无关的
宏的缺点: 当然和函数相比宏也有劣势的地方:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2. 宏是没法调试的。
3. 宏由于类型无关,也就不够严谨
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。
比如:宏的参数可以出现类型,但是函数做不到。
举个例子
# define ADD(mon,arr) (arr*)malloc(mon*sizeof(arr))
int main() {
	int a = 0;
	int *ps=ADD(10, int);
	for (int i = 0; i < 10; i++) {
		*(ps+i) = i;
	}
	for (int j = 0; j < 10; j++) {
		printf("%d ", *(ps + j));
	}


	return 0;
}

大家看这样我们在动态开辟内存时就可以用宏定义来开辟,直接就可以传类型
讲了这么多我们把函数与宏定义比较来做个表格
命名约定
一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。
那我们平时的一个习惯是:
把宏名全部大写
函数名不要全部大写

#undef

这条指令用于移除一个宏定义。
大家看用#undef移除了ARR后就不能再用了。

命令行定义

许多 C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些。)

条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
举个例子
int main() {
	int a = 1;
#if 1//如果为真那么执行为假不执行
	a = 0;
	printf("haha");
#endif
	if (a) {
		printf("hehe");
	}


	return 0;
}
#if的用法与if语句类似
int main() {
#if 0
	a = 0;
	printf("haha");
#elif 2<1
	printf("wawa");
#elif 5
	printf("wawa");
#else
	printf("heihei");

#endif
	return 0;
}
这相当于多分支if语句
# define MAR 0
int main() {
#if defined(MAR)
	printf("hehe");
#endif

	return 0;
}

大家看没有定义MAR就没有执行printf函数输出
定义后输出了

文件包含

我们已经知道, #include 指令可以使另外一个文件被编译。
就像它实际出现于 #include 指令的地方一样。
这种替换的方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次。

头文件被包含的方式:

在这个是库函数的头文件的包含用<>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用 “” 的形式包含?
答案是肯定的, 可以
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。
这个是本地文件的包含用“”
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。
如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用 “” 的形式包含?
答案是肯定的, 可以
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。
在大的程序工程中我们可能会出现头文件重复这种情况,我们该怎么解决呢?
1,
可以用我们刚刚学习的 条件编译
2,
该预处理指令可以防止头文件的重复使用
#pragma pack()这个预处理指令可更改默认对齐数

 以上就是全部内容了,如果有错误或者不足的地方欢迎大家给予建议。 

  • 21
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值