前言
上一节我们了解了文件操作的相关内容,本节我们来了解一下预处理指令,那么废话不多说,我们正式开始今天的学习
预定义符号
在C语言中,设置了一些预定义的符号,可以供我们直接使用,预定义符号是在程序的预处理过程中直接被处理的,那么C语言中定义的符号有下面几种:
1.__FILE__
表示当前正在编译的源文件的地址
2.__LINE__
表示文件当前所在的行号
3.__DATE__
表示文件被编译的日期
4.__TIME__
表示文件被编译的时间
5.__STDC__
如果编译器遵循ANSI C,其取值为1,若不是,则它的取值未定义(gcc编译器支持)
下面我们来尝试使用一下以上预定义符号:
#define _CRT_SECURE_NO_WARNINGS 1#include <stdio.h>int main(void)
{printf("%s\n", __FILE__);printf("%s\n", __DATE__);printf("%s\n", __TIME__);printf("%d\n", __LINE__);return 0;
}
这些符号都是在编译前的预处理阶段就进行了处理
#define 定义常量
#define 有两种不同的功能:
1. #define 可以定义符号(常量)
2. #define 可以定义宏
#define 使用的基本语法如下:
#define name stuff
例如:
#define MAX 100
#define MIN 1
在程序的预处理阶段,代码中的 name 变量就会被替换为数值 stuff
stuff 定义的数值的类型不一定全要为整数,例如:
#define STR "hello world"
我们还可以使用 #define 定义相对复杂的函数,例如:
#define forever for(;;)
在这个代码当中,for 函数的初始化部分、调整部分和判断部分都被省略。由于该 for 函数判断条件没有写,这样就意味着判断条件是恒为真的,就会造成死循环
如果我们定义的 stuff 长度过长,我们可以运用续行符( \ ),除了程序的最后一行以外,程序的每一行都可以添加换行符
#define DEBUG_PRINT printf("file:%s\tline:%d\t \date:%s\ttime:%s\n" ,\__FILE__,__LINE__ ,\
__DATE__,__TIME__ )
那么此时,我们肯定会存在一个疑问:在 #define 定义标识符的时候,需不需要在末尾加上一个;呢?
建议是最好不要加上分号,举个简单的例子:
#define MAX 1000;int main(void)
{int a = MAX;return 0;
}
在此代码的预处理阶段,原代码会将代码转变成如下这种形式:
#define MAX 1000;int main(void)
{int a = 1000;;return 0;
}
此时 int a = 1000后面就有两个分号,就会造成程序的错误
还有可能会出现这样的情况:
#define MAX 1000;int main(void)
{printf("%d\n", MAX);return 0;
}
我们在打印 MAX 的时候,由于其带了分号,就会造成打印不成功
#define 定义宏
#define 允许把参数替换到文本中去,这种实现通常被称作定义宏
宏的声明方式如下:
#define name( parament-list ) stuff
parament-list 是一个由逗号隔开的符号表,他们可能会出现在 stuff 中
其中需要注意:
参数列表的左括号必须与 name 紧紧的挨在一起,如果两者之间存在着空格,那么参数列表就会被解释成 stuff 的一部分
下面来举一个例子:
例如我们需要定义一个宏来求某个数的平方,我们则可以这么写:
#define SQUARE(x) x*xint main(void)
{int a = 4;int ret = SQUARE(a);printf("%d\n", ret);return 0;
}
局限性
但是这个程序存在一定的弊端:若 SQUARE 里面的参数是 a + 1:
#define SQUARE(x) x*xint main(void)
{int a = 4;int ret = SQUARE(a+1);printf("%d\n", ret);return 0;
}
我们发现结果并不是我们想要的 25 ,而是 9 ,这是为什么呢?
因为宏参数在进行替换的时候,是直接采取替换的,也就是说,会被替换成:a + 1 * a + 1,由于乘法的优先级是大于加法的,所以会采取 4 + 4 + 1 的运算模式,所以算出来的结果就是 9
要解决这个问题,我们需要在宏中添加括号:
#define SQUARE(x) (x) * (x)
这样算出来的结果才是 25
在这个定义中我们使⽤了括号,想避免之前的问题,但是同时这个宏可能会出现新的错误
例如:
int a = 5;printf("%d\n" ,10 * DOUBLE(a));
我们想要打印 100 到屏幕上,但我们实际运行程序的时候,打印的却是 55
这个问题,的解决办法是在宏定义表达式两边加上⼀对括号
#define DOUBLE( x) ( ( x ) + ( x ) )
带有副作用的宏参数
当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作⽤,那么你在使⽤这个宏的时候就可 能出现危险,导致不可预测的后果。副作⽤就是表达式求值的时候出现的永久性效果
例如我们要写一个宏,其功能是求两个数的较大值:
#define MAX(X,Y) ((X)>(Y)?(X):(Y))int main(void)
{int a = 1;int b = 2;int m = MAX(a++, b++);//int m = (a++,b++) ((a++)>(b++)?(a++):(b++));printf("m = %d\n", m);printf("a = %d\n", a);printf("b = %d\n", b);return 0;
}
我们可以发现 ++ 的操作在程序中执行的次数不止一次,这样就导致了结果存在问题
宏的替换规则
在程序中扩展#define定义符号和宏时,需要涉及⼏个步骤:
1.在调⽤宏时,⾸先对参数进⾏检查,看看是否包含任何由#define定义的符号。如果是,它们⾸先 被替换
2.替换文本随后被插⼊到程序中原来⽂本的位置。对于宏,参数名被他们的值所替换
3.最后,再次对结果文件进⾏扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上 述处理过程
我们需要注意:
1.宏参数和 #define 定义中可以出现其他 #define 定义的符号。但是对于宏,不能出现递归
例如:
int m = MAX(a, MAX(2, 3));
2.当预处理器搜索 #define 定义的符号的时候,字符串常量的内容并不被搜索
宏与函数的对比
我们来比较一下函数和宏:
#define MAX(X,Y) ((X)>(Y)?(X):(Y))int Max(int x, int y)
{return x > y ? x : y;
}
宏通常被应⽤于执⾏简单的运算,当执行较为简单的运算的时候,宏相较于函数更加具有优势,原因有两点:
1.通过 函数栈帧的创建与销毁 的知识,我们可以知道,函数在调用函数、执行运算、返回函数的时候都需要花费时间,宏在执行小型运算时所需要i的时间相较于函数使用的时间、空间会更少,所以宏⽐函数在程序的规模和速度⽅⾯更胜⼀筹
2.函数的参数必须要声明为特定的类型
和函数相⽐宏的劣势:
1.每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加程序的⻓度
2.宏是没法调试的
3.宏由于类型⽆关,不够严谨
4.宏可能会带来运算符优先级的问题,导致程容易出现错
宏有时候可以做函数做不到的事情。⽐如:宏的参数可以出现类型,但是函数做不到
例如:
#define Malloc(n,type) (type*)malloc(n*sizeof(type))int main(void)
{int* p = (int*)malloc(10 * sizeof(int));Malloc(10, int);return 0;
}
宏和函数的对比:
属性 | #define定义宏 | 函数 |
代码长度 | 每次使用的时候,宏代码都会被插入程序,程序长度会大幅增加 | 函数代码只出现在一个地方,每次使用的时候去那个地方调用对程序长度影响不大 |
执行速度 | 更快 | 因为有函数的调用与返回,相对较慢 |
操作符优先级 | 需要多加括号,因为操作符优先级的缘故,不加括号往往会造成无法预料的结果 | 参数只在调用的时候求值一次,他的结果直接传递给函数,表达式的取值更容易预测 |
带有副作用的参数 | 参数可以被替换到函数的多个位置,可能导致宏的参数被多次计算,产生不可预料的结果 | 参数只在调用的时候求值一次,他的结果直接传递给函数,表达式的取值更容易预测 |
参数类型 | 宏的参数与类型无关 | 函数的参数与类型有关 |
调试 | 宏不方便调试 | 函数可以逐语句调试 |
递归 | 不支持递归 | 支持递归 |
拓展:在C++中,有一个函数叫做内联函数(inline),这个函数既具有函数的特点,又具有宏的特点
# 以及 ##
# 运算符
#运算符将宏的⼀个参数转换为字符串字⾯量。它仅允许出现在带参数的宏的替换列表中
#运算符所执⾏的操作可以理解为“字符串化”
直接写出概念可能有点难以理解,下面我们来举例说明:
我们首先铺垫一下:
int main(void)
{printf("hello""world\n");printf("helloworld\n");return 0;
}
如上面的代码,这两个字符串本质上是相同的
再看一下这个代码:
int main(void)
{int a = 1;printf("the value of a is %d\n", a);int b = 20;printf("the value of b is %d\n", b);float f = 5.6f;printf("the value of f is %f\n", f);return 0;
}
这三个打印函数的格式是非常相似的,那么我们此时就考虑到另一个问题:我们能不能把它封装成一个函数呢?或者说可不可以把它写成一个宏呢?答案是可行的
#define Print(n,format) printf("the value of n is "format"\n", n)int main(void)
{int a = 1;Print(a,"%d");int b = 20;Print(b,"%d");float f = 5.6f;Print(f,"%f");return 0;
}
我们根据前置的铺垫可以写出这样的代码,但当我们在运行程序的时候发现了这样的一个问题:
我们可以观察到:其中的 n 并没有被替换,此时我们就需要用到 # 操作符了,# 操作符可以让参数直接转换成字符串
#define Print(n,format) printf("the value of "#n" is "format"\n", n)int main(void)
{int a = 1;Print(a,"%d");int b = 20;Print(b,"%d");float f = 5.6f;Print(f,"%f");return 0;
}
## 运算符
## 可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的⽂本⽚段创建标识符
## 被称为记号粘合,这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的
我们同样的进行举例说明:
如果我们想写⼀个函数求2个数的较⼤值的时候,不同的数据类型就得写不同的函数
例如:
int int_max(int x, int y)
{return x > y ? x : y;
}
float float_max(float x, float y)
{return x > y ? x : y;
}
但是这样操作下来程序会显得十分冗杂,此时我们可以用##来处理,换一个写法:
#define GENERIC_MAX(type) \type type##_max(type x,type y) \{ \return x>y?x:y; \}//定义函数
GENERIC_MAX(int);
GENERIC_MAX(float);int main(void)
{int r1 = int_max(3, 5);printf("%d\n", r1);float r2 = float_max(3.1f, 5.2f);printf("%f\n", r2);return 0;
}
此时我们用 ## 操作符把位于两端的符号合并成为了一个符号,并由此来定义一个函数
命名约定
因为宏和函数的使用语法很相似,所以在我们书写宏的时候,通常会这样的习惯:
1.将宏的名字全部大写
2.函数的名字不要全部大写
这样我们能更好的分辨宏和函数,当然,有时会存在着特殊情况;例如 offsetof 是一个宏,它是用来计算结构体成员相较于结构体起始位置的偏移量的宏,他虽然是宏但是它全是小写
#undef 的使用
#undef 指令⽤于移除⼀个宏定义,例如:
#define MAX 100int main(void)
{printf("%d\n", MAX);
#undef MAXprintf("%d\n", MAX);return 0;
}
此时我们移除了 MAX 所以会报错
命令行定义
我们在 gcc 编译器上可以在命令行里指定变量的数据,当我们根据同⼀个源⽂件要编译出⼀个程序的不同版本的时候,这个特性有点⽤处,(假定某个程序中声明了⼀个某个⻓度的数组,如果机器内存有限,我们需要⼀个很⼩的数组,但是另外⼀个机器内存⼤些,我们需要⼀个数组能够⼤些)因为我当前的环境是 VS 而且该内容并不是很重要,所以仅仅作了解就行
条件编译
我们在编译一个程序的时候我们如果需要将一条语句或者一组语句编译或者放弃是很方便的,因为我们有条件编译指令
对于某些调试性的代码,直接删除很可惜,但是保留下来又很碍事,所以我们此时可以选择性的编译,例如:
#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;
}
假如我们不需要使用 printf 函数,我们可以注释掉
#define __DEBUG__
这样当我们在运行代码的时候, printf 的指令就不会被执行
下面我们来盘点一下常见的条件编译指令:
1.单分支的条件编译语句
#if 常量表达式
//...
#endif
例如:
int main(void)
{
#if 0printf("haha\n");
#endifreturn 0;
}
这样就不会执行打印 haha 的操作,只有满足 if 后面的条件时,里面的语句才会被执行
2.多分支的条件编译语句
使用 #elif 和 #else 来进行多分支的条件编译语句
#define M 2int main(void)
{
#if M==0printf("haha\n");
#elif M==1printf("hehe\n");
#elif M==2printf("hello world\n");
#elseprintf("luelue");
#endifreturn 0;
}
3.判断是否被定义
#define MAX 0int main(void)
{
#if defined(MAX)printf("haha\n");
#endifreturn 0;
}
使用 #if defined( ) 可以判断是否被定义,他还有另外一这种写法:
#define MAX 0int main(void)
{
#ifdef MAXprintf("haha\n");
#endifreturn 0;
}
两者的逻辑都是一模一样的,若是想写不被定义的代码,只需要在 #define 前加上 !,或者写作 #ifndef
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
当我们代码的长度不长时,我们很难用到条件编译指令,当我们的代码很长,且想要实现多平台的功能时,就经常会用到条件编译指令
头文件的包含
我们知道,我们可以自己创造头文件
#pragma onceint Add(int x, int y)
{return x + y;
}
该头文件里面的内容可以在其他其他文件里被使用,我们通常认为我们自定义的头文件需要用 " " 包含 而不是使用 < >
#define _CRT_SECURE_NO_WARNINGS 1#include <stdio.h>#include "test.h"int main(void)
{int a = 1;int b = 2;int c = Add(a, b);printf("c = %d\n", c);return 0;
}
其实不然," " 和 < > 的不同仅仅在于他们的查找策略不同,二者使用过程中,并不存在绝对的错误
" " 的查找策略:先在源⽂件所在⽬录下查找,如果该头⽂件未找到,编译器就像查找库函数头⽂件⼀样在 标准位置查找头⽂件,如果都找不到就会报编译错误
也就是说,库文件可以用 " " 查找。可是,但是这样做查找的效率就低些,当然这样也不容易区分是库⽂件还是本地⽂件了
嵌套文件包含
假设头文件被多次包含会怎么样?
如果test.c⽂件中将test.h包含5次,那么test.h⽂件的内容将会被拷⻉5份在test.c中。 如果test.h⽂件⽐较⼤,这样预处理后代码量会剧增。如果⼯程⽐较⼤,有公共使⽤的头⽂件,被⼤家都能使⽤,⼜不做任何的处理,那么后果真的不堪设想。 如何解决头⽂件被重复引⼊的问题呢?
答案是使用条件编译
#ifndef __TEST_H__
#define __TEST_H__int Add(int x, int y)
{return x + y;
}#endif
这样做就只会被包含一次,或者使用:
#pragma once
结尾
条件编译指令的内容很多,远远不止我上面归纳的这些,感兴趣的朋友可以自行去了解,我这里便不再作过多的讲解了,谢谢您的浏览