C语言程序设计 | 程序编译与预处理

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

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

二、详解编译+链接

1.翻译环境

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

图示如下:

img

源文件是指那些后缀是.c的文件,例如:

img

编译器则是由cl.exe的程序文件执行的

目标文件则是值那些后缀为.obj的文件,例如:

img

2.编译

2.1预编译/预处理

在这个预编译的阶段,程序会进行3个方面等的处理。

1.头文件的包含 #include

2.#define定义符号的替换,删除定义的符号

3.注释删除

2.2编译

这里的编译阶段是把C语言代码转换成汇编代码,并且进行了4个步骤。

1.语法分析

2.此法分析

3.符号汇总

4.语义分析

其中重点讲解的是符号汇总,在这个步骤中,程序会把那些全局符号进行汇总。

例如定义的全局变量,函数(包括main函数)。

2.3汇编

这个阶段是编译的最后一个阶段,在完成这个阶段后,将会进入链接阶段,生成.exe文件。

此阶段的操作呢是把汇编代码转换成二进制代码,根据上个阶段的符号汇总步骤形成符号表。

三、预处理详解

1.预定义符号

C语言中有些符号是预定义好的,可以直接使用,例如:

FILE LINE DATE TIME STDC//进行编译的源文件 //文件当前的行号 //文件被编译的日期 //文件被编译的时间 //如果编译器遵循ANSI C,其值为1,否则未定义

这些预定义符号都是语言内置的。
举个栗子 :

printf("file:%s line:%d\n", __FILE__, __LINE__);

2.#define

2.1#define定义标识符

语法:
#define name stuff

#define MAX 1000
举个栗子:

// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。

#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ ) 

这里的反斜杠是为了把回车转义,让回车不是回车。

#define的作用呢就是在预编译阶段把预定义好的标识符在编译阶段**替换掉,**也就说在编译阶段是看不见预定义这些东西的,它是直接都替换完了的。另外在预定义标识符的时候,最好最好不要在后面加上分号,容易引起语法错误。

2.2#define宏

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。

下面是宏的申明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中 。

注意:
参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分 。

代码实例:

#include <stdio.h>
#define SQUARE(X) X*X
int main()
{
	int num = 10;
	int ret = SQUARE(num);
	printf("%d ", ret);
	return 0;
}

这里的宏定义是把X作为参数,在接下来的预编译阶段完成替换。上面的代码在编译完成后的真正模样是这样的

#include <stdio.h>
#define SQUARE(X) X*X
int main()
{
	int num = 10;
	int ret = 10 * 10;
	printf("%d ", ret);
	return 0;
}

注意:上面的宏定义如果就这样写会出现问题

代码实例:

#include <stdio.h>
#define SQUARE(X) X*X
int main()
{
	int num = 10;
	int ret = SQUARE(num+1);
	printf("%d ", ret);
	return 0;
}

正常来看,答案应该是121,但是打印出来的确实21,为什么呢?

原因是这里的参数传进去后,只是进行了相应的替换,而C语言有操作符优先级的存在,所以导致结果的不同。

上面的代码应该是这个样子:

#include <stdio.h>
#define SQUARE(X) X*X
int main()
{
	int num = 10;
	int ret = 10 + 1*10+1;
	printf("%d ", ret);
	return 0;
}

按照操作符的优先级来计算,应该是10+10+1,所以结果是21.

那么怎么来解决这个问题呢,答案很简单,给加上括号即可。

代码如下:

#include <stdio.h>
#define SQUARE(X) (X)*(X)
int main()
{
	int num = 10;
	int ret = SQUARE(num+1);
	printf("%d ", ret);
	return 0;
}

在宏定义的时候,将参数部分加上括号,防止优先级的存在导致答案与自己预想的不一样。

这样加上括号也还不够严谨,来看看下面的这段代码:

#include <stdio.h>
#define SQUARE(X) (X)*(X)
int main()
{
	int num = 10;
	int ret = SQUARE(10*num+1);
	printf("%d ", ret);
	return 0;
}

如果代码是这样写的话,那么他的替换将是下面的这个样子:

#include <stdio.h>
#define SQUARE(X) (X)*(X)
int main()
{
	int num = 10;
	int ret = 10 * (10 + 1) * (10 + 1);
	printf("%d ", ret);
	return 0;
}

那么怎么解决这个问题呢,那就是再加上一个括号,令这个宏作为一个整体:

#include <stdio.h>
#define SQUARE(X) ((X)*(X))
int main()
{
	int num = 10;
	int ret = 10 * (10 + 1) * (10 + 1);
	printf("%d ", ret);
	return 0;
}

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

2.3#define替换规则

这里简单的列举#define替换的规则

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

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先
    被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上
    述处理过程。
    注意:
  4. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
  5. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
    代码实例:
#define num 2
#include <stdio.h>
#define DOUBLE(X) ((num)*(X))
int main()
{
	int a = 10;
	int ret = DOUBLE(a);
	printf("%d ", ret);
	return 0;
}

img

首先是将变量num先替换成2,然后再把定义的宏替换成(2)*(X)

整体替换如下所示:

#define num 2
#include <stdio.h>
#define DOUBLE(X) ((num)*(X))
int main()
{
	int a = 10;
	int ret = ((2)*(10));
	printf("%d ", ret);
	return 0;
}

2.4#和##

#define是不是可以讲字符串也替换呢?如果可以,怎么替换呢?

我们先看看下面的代码:

#include <stdio.h>

int main()
{
	char* p = "hello ""bit\n";
	printf("hello"" bit\n");
	printf("%s", p);
	return 0;
}

打印结果:

img

也就是说C语言是允许这样打印字符串的,那么我们就可以根据这样来定义这样的一个宏:

#define PRINT(FORMAT, VALUE) printf("the value is "FORMAT"\n", VALUE)

int main()
{
	PRINT("%d", 10);
	return 0;
}

这里定义了一个PRINT的宏,里面的参数有两个,当第一个参数是一个字符串的时候,那么代码就应该是下面的这样子:

#define PRINT(FORMAT, VALUE) printf("the value is "FORMAT"\n", VALUE)

int main()
{
	printf("the value is ""%d""\n", VALUE);
	return 0;
}

跟前面刚开始的打印字符串是一样的道理。但这里只有当字符串作为宏参数的时候才可以把字符串放在字符串中。

那如果是我想在屏幕上打印一个变量的数字等于多少,该怎么打印呢?

我们可以创建这样的一个宏:

#include <stdio.h>
#define PRINT(FORMAT, VALUE)\
printf("the value of " #VALUE "is "FORMAT "\n", VALUE)

int main()
{
	int i = 10;
	PRINT("%d", i + 3);
	return 0;
}

代码中的 #VALUE 会预处理器处理为:
“VALUE” .

最终的输出的结果应该是 :

img

这里的#号是将i+3直接转换成字符创i+3,然后完成替换。

## 的作用

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

这个比较简单,其实就是把##两边的符号合并成一个符号

代码实例:

#include <stdio.h>
#define ADD_TO_SUM(num, value) \
sum##num += value

int main()
{
	int sum5 = 0;
	ADD_TO_SUM(5, 10);//作用是:给sum5增加10
	printf("%d", sum5);
	return 0;
}

先定义一个合格的标识符,这里定义的sun5即是合格的标识符。

3.带副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
例如:

 x+1;//不带副作用
 x++;//带有副作用 

来看下面代码的宏输出的结果是什么:

#include <stdio.h>
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )

int main()
{

	int x = 5;
	int y = 8;
	int z = MAX(x++, y++);
	printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?
	return 0;
}

打印结果:

img

代码替换完成的样子:

z = ( (x++) > (y++) ? (x++) : (y++));

首先我们可以看到,宏参数有两个,一个是x++,一个是y++,这种操作符会改变x和y本身的值,所以就导致了结果的不同。

4.宏和函数的对比

其实我们可以看到宏和函数有些地方也是差不多的,但是二者谁优谁劣呢?

宏通常被应用于执行简单的运算。
比如在两个数中找出较大的一个 。

#define MAX(a, b) ((a)>(b)?(a):(b))

那为什么不用函数来完成这个任务?
原因有二:
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。
宏是类型无关的。

宏的缺点:当然和函数相比宏也有劣势的地方:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。2. 宏是没法调试的 。

3. 宏由于类型无关,也就不够严谨。4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

但也还是有函数做不到的事情,但是宏可以做到。例如:

#include <stdio.h>
#define MALLOC(num, type) (type*)malloc(num * sizeof(type))

int main()
{	
	//使用
	MALLOC(10, int);
	//预处理器替换之后:
	int*p = (int*)malloc(10 * sizeof(int));
	return 0;
}

利用#define替换的原理,我们可以这样进行操作。

宏和函数的一个对比:

属 性#define定义宏函数
代 码 长 度每次使用时,宏代码都会被插入到程序中。除了非常 小的宏之外,程序的长度会大幅度增长函数代码只出现于一个地方;每 次使用这个函数时,都调用那个 地方的同一份代码
执 行 速 度更快存在函数的调用和返回的额外开 销,所以相对慢一些
操 作 符 优 先 级宏参数的求值是在所有周围表达式的上下文环境里, 除非加上括号,否则邻近操作符的优先级可能会产生 不可预料的后果,所以建议宏在书写的时候多些括 号。函数参数只在函数调用的时候求 值一次,它的结果值传递给函 数。表达式的求值结果更容易预 测。
带 有 副 作 用 的 参 数参数可能被替换到宏体中的多个位置,所以带有副作 用的参数求值可能会产生不可预料的结果。函数参数只在传参的时候求值一 次,结果更容易控制。
参 数 类 型宏的参数与类型无关,只要对参数的操作是合法的, 它就可以使用于任何参数类型。函数的参数是与类型有关的,如 果参数的类型不同,就需要不同 的函数,即使他们执行的任务是 不同的。
调 试宏是不方便调试的函数是可以逐语句调试的
递 归宏是不能递归的函数是可以递归的

四、#undef

这条指令很简单,有些宏定义我们在后续的代码中如果不想使用了,可以使用#undef取消这个宏定义。

五、条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件
编译指令。

比如说:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。

#include <stdio.h>
#define __DEBUG__
int main()
{
int i = 0;
int arr[10] = {0};
for(i=0; i<10; i++)
{
arr[i] = i;
#ifdef __DEBUG__
printf("%d\n", arr[i]);//为了观察数组是否赋值成功。
#endif //__DEBUG__
}
return 0;
}

有这条指令的打印结果:

img

#include <stdio.h>
//#define __DEBUG__
int main()
{
	int i = 0;
	int arr[10] = { 0 };
	for (i = 0; i < 10; i++)
	{
		arr[i] = i;
#ifdef __DEBUG__
		printf("%d\n", arr[i]);//为了观察数组是否赋值成功。
#endif //__DEBUG__
	}
	return 0;
}

没有这条指令的打印结果:

img

注意,这里是把宏定义去掉了,因为没有宏定义,所以在编译阶段不会进行这段带有指令的代码。

总结

这篇博客是本人对C语言程序设计关于编译与预处理方面的一些理解,希望对大家的学习能够有所帮助,另外有些地方写的不对的,可以指出来,我会进行更改,感谢大家观看本篇博客。

  • 8
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值