5 min to read
椋鸟C语言笔记#37
预处理指令
萌新的学习笔记,写错了恳请斧正。
预处理指令就是 C 语言中的一些特定格式的用于辅助编译器预处理的指令,这些指令在编译时的预处理阶段就被处理,并不会被保留到汇编文件中。
预定义符号
首先是一些内置的预定义符号,这些符号可以直接使用,在预处理阶段就被替换为对应的其他内容
包括(注意前后都是两个下划线):
- __FILE__:替换为源文件的文件名(包括地址与后缀)
- __LINE__:替换为文件中此符号所在行的行号
- __DATE__:替换为文件被编译的日期
- __TIME__:替换为文件被编辑的时间
- __STDC__:如果编译器遵循 ANSI C 标准,这将被替换为 1,否则未定义
预处理指令 #define
#define 定义的宏常量
#define 可以在预处理时把程序中的某个字段直接替换为另一个字段,然后在继续编译
注意:
- #define 之间可以嵌套
- #define 的第一个字段(被替换字段)必须全连续(不能存在空白字符),而第二个字段可以存在空白字符甚至多行字符(需要续行符辅助)
- #define 的定义不能产生循环
使用示例如下(输出 100):
#include <stdio.h>
#define M 100
#define __p printf
#define PRINT __p("%d", M)
int main()
{
PRINT;
return 0;
}
注:如果不写替换字段,则默认替换为 1
使用续行符扩展 #define 的功能
上面提到可以使用续行符来完成多行的替换,具体怎么操作呢?
其实很简单,只要正常分行,前面每一行的结尾都加上一个反斜杠即可(反斜杠后就是换行符,之间不能再有任何其他字符!!!),比方说:
#include <stdio.h>
#define DEBUG_PRINT printf("file:%s\nline:%d\ndate:%s\ntime:%s\n",\
__FILE__, __LINE__,\
__DATE__, __TIME__ \
)
int main()
{
DEBUG_PRINT;
return 0;
}
#define 定义宏
#define 指令还包含一个机制,就是宏定义功能
宏定义可以理解为一种另类的 “函数”,可以进行多字段的替换
其声明方式为:
#define name(parament_list) stuff
// name是宏的名称
// parament_list是参数列表,类似于函数的参数列表
// stuff是替换内容
其功能就是将 stuff 中所有的与参数列表相同的字段替换为我们使用时输入的字段
比方说下面就是定义了一个计算平方数的宏(注意这里后面替换字段打括号,不然替换后可能会有运算符顺序问题,再次声明宏是替换字符,不是算完给一个返回值):
#include <stdio.h>
#define SQUARE(x) ((x)*(x))
int main()
{
printf("%d\n", SQUARE(7));
return 0;
}
有人可能会认为宏没什么必要,函数能覆盖宏的功能,但其实不是这样。宏定义说到底还是字段的替换,是没有参数类型的说法的,参数不一定是有意义的变量,甚至可以是类型名、函数名乃至无意义的字符段。所以就衍生出许多有趣的用法,比方说我们平时大量使用动态内存分配时会觉得书写麻烦,就可以这样:
#include <stdio.h>
#include <stdlib.h>
#define MALLOC(p, size, TYPE) TYPE *p = ((TYPE*)malloc(size*sizeof(TYPE)))
int main()
{
MALLOC(p, 114514, int);
printf("%d\n", malloc_usable_size(p));
free(p);
p = NULL;
return 0;
}
这里宏的有一个参数就是类型名,显然函数是做不到的
使用宏时不要使用有副作用的参数
带有副作用指参数表达式会影响某些变量的值,比方说自增自减表达式
比方说下面这段程序的输出结果是:x=6 y=10 z=9
#include <stdio.h>
#define MAX(a, b) ((a)>(b)?(a):(b))
int main()
{
int x = 5, y = 8, z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);
return 0;
}
为什么呢,我们看看其预处理文件中的代码部分:
int main()
{
int x = 5, y = 8, z = ((x++)>(y++)?(x++):(y++));
printf("x=%d y=%d z=%d\n", x, y, z);
return 0;
}
说到底还是因为宏是字段替换而不是向函数那样创建栈帧空间有形参和返回的概念
编译器到底是怎么处理宏的
- 在调用宏时首先对参数进行检查,看看有没有由 #define 定义的符号,如果有那就先替换
- 然后将替换字段插入到程序中原来字段的位置
- 最后再一次对结果进行扫描,如果又有由 #define 定义的符号,那就返回第一步
注意:在扫描 #define 定义的符号时,字符串中的内容(双引号包起来的内容)会被跳过
宏与函数的对比
宏优势:
- 一些简单的计算,比方说上面的取最大值的宏,使用宏拥有更高的运行效率
- 宏的参数是靠字段替换存在的,所以能完成更多特殊的功能,比方说上面的 malloc
宏劣势:
- 宏可能会在预处理后大大加长代码长度
- 宏无法被调试,出了问题不好找
- 宏没有类型的概念,不够严谨,可能不利于代码的健壮性
- 宏极容易造成运算符优先级的问题,在定义时需要小心加括号
- 宏不能递归
约定俗成的命名规则
为区分宏定义与函数的定义,一般默认宏名为全大写,而函数名不要全大写
#与 ## 运算符
#运算符(字符串化)
#运算符用于将宏的一个参数转换为字符串字面量,只能用在有参数的宏中。
比方说下面是一个打印某变量值的宏定义:
#include <stdio.h>
#define PRINT(n) printf("The value of "#n" is %d\n", n)
int main()
{
int a = 10;
PRINT(a);
return 0;
}
这里 #n 的 n 照样被替换为 a,随后字符串化使得这个 a 变成了字符串量,也就是说 PRINT(a); 会被预处理为如下一行代码:
printf("The value of ""a"" is %d\n", n);
//printf的格式控制字符串其实是可以由多个字符串拼起来的,中间不要有逗号分割即可
## 运算符(记号粘合)
用于把位于其两侧的符号合成为一个符号以使某侧的符号被判定为 #define 定义的标识符替换
比方说,我们用定义把 A 替换为 B,然后下面有一个 AC,这时如果什么都不管,AC 还是 AC(AC 整体间没有空白字符分割会被当成一整个符号)。这时我们就可以把 AC 写成 A##C,那么 A 和 C 这两个符号就会被编译器认为是两个符号,就会把 A 替换为 B,然后 B 与 C 粘合产生新符号 BC。
比如我们写一个生成取最大值函数的宏:
#include <stdio.h>
#define GENERIC_MAX(type) \
type type##_max(type x, type y) \
{ \
return x > y ? x : y; \
}
GENERIC_MAX(int)
GENERIC_MAX(float)
int main()
{
printf("%d\n", int_max(3, 4));
printf("%f\n", float_max(3.0f, 4.0f));
}
#undef 指令
这条指令用于移除一条 #define 定义(就是说这条定义到此为止了,下面不要再换了)
比方说下面输出 0 1 0(只有第二个 A 在预处理时被替换为 1):
#include <stdio.h>
int main()
{
int A = 0;
printf("%d\n", A);
#define A 1
printf("%d\n", A);
#undef A
printf("%d\n", A);
return 0;
}
命令行定义
#define 的定义也可以在编译时添加,比方说:
$ gcc -D SIZE=10 program.c
//Linux gcc环境下
这条指令就是在编译时指定 #define SIZE 10
条件编译
条件编译就和条件语句差不多,是判断条件然后选择是否编译某段语句或执行某段预处理指令
常见的条件编译指令有:
- 如果常量表达式成立则编译中间的代码
#if 常量表达式
//...
#endif
比如:
#define __DEBUG__ 1
#if __DEBUG__
//...
#endif
- 多分支条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#elif 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
- 判断是否被定义
下面两段含义相同,代表判断 symbol 是否被定义:
#if defined(symbol)
//...
#endif
#ifdef symbol
//...
#endif
下面两段含义相同,代表判断 symbol 是否未被定义:
#if !defined(symbol)
//...
#endif
#ifndef symbol
//...
#endif
条件编译指令可以嵌套:
//嵌套
#ifdef A
#ifdef B
printf("A and B is defined.\n"); //...
#endif
#endif
#include 包含头文件
#include 包含头文件相当于把对应的文件直接复制到 #include 所在的位置
包含库文件
//包含库文件
#include <filename.h>
编译器将在标准路径下查找对应的头文件,如果找不到这报编译错误
包含本地头文件
//包含本地头文件
#include "filename.h"
编译器首先在源文件所在目录下查找,如果没找到则去标准路径继续查找,找不到则报编译错误
包含库文件也可以使用双引号,但是运行效率低且不易区分头文件,是不好的习惯
标准路径
Linux gcc 下:/usr/include
嵌套文件包含
由于头文件中可能还包含了其他的头文件,有时部分头文件可能被包含多次,这会给编译器带来不必要的压力。如果是一个特大工程,这甚至会造成以小时甚至天计算的时间损失。
所以我们有两个解决方案:
- 条件编译
比如头文件 test.h,我们把头文件内容写在如下代码中:
#ifndef __TEST_H__
#define __TEST_H__
...//头文件的内容
#endif //__TEST_H__
这样这个头文件就最多只会被包含一次了
- 使用 #pragma 指令
只要在头文件前加一行指令即可确保次头文件只被包含一次:
#pragma once
//一些老版本的编译器不支持
其他预处理指令
不常用,不介绍
Comments