程序环境和预处理

本章重点:
  • 程序的翻译环境
  • 程序的执行环境
  • 详解:C语言程序的编译+链接
  • 预定义符号介绍
  • 预处理指令 #define
  • 宏和函数的对比
  • 预处理操作符###的介绍
  • 命令定义
  • 预处理指令 #include
  • 预处理指令 #undef

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

下面我来画出程序的编译过程:

 

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

程序的翻译环境

编译环境本身也分为两个阶段:编译和链接

编译期间也分三个阶段:预处理、编译、汇编 。它们各自的作用是:

我们首先写出一个简单的代码来查看编译期间的过程:

#include<stdio.h>
#define MAX 100
int main()
{
    printf("%d\n",MAX);
    return 0;
}

我们选择使用vs code 编译器,然后配置好gcc环境,来查看编译期间的每一步发生了什么。

第一步我们在终端输入:

gcc   test.c  - E  - o  test.i    // 进行预处理,预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。

 接下来我们来查看test.i 文件:

我们能看见代码上面有很长一段代码,而我们原本代码中的 #include<stdio.h> 和 #define MAX 100 不见了,因而我们可以得知,预处理阶段进行的是文本操作,将源代码中的头文件和定义的符号进行文本替换。

第二步我们在终端输入:

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

 我们来查看test.s 文件:

我们可以看出来这个文件里面都是汇编代码,由此我们可以得知:编译期间就是将我们的源代码转换成汇编代码。

第三步我们在终端输入:

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

这时我们打开test.o 文件就可以看到,这个文件里面是二进制代码 ,由此我们可以得知汇编过程就是将汇编代码转换成二进制指令。

程序的运行环境

程序的执行过程:

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

预处理详解

预定义符号

__FILE__       // 进行编译的源文件
__LINE__     // 文件当前的行号
__DATE__     // 文件被编译的日期
__TIME__     // 文件被编译的时间
__STDC__     // 如果编译器遵循 ANSI C ,其值为 1 ,否则未定义

这些预定义符号都是语言内置的。

 我们来举个例子:

#include<stdio.h>

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

该代码的运行结果为:

 #define

#define 定义标识符

#define name stuff

举例:

#define MAX 1000
#define reg register           // register 这个关键字,创建一个简短的名字
#define do_forever 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 name( parament - list ) stuff其中的parament - list是一个由逗号隔开的符号表,它们可能出现在stuff 中。
注意: 参数列表的左括号必须与 name 紧邻。如果两者之间有任何空白存在,参数列表就会被解释为 stuff 的一部分。

 我们首先来写一个简单的定义宏的代码:


#define SQUARE(x,y) ((x)*(y))
int main()
{
    int c=SQUARE(5,9);
    return 0;
}

这时候我们我们来查看预编译时候的代码:

我们发现传参的数被代到了上面的公式里完成了替换。

#define的替换规则

在程序中扩展 #define 定义符号和宏时,需要涉及几个步骤。
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由 #define 定义的符号。如果是,它们首先被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由 #define 定义的符号。如果是,就重复上述处理过程。

注意: 

1. 宏参数和 #define 定义中可以出现其他 #define 定义的变量。但是对于宏,不能出现递归。
2. 当预处理器搜索 #define 定义的符号的时候,字符串常量的内容并不被搜索。

那我们如何把参数插入到字符串中?

 #和##

我们来试试这样写代码:

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

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

我们再来在终端输入  gcc test.c -E -o test.i  来查看预处理后的结果:

这时我们看到参数被替换到字符串当中。但是这里只有当字符串作为宏参数的时候才可以把字符串放在字符串中。

还有一个方法就是使用#号:

int i = 10; 
#define PRINT(FORMAT, VALUE)  \ 
 printf("the value of " #VALUE "is "FORMAT "\n", VALUE)

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

预处理后的结果为:

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

 

 #define ADD_TO_SUM(num, value) \
         sum##num += value

int main()
{
    int sum5=0;
    ADD_TO_SUM(5, 10);
    return 0;
}

预编译后的结果为:

我们可以看到我们把sum和5结合到一起形成了一个符号。

宏和函数的对比

宏通常被应用于执行简单的运算。比如在两个数中找出较大的一个。
#define MAX(a, b) ((a)>(b)?(a):(b))

在这里宏有两个优点:

1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序 的规模和速度方面更胜一筹

2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于 > 来比较的类型。 宏是类型无关的
宏也有劣势的地方:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2. 宏是没法调试的。
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
因此宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。

下面我们列举一些宏和函数的优缺点:

 

命名约定

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

#undef

这条指令用于移除一个宏定义。
#undef NAME
// 如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。

举例一个代码:

 

这里的代码在#undef MAX 之前的MAX没有报错,但在之后报错了,这就说明前面的定义被移除了

  • 38
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 15
    评论
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值