目录
在之前,我们已经介绍了.c文件在运行的过程图解,大的方面要经过两个方面。
一、翻译环境
1.预处理(预编译)
2.编译
3.汇编
4.链接
二、运行环境
我们在这里,主要介绍以下预处理阶段的事情,重点是#define定义宏,宏和函数对比的各自优点和缺点。
预处理阶段 主要处理那些源文件中#开始的预编译指令**。比如:#include,#define,处理的规则如下:**
(1)将所有的 #define 删除,并展开所有的宏定义。
**(2)**处理所有的条件编译指令,如:#if、#ifdef、#elif、#else、#endif
(3)处理#include 预编译指令,将包含的头文件的内容 插入到 该预编译指令的位置。这个过程是递归进行的,也就是说也 被包含的头文件 可能包含其他文件 。
**(4)**删除所有的注释
(5) 添加行号和文件名标识 ,方便后续编译器生成调试信息等。
(6) 或保留所有的#pragma 的编译器指令,编译器后续会使用。
一.预定义符号和#define定义常量
1)预定义符号
在C语言中,设置了一些预定义符号,可以直接使用,预定义符号也是在预处理期间处理的。
_ FILE _ //进行编译的源文件
_ LINE _ //文件当前的行号
_ DATE _ //文件被编译的日期
_ TIME _ //文件被编译的时间
_ STDC _ //如果编译器遵循ANSI C,其值为1,否则未定义
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main()
{
FILE* pf = fopen("test1.txt", "w+");
printf("file:%s line:%d date:%d\n", __FILE__, __LINE__, __DATE__);
return 0;
}
这里,可以看出,我们是可以直接运行这些与定义符号的。
2)#define定义常量
cpp
# define MAX 100
# define reg register //为register这个关键字,创建一个新的名字
#define do_forever for(;;) //死循环,起一个更加形象的名字
#define CASE break;case //在写case语句的时候自动把break写上
//如果定义的stuff过长,可也分成几行写, 除了最后一行外,每行的后面都加上一个反斜杠(续行符)
#define DEBUG_PRINT printf("fine:%s tline:%d\t \
date:%s \ time:%s\n \
",__FILE__,__LINE__, \
__DATE__,TIME__ )
这里需要注意的是,当定义的语句过长的时候,用**'\'来换行继续写****,这里当续行符来使用。**
注意:在使用#define定义标识符的时候,不要在最后加上分号(防止我们出错)
二.#define定义宏
#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏或者定义宏(define macro)。
#definename( parament-list ) stuff
这里parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:参数列表的左括号必须与name紧邻**,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。**
举例:
cpp
#define SQUARE( x ) x*x
cpp
int a = 5;
printf("%d\n", SQUARE( a + 1));
这里看这一段代码,我们可能会说执行的结果为36,但是实际上它将打印11。
我们为了得到36的正确结果,在定义宏的时候,为了避免出现的错误,我们一般是在宏定义表达式的两边加上一对括号。
#define DOUBLE(x) ( (x) + (x) )
注意:所有用于对数值表达式进行求值得宏定义都应该用这种加括号的方式,避免在使用宏时由于参数中得操作符或操作符之间不可预料得相互作用。
宏的替换规则
1.在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被它们的值所替换。
3.最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就是重复上述处理过程。
注意:1 宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
三.宏和函数的对比
1.宏通常被应用于简单的运算。(如下:)
#defineMAX(a,b)( (a) > (b) ? (a) : (b) )
用函数来完成上述代码比较两数大小也是可以的,但是于宏相比,宏更好。
宏的优势:
1.宏比函数再程序的规模和速度上更胜一筹。
2.宏的参数是于类型无关的。(很重要)
3. 宏的参数可以出现类型,函数做不到。(很重要)
宏的参数出现了类型。
cpp
#define MALLOC(num, type) (type )malloc(num*sizeof(type))
MALLOC(10,int);
//上述的代码,相当于下面的代码
(int*)malloc(10 * sizeof(int));
宏的劣势:
1.每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2.宏是没办法调试的。
3.宏由类型无关,也就不够严谨。
4.宏可能会带来运算符优先的问题,导致程序容易出错。
|--------------|-------------------------------------------------------------------------------|-----------------------------------------------------|
| 属性 | #define定义宏 | 函数 |
| 代码长度 | 每次使用时,宏代码都会被插入到程序中**。除了非常小的宏之外,程序长度会大幅度增加** | 函数的代码只出现于一个地方;每次使用函数的时候,都调用那个地方的同一份代码 |
| 执行速度 | 更快 | 存在函数的调用和返回的额外开销,相对慢一些 |
| 操作符优先级 | 宏参数的求值是再所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以****建议宏在书写的时候多加括号 | 函数参数只在函数调用的时候求值一次,将结果值传递给函数。 |
| 带有副作用的参数 | 参数可能被替换到宏中的多个位置,如果宏的参数被多次计算,带由副作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次**,结果容易控制** |
| 参数类型 | 宏的参数与类型无关**,只要对参数的操作是合法的,它就可以使用于任何参数类型。** | 函数的参数是与类型有关的**,如果参数类型不同,就需要不同的函数,即使它们的任务是不同的。** |
| 调试 | 宏不方便调试 | 函数可以逐语句调试 |
| 递归 | 不能递归 | 可以递归 |
[宏和函数的对比]
四、#和##运算符
**1. #运算符将宏的一个参数转换为字符串字面量。**它仅允许出现在带参数的宏的替换列表中。
2. #运算符所执行的操作可以理解为"字符串化"
cpp
int a = 10;
#define PRNT(n) printf("the value of "#n " is %d",n);
##运算符:可以把位于它两边符号和成一个符号**,它允许宏定义从分离的文本片段创建标识符。****##被称为记号粘合**
当定义一个比较两个数较大值的时候,类型不同的数据就得写不同的函数,例如:
cpp
int int_max(int x, int y)
{
return x > y ? x : y;
}
float float_max(float x, float y)
{
return x > y ? x : y;
}
这时候,用##来实现宏定义,这时候就非常简单了,下面的代码只要传不同的类型,就可以实现不同类型的函数定义。
cpp
#define GENERIC_MAX(type) \
type type##_max(type x ,type y) \
{ \
return (x > y ? x : y); \
} \
五、条件编译
在执行编译一个程序的时候,我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
1.
#if 常量表达式
#endif
2.多个分支的条件编译
#if 常量表达式
#elif 常量表达式
#else
#endif
3.判断是否被定义
#if defined(symbol)
#ifdef
#if !defined(symbol)
#ifdef symbol
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