《C语言进阶》 第七部分 程序环境和预处理

大家好!这篇文章是我们讲《C语言进阶》里的最后一块知识,学完这块知识我们能了解一个程序是如何变成一个可执行文件。好了,话不多说,直接开始。
在这里插入图片描述

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

在ANSI C的任何一种实现中,存在两个不同的环境。
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境,它用于实际执行代码。

一个大致的流程:
在这里插入图片描述

2. 详解编译+链接

2.1 翻译环境

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

2.2 编译本身也分为几个阶段:

编译里面还包涵了三个小的方面:预编译,编译,汇编。
在预编译里处理了一些文本操作:
1.头文件的包含(#include)。
2.删除注释。
3.#define定义符号的替换。

在编译里把C语言代码转换成汇编代码:
1.语法分析。
2.词法分析。
3.语义分析。
4.符号汇总。

在汇编里把汇编代码转换成二进制指令:
1.形成符号表。

2.3 链接

在链接里把每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序:
1.合并段表。
2.符号表的合并和重定位。
3.多个目标文件进行链接时会通过符号表查看来自外部的符号是否真实存在。

3 运行环境

程序执行的过程:
1.程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2. 程序的执行便开始。接着便调用main函数。
3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
4. 终止程序。正常终止main函数;也有可能是意外终止。

4. 预处理详解

4.1 预定义符号

在这里插入图片描述
这些预定义符号都是语言内置的。这些符号一般写日志时可以使用。

4.2 #define

4.2.1 #define 定义标识符

在这里插入图片描述
一些使用方法:
在这里插入图片描述
提问:在define定义标识符的时候,要不要在最后加上 ;
在这里插入图片描述
建议不要加上 ; ,这样容易导致问题。

4.2.2 #define 定义宏

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
下面是宏的申明方式:
在这里插入图片描述
注意:
参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

举个例子:
在这里插入图片描述
这里DOUBLE会被替换成3+3。

警告:这个宏存在一个问题:
观察下面的代码段:
在这里插入图片描述
可能很多人会认为是60,但是却是33,这是为什么呢?
因为这被替换成10*3+3,所以在宏定义时应该加上括号。
在这里插入图片描述
所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。

4.2.3 #define 替换规则

在这里插入图片描述
举个例子:

#include <stdio.h>

#define M 100
#define DOUBLE(x) ((x)+(x))

int main()
{

	printf("%d\n", 10*DOUBLE(3+M));//M被替换
	printf("M = %d\n", 10);//M不能被替换
	return 0;
}

4.2.4 #和##

如何把参数插入到字符串中?
我们先想一个问题:

int main()
{
	int a = 10;
	printf("the value of a is %d\n", a);

	int b = 20;
	printf("the value of b is %d\n", b);

	return 0;
}

我们在多次打印的时候想把a放到字符串里,b放到字符串里…
我们打印很多的时候,我们不能这样一个一个写,函数也做不到,就要用到宏。
补充一点知识:
首先我们看看这样的代码:

int main()
{
	printf("hello world\n");
	printf("hello " "world\n");
	printf("hel" "lo " "world\n");
	return 0;
}

在这里插入图片描述
我们发现字符串是有自动连接的特点的。

那我们是不是可以写这样的代码:使用 # ,把一个宏参数变成对应的字符串
在这里插入图片描述
在这里,PRINT(a);替换后的样子是printf(“the value of ““a”” is %d\n”, a);
PRINT(b);替换后的样子是printf(“the value of ““b”” is %d\n”, b);

## 的作用
##可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符。

举个例子:

#include <stdio.h>

#define CAT(C, num)  C##num
int main()
{
	int Class10 = 100;
	printf("%d\n", CAT(Class, 10));
	return 0;
}

在这里,CAT(Class, 10)就会被替换成Class10,然后替换成100。
注:这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

4.2.5 带副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
在这里插入图片描述
我们看一下下面的代码:

#include <stdio.h>


#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
	int a = 3;
	int b = 5;
	int m = MAX(a++, b++);
 
	printf("%d\n", m);
	printf("a=%d b=%d\n", a, b);

	return 0;
}

这里的结果是多少?
首先,MAX(a++, b++);会被替换成((a++) > (b++) ? (a++) : (b++));
这里没有先使用a,是直接把a++替换过去。
a是3,b是5,3>5假,算的是b++,但是现在a=4,b=6,b++是先使用后++,所以m=6,a=4,b=7。

4.2.6 宏和函数对比

宏通常被应用于执行简单的运算,比如在两个数中找出较大的一个。
那为什么不用函数来完成这个任务?
在这里插入图片描述
宏的缺点:当然和函数相比宏也有劣势的地方:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2. 宏是没法调试的。
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
举个例子:

#define MALLOC(num, type)  (type *)malloc(num * sizeof(type))
int main()
{
	MALLOC(10, int);//类型作为参数
	//预处理器替换之后:(int*)malloc(10 * sizeof(int));
	return 0;
}

4.2.7 命名约定

一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。
那我们平时的一个习惯是:把宏名全部大写,函数名不要全部大写。

4.3 #undef

这条指令用于移除一个宏定义。
在这里插入图片描述
在这里插入图片描述
我们可以看到MAX不能使用了。

4.4 命令行定义

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

4.5 条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
在这里插入图片描述
看下面的代码:
在这里插入图片描述
在这里插入图片描述
if后面为真时,执行语句,为假时不执行语句。
if后面可以跟变量吗?
在这里插入图片描述
我们可以看到这样是不行的,因为这个是预处理指令是在预编译时候,而变量的创建是在运行的时候。
我们可以这样写:
在这里插入图片描述
常见的条件编译指令:

#if 常量表达式
 //...
#endif
//2.多个分支的条件编译
#if 常量表达式
 //...
#elif 常量表达式
 //...
#else
 //...
#endif
//3.判断是否被定义
#if defined(symbol)//反面:#if !defined(symbol)
//
#endif

#ifdef  symbol//反面:#ifndef symbol
//
#endif
//4.嵌套指令
#if defined(OS_UNIX)
   #ifdef OPTION1
        unix_version_option1();
   #endif
   #ifdef OPTION2
        unix_version_option2();
   #endif
#elif defined(OS_MSDOS)
  #ifdef OPTION2
        msdos_version_option2();
   #endif
#endif

4.6 文件包含

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

4.6.1 头文件被包含的方式:

在这里插入图片描述
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。
在这里插入图片描述
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。

这样是不是可以说,对于库文件也可以使用 “” 的形式包含?
答案是肯定的,可以。
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

4.6.2 嵌套文件包含

在这里插入图片描述
comm.h和comm.c是公共模块。
test1.h和test1.c使用了公共模块。
test2.h和test2.c使用了公共模块。
test.h和test.c使用了test1模块和test2模块。
这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复。

如何解决这个问题?
答案:条件编译。

在这里插入图片描述
总结:
到这里,我们C语言的基本内容结束了,以后我可能会讲一些C语言练习题,或一些游戏什么的,或者对C语言的深度解析。马上我先准备写一点关于C语言的数据结构,希望大家也可以给我一些建议。如果大家认为我有哪些不足之处或者知识上的错误都可以告诉我,我会在之后的文章中不断改正,也请大家多多包涵。如果大家觉得这篇文章有用的话,也希望大家可以给我关注点赞,你们的支持就是对我最大的鼓励,我们下一篇文章再见。
在这里插入图片描述

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

学代码的咸鱼

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

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

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

打赏作者

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

抵扣说明:

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

余额充值