程序环境和预处理

 

马行无力皆因瘦,人不风流只为贫

作者:Mylvzi 

 文章主要内容:程序环境和预处理 

前言:

c程序的实现并不是简单的在vs上写好代码再调试执行的过程,vs是一种集成开发环境,简单来说,集成开发环境就是一个智能软件,它帮助人们进行代码的编写,执行,但背后具体的细节往往被忽略,了解这些细节有助于我们加深对计算机底层原理的认知,也能提高我们的代码理解能力

一.程序环境

任何一个ANSI C(标准C语言)的实现都需要经过两个“环境”-->翻译环境和执行环境

翻译环境:将所写代码(文本信息)转变为计算机语言(机械指令,二进制语言)形成目标文件并与链接库链接(给的任务看不懂,翻译为机器能看懂的语言)

-->将中文翻译成英文,并给你一些注释(链接库)

执行环境:执行计算机指令(得到任务,开始干活)

程序环境和预处理_第1张图片

 二.详解编译和链接

 翻译环境图解

程序环境和预处理_第2张图片

编译

编译:将文本信息翻译为计算机语言

编译分为三个过程:

1.预编译(预处理)-->文本操作,处理预处理指令(头文件包含,#define的替换,注释删除)

#include #define的替换都叫做预处理指令

2.编译-->(将C语言代码翻译为汇编指令)

3.汇编-->(将汇编指令转变为机械指令)

程序环境和预处理_第3张图片

 在LINUX环境中,利用gcc编译器查看编译过程的每一步:

程序环境和预处理_第4张图片

 LINUX:目标文件和可执行程序是通过ELF这种文件格式来存放计算机指令

readelf:读取elf的文件格式:
test.o的这种文件是按照段的形式存储的,他的信息被一个一个段划分存储

程序环境和预处理_第5张图片

 运行时环境

程序执行的过程:

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

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

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

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

三.预编译详解

  预编译阶段会处理所有的预处理指令,预处理指令有很多,比如:头文件的包含,#define的替换等等,下面将详细讲解预编译过程中都会处理那些预处理指令

1.预定义符号

预定义符号:是语言内置的,需要使用的时候就直接打印就行(就是为了方便)

在预处理阶段会直接被替换为相应的文本

程序环境和预处理_第6张图片

2.#define 

#define定义的标识符

程序环境和预处理_第7张图片

//#define定义的标识符
#define M 100
#define REG register
#define FOR for (;;)     //用更形象的符号来替换一种实现
#define CASE break;case        //在写case语句的时候自动把 break写上。
 /*如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。*/
#define DEBUG_PRINT printf("file:%s\tline:%d\t \     
                          date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ ,       \
__DATE__,__TIME__ )

注意:在使用#define定义的标识符时,不要在末尾添加“ ;”,否则可能出现两个分号的情况 

 #define定义的宏(重点)

程序环境和预处理_第8张图片

宏:就是一种替换文本的工具,和#define定义的标识符作用类似

注意:

1.成员列表(parament-list)的左括号必须与name相连

2.stuff的每个参数都必须单独使用()括起来,且整体也要使用()括起来,避免优先级的错误 

 

//宏
#define SQUARE(x) ((x)*(x))
int main()
{
	int a = SQUARE(5);
	printf("%d\n", a);//输出5*5=25
	return 0;
}

#define SQUARE(x) x*x  //不添加括号,存在优先级的错误
//宏只是一种“形式”上的替换,他根本不会计算表达式的值
int main()
{
	int a = SQUARE(3+2);
	printf("%d\n", a);//3+2*3+2=11
	return 0;
}

#define DOUBLE(x) (x) + (x)
int main()
{
	int a = 5;//会输出100吗
	printf("%d\n", 10 * DOUBLE(a));//10*(5)+5 == 55
	return 0;
}

#define的替换规则:

#define 替换规则 在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先 被替换。

2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。

3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上 述处理过程。 

注意:#define定义过程中可以嵌套,但定义的宏无法递归使用

           如果定义的标识符常量在字符串中出现,在预处理阶段并不会被替换

3.#和## 

 #:使宏参数以字符串的形式出现

//我希望在字符串中添加宏参数,并不能直接写一个参数,因为在预处理时,字符串的内容并不会被扫描
//解决方法:就是让参数以字符串的形式出现-->在参数前添加#

#define PRINT(n, format) printf("the value of " #n " is " format "\n", n)
//format不需要再单独添加“”,因为传递的参数自带“”

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


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

	float f = 4.5f;
	//printf("the value of f is %f\n", f);
	PRINT(f, "%f");
	return 0;
}

##: 把左右两边的符号合成为一个符号

#define CAT(x,y) x##y

int main()
{
	int ab = 10;
	printf("%d\n", CAT(a,b));//10
	printf("%d\n", ab);//10
	return 0;
}

注意:连接后的标识符必须是一个合法的,可以被识别的标识符,否则就无意义 

 4.带有副作用的宏

如果参数具有“副作用”,可能会出现意想不到的结果,要避免宏的参数具有副作用

//带副作用的宏参数
int main()
{
	int a = 10;
	//int b = a+1;//b=11, a=10  无副作用
	//int b = ++a;//b=11, a=11  具有副作用
	return 0;
}

#define MAX(x, y) ((x)>(y)?(x):(y))
int main()
{
	int x = 5;
	int y = 8;
	int z = MAX(x++, y++);//带有副作用的参数
	//替换:((5++) > (8++) ? (5++) : (8++))
	//       先使用值,5<8 --> (8++) -->所以z=8+1=9
	//((5++) > (8++) 值使用完之后在进行副作用,x=x+1=6,y=y+1=9
	//最后的(8++)使用完后,执行y=y+1=9+1=10
	//x=6,y=10,z=9
	printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?
                               6  10  9

	return 0;
}

5.宏与函数的区别 

宏是小聪明,好看,好用,方便,但是不严谨,只能适用于简单的工作

函数是大智慧,使用不方便(耗时长),消耗的资源多,但是严谨,适用于大型工作

1.宏更适用于执行简单运算 

程序环境和预处理_第9张图片 2.宏与函数的比较

1.宏无法调试,而函数可以调试,可以发现计算过程中的错误

2.宏是形式上的替换,在预编译阶段直接替换,会增加源代码长度,如果大量使用,会导致代码长度过长;而函数是随用随找,不会增加代码长度(使用就进入)

3.宏不需要声明类型,无类型限制(不太严谨),函数有严格的类型限制

4.宏在传递参数时不会计算结果,可能出现副作用;而函数在传递时,会先计算值,不会有歧义

5.宏的参数可以出现类型,但函数不能(把类型当作参数)

6.宏的参数必须严格添加(),避免优先级的错误!

7.宏的执行速度更快,函数因为函数调用,函数栈帧开辟以及函数返回所需额外时间更多,所以执行效率较低(但如果是大型计算过程,相较于计算过程所需的时间,函数额外使用的时间可以忽略不计)

8.宏无法递归,函数可以递归使用

9.宏名一般全部大写,函数名一般首字母大写

//宏的参数可以是类型
#define MALLOC(num,type) (type*)malloc(sizeof(type)*num)
int main()
{
	int* p = (int*)malloc(sizeof(int) * 10);//我觉得这样开辟太麻烦,使用宏替换
	int* p = MALLOC(10,int);//这样就实现了相同的功能
		                     //宏的参数可以出现类型,而函数不能
	return 0;
}

3.集百家之长-->内联函数 

内联函数: 集合宏的优点(计算速度快)和函数的优点(方便调试)的一种特殊类型的函数

1.可以被调试,可以观察每一步计算的具体过程

2.不需要进行函数栈帧的开辟,函数返回,节省时间

3.内联函数还是由编译器处理的,宏是在预处理阶段进行的形式替换

inline int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int a = 10;
	int b = 20;
	int c = Add(a, b);
	printf("%d\n", c);

	return 0;
}

 

6.#undef

#undef用于去除已有的宏定义

格式:#undef NAME

在预编译过程中执行

7.命令行定义

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

程序环境和预处理_第10张图片 

8. 条件编译

所谓的条件编译就是在不同条件下编译不同的代码,既不用删除代码,也不需要编写多份代码

 程序环境和预处理_第11张图片

9.文件包含 

 #include指令可以被另一个文件编译,会被编译的结果替换

替换方式很简单:

1.#include出现在哪里,就在哪里替换

2.出现一次,就替换一次;出现十次,就替换十次(相当于被编译了10次)

头文件包含

C语言中的头文件包含有两种方式:

1.《 》包含  《》包含一般应用于库文件的包含,如:#include

2.“”包含        " "包含一般应用于程序员本身头文件的包含,如:#include "test.h"

注意:“包含”其实是一种搜寻的过程,即检查路径之下是否包含被编译的文件;

“”的搜寻路径有两种,会现在源文件所在目录下搜寻,如果未找到,去库文件路径下搜寻;而《》的搜寻路径只有库文件路径

使用《》包含库文件,效率更高!

处理嵌套文件包含

#include的替换方式是出现一次,就替换一次,就编译一次;在使用的过程中可能会出现嵌套文件的包含,那么同一文件有可能在预处理的过程中被编译多次,大大增加代码量,要解决这个问题,有两种解决方式(通过条件编译解决)

程序环境和预处理_第12张图片 

四.其他预处理指令 

#error  是一种在程序中提醒和报错的工具

#pragma  

#line

... ... ...

比如:#pragma pack(8) 修改默认对齐数为8

五.总结

本文主要讲解了c程序实现过程中的两个环境:翻译环境和执行环境,具体讲解了翻译环境中的编译和链接的过程;又详细的讲解了编译过程中的预编译过程;尽管如今的集成开发环境已经如此便利,我们还是要去学习背后的基本逻辑与代码,这样才能加深我们对代码的理解能力

推荐书籍:

1.《程序员的自我修养》(强烈推荐)

2.《高质量c/c++编程指南》

3.《编译原理》

4.《C语言深度剖析》

你可能感兴趣的:(预处理)