预处理讲解
1.1预定义的符号
_FILE_ //进行编译的源文件
_LINE_ //文件当前的行号
_DATE_ //文件被编译的日期
_TIME_ //文件被编译的时间
_STDC_ //如果编译器遵循ANSI C,其值为1,否则未定义。
这些预定义符号都是语言内置的。
![](https://i-blog.csdnimg.cn/blog_migrate/53d577609ce1f04bd87bccabff741194.png)
如图所示:这些符号是有准确含义的。
vs只遵循一部分ANSI C的标准,所以属于未定义_STDC_
1.2#define
1.2.1#define定义标识符
语法:#define name stuff
#define reg register
#define do_forever for(;;)
#define CASE break;case
|
//为 register这个关键字,创建一个简短的名字
//用更形象的符号来替换一种实现
//在写case语句的时候自动把 break写上。
|
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )
提问:
在define定义标识符的时候,要不要在最后加上 ; ?
建议不要加上 ; ,这样容易导致问题。
比如下面的场景:
#define mm 100;
if(condition)
max = mm;
else
max = -100;
这里会出现语法错误。因为mm被替换成了100;语句变成了max=100;;两个分号相当于两行,if语句有两行需要大括号。
1.2.2#define定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
下面是宏的申明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的参数表,它们可能出现在stuff中
注意:
参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
进行宏的定义时,要对参数的两边加上括号,表达式的两边也要加上括号
示例:#define max(x,y) ((x)>(y) ? (x) : (y))
提示:
所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
1.2.3#define替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
示例:printf("mm = %d",100);在字符串mm = %d中的宏不会被搜索。
1.2.4#和##
如何把宏的参数插入到字符串中?
应用示例:
printf("the value of a is %d",a);
printf("the value of b is %d",b);
printf("the value of c is %d",c);
一个一个写太麻烦,但字符串中的abc不会被宏搜索,这时#就可以完成字符串的修改。
下面介绍一个规律:
printf("hello world\n");
printf("hello ""world\n");
//两个字符串hello 和world\n
printf("hell""o w""orld\n");
//三个字符串hell和o w和orld\n
但打印结果都是hello world
char* p = "hello ""bit\n";
printf("hello"" bit\n");
printf("%s", p);
打印结果都是hello bit
我们发现字符串是有自动连接的特点的。
#define PRINT(FORMAT, VALUE) printf("the value is "FORMAT"\n", VALUE);
...
PRINT("%d", 10);
这里只有当字符串作为宏参数的时候才可以把字符串放在字符串中,字符串"%d"是宏参数中的FORMAT
结合#就可以完成字符串替换的效果了
#define PRINT(n) printf("the value of"#n" is %d\n",n)
原因在于#n会替换n中的值,并将#n替换成"n"(n会变成参数值),字符串内容就变成了"the value of""n"" is %d\n"
如:PRINTF(a)会被替换成printf("the value of""a"" is %d\n",a)
代码中的 #VALUE 会预处理器处理为:"VALUE" .
下面介绍##在宏中的用法
##可以把位于它两边的符号合成一个符号。它允许宏定义从分离的文本片段创建标识符。
示例:
#define CAT(C, num) C##num
int main()
{
int Class104 = 1000;
printf("%d\n",CAT(Class, 104));
return 0;
}
//结果打印1000
注:这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。
这两个符号用的机会非常少。明白意思就行。
1.2.5带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
例如:
x+1;
//不带副作用
x++;
//带有副作用
MAX宏可以证明具有副作用的参数所引起的问题。
#define MAX(x, y) ((x) > (y) ? (x) : (y))
int main()
{
int a = 3;
int b = 5;
int m = MAX(a++, b++);
//注意,宏的参数是不进行计算的,而是直接替换
//替换后表达式变成:
int m = ((a++) > (b++) ? (a++) : (b++));
//此时先算a++,a变成4,然后b++,b变成6,在使用时,是3 < 5,结果为假,执行b++,先使用,后++,所以m是6,b的值为7,a的值为4。这和想表达的意思相差很多。
printf("m = %d , a = %d , b = %d\n",m,a,b);
return 0;
}
1.2.6 宏和函数对比
宏通常被应用于执行
简单的运算。比如在两个数中找出较大的一个。
那为什么不用函数来完成这个任务?
原因有二:
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。宏只需要进行逻辑运算,函数要进行函数调用,逻辑运算,函数返回。
2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。
宏的缺点:当然和函数相比宏也有劣势的地方:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2. 宏是没法调试的。代码调试是生成可执行程序调试,而宏在预处理阶段就已经被替换了。复杂逻辑运算就不推荐用宏。
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题(可以通过加括号减少问题),导致程容易出现错
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
示例:
#define MALLOC(num, tpye) ((type*)malloc(num*sizeof(type)))
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
//你不能这样
int* p = (int*)malloc(10 , int);因为函数传参不能传类型
//使用宏可以达到传参数的效果
int* q = MALLOC(10,int);
//被替换成
int* q =
((int*)malloc(10*sizeof(int)));
return 0;
}
宏和函数的对比
属 性
|
#define定义宏
|
函数
|
代 码 长 度
|
每次使用时,宏代码都会被插入到程序中。除了非常
小的宏之外,程序的长度会大幅度增长
|
函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
|
执 行 速 度
|
更快
|
存在函数的调用和返回的额外开销,所以相对慢一些
|
操 作 符 优 先 级
|
宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。
|
函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。
|
带 有 副 作 用 的 参 数
|
参数可能被替换到宏体中的多个位置,所以带有副作
用的参数求值可能会产生不可预料的结果。
|
函数参数只在传参的时候求值一次,结果更容易控制。
|
参 数 类 型
|
宏的参数与类型无关,只要对参数的操作是合法的,
它就可以使用于任何参数类型。
|
函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的。
|
调 试
|
宏是不方便调试的
|
函数是可以逐语句调试的
|
递 归
|
宏是不能递归的
|
函数是可以递归的
|
在c99和c++的语法中,有一个内联函数(inline),具有宏和函数的优点。
1.2.7命名约定
一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。
那我们平时的一个习惯是:把宏名全部大写,函数名不要全部大写。
但并不一定,也有全小写的情况,比如结构体求偏移量offsetof就是一个宏,命名约定是给我们的代码一个命名标准。
1.3#undef
这条指令用于移除一个宏定义。可以移除宏,还可以移除#define定义的标识符。
#undef NAME
//
如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
1.4命令行定义
许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。在Linux中容易演示
![](https://i-blog.csdnimg.cn/blog_migrate/e6286e71ffa43fe6aea339d7b59eaffa.png)
图中框中的都可以算作命令行,可以在命令行中对代码中的符号进行定义,如图所示:SZ没有进行-DSZ = 10操作之前,SZ属于未声明符号,进行-DSZ = 10操作之后,生成a.out文件,执行得到结果,这就是命令行定义。
当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。)
![](https://i-blog.csdnimg.cn/blog_migrate/33537e339d4fe9f1fb05725be28eb9cc.png)
![](https://i-blog.csdnimg.cn/blog_migrate/158015f17a1ff1f9cbe4d84d47e5a305.png)
is是一个命令,-a和-l可以理解为参数,对同一个命令给不同的参数,会得到不同的结果。
1.5条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
比如说:调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。
int main()
{
int i = 0;
int n = 10;
for(i = 0; i< n; i++)
{
#if 1
//if后面是常量表达式,结果为真,if和endif框起来的部分就参与编译,结果为假,不参与编译
//不能是变量,#if是预处理部分进行否使用的判断,而常量是执行时才产生的
printf("%d", i);
//当我们需要选择性的使用这行代码时,除了注释掉还可以使用条件编译指令
#endif
}
return 0;
}
这个功能在实际运用中还是很普遍的
条件编译指令是预处理指令,预处理指令是预处理阶段执行的指令。#define和#include也是预处理指令
其他的预处理指令还有:
1.条件编译
#if 条件表达式
//……
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
2.多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
3.判断是否被定义
#if defined(symbol)
//检查symbol是否被定义过,定义过则为真,参与编译
//……
#endif
#ifdef symbol
//与#if defined(symbol)是一个意思
//……
#endif
#if !defined(symbol)
//定义过则为假,不参与编译
//……
#endif
#ifndef symbol
//与#if !defined(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
一套代码想要跨平台就会大量使用条件编译,根据接口来进行条件判断使用什么样的代码。
1.6文件包含
从前面的知识可知,预处理阶段会进行头文件的包含,如果头文件多次声明会怎么样?
![](https://i-blog.csdnimg.cn/blog_migrate/560ac2cc107cd7002e641bf4930daf02.png)
已知test.h中的内容是:
int add(int x, int y)
{
return x+y;
}
在工程栏选择文件名右击,选择属性,进行如下操作:
![](https://i-blog.csdnimg.cn/blog_migrate/b68ace6585922f3da427d4cddf9ec0ec.png)
改为是,点击应用,点击确定,ctrl+f7,在文件路径底下找到debug文件夹中的test.i文件,打开后转到结尾。可以看到
![](https://i-blog.csdnimg.cn/blog_migrate/cff8d839da420c895576ca770ba3d802.png)
文件被重复包含了五次,造成了代码冗余
在实际中,会有多名程序员来写代码,在进行整合时会造成头文件的重复包含,如何即使声明多次,也只包含一次呢?
进行条件编译
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif
#ifndef __TEST_H__进行条件判断,第一次包含,__TEST_H__未定义,那么#define __TEST_H__,完成__TEST_H__的定义,第二次包含__TEST_H__已经定义,条件为假,#ifndef和#endif中间的部分不再参与编译,定义的符号一般根据头文件的名字来写。
不过现在使用#pragma once也能达到同样的效果。在特别古老的编译器下是不支持这种写法的,比如VC6.0。
高端的编译器会自动加上。
1.6.1头文件包含的方式
本地文件包含:#include"filename.h"
库文件包含:#include<stdio.h>
包含的方式有什么区别?
查找策略不同
本地文件查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误
库文件查找策略: 查找头文件直接去标准路径(可以通过Everything工具查找头文件位置)下去查找,如果找不到就提示编译错误
所以库文件也可以用""引用,就是效率比<>低,当大量头文件的包含使用""引用,会造成效率下降