目录
1 预处理器
2 预处理指令
2.1 位置
2.2 格式
2.3 换行
2.4 结束符
2.5 位置限制
3 宏定义
3.1 语法格式
3.2 使用宏定义常量
3.3 使用宏定义数据类型
3.4 宏定义的替换文本
3.5 宏定义嵌套
3.6 取消宏定义
4 带参数的宏定义
4.1 语法格式
4.2 案例演示
4.3 注意事项
4.3.1 宏名和形参列表之间不能有空格
4.3.2 可以省略形参的数据类型
4.3.3 形参和表达式建议使用小括号包裹
4.3.4 宏定义的优先级高于编译器的语法解析
4.3.5 不会进行语法检查
4.4 带参宏定义和函数的区别
4.4.1 对比总结表
4.4.2 案例对比演示
1 预处理器
C 语言编译器在编译程序之前,会先使用预处理器(preprocessor)处理代码。
预处理器的任务是对源代码进行初步处理,生成经过预处理的中间代码,然后再将这些中间代码送入编译器进行编译。
预处理器的主要任务包括:
- 宏替换:用特定的文本替换宏定义中的标识符。
- 文件包含:将指定的文件内容插入到当前文件中。
- 条件编译:根据条件选择性地编译某些代码段。
- 其他任务:如删除注释、展开行号和文件名信息等。
2 预处理指令
预处理指令以 # 号开头,用于指导预处理器执行不同的任务。预处理指令具有以下特点:
2.1 位置
预处理指令通常应该放在代码的开头部分,但在某些情况下也可以放在代码的其他地方。
强烈建议将预处理指令放在文件的顶部,以提高代码的可读性和可维护性。
2.2 格式
预处理指令都以 # 开头,指令前面可以有空白字符(比如空格或制表符),# 和指令的其余部分之间也可以有空格,但为了兼容老的编译器,一般不留空格。
// 推荐写法
#include <stdio.h> // 可以使用格式化代码工具
// 不推荐写法
#include<stdio.h>
#include <stdio.h>
# include <stdio.h>
2.3 换行
预处理指令默认是一行的,如果需要折行,可以在行尾使用反斜杠 \。
#include <std\
io.h>
2.4 结束符
预处理指令不需要分号作为结束符,指令结束是通过换行符来识别的。
#include <stdio.h>; // 这里有分号会报错
#define PI 3.14; // 分号会成为 PI 的值的一部分
2.5 位置限制
预处理指令通常不能写在函数内部,尽管某些编译器的扩展允许这样做,但强烈不建议这么做,以保持代码的可移植性和一致性。
int main ()
{
// 一般不允许写在这里
#include <stdio.h>
return 0;
}
3 宏定义
3.1 语法格式
宏定义是 C 语言预处理器的一种功能,用于用一个标识符(宏名称)来表示一个替换文本。如果在后面的代码中出现了宏名称,预处理器会将它替换为对应的文本,这一过程称为宏替换或宏展开。
宏定义的基本语法形式如下:
#define 宏名称 替换文本
- 宏名称:宏的名称,是一个标识符,通常使用大写字母表示,以便与变量名区分开来。
- 替换文本:宏名称在代码中的每次出现都会被替换为这段文本【纯粹的文本替换】。
3.2 使用宏定义常量
宏定义常量是一种常见的用法,可以用来定义一些固定的数值,这样可以在代码中统一管理和修改这些常量。
#include <stdio.h>
// 定义常量 PI
#define PI 3.14
int main() {
// 定义变量保存半径,值通过用户输入获取
double radius;
printf("请输入半径:");
scanf("%lf", &radius);
// 计算面积并输出
double area = PI * radius * radius;
printf("圆的面积:%.2f\n", area);
return 0;
}
在上面的示例中,使用宏定义声明了常量 PI,在后续代码中,每次出现 PI 都会被预处理器替换为 3.14。
可以在终端中输入预处理指令:gcc -E 源文件名.c -o 源文件名.i ,然后查看生成的 “源文件名.i” 的文件内容,如下所示:
3.3 使用宏定义数据类型
在 C 语言中,宏定义不仅可以用来定义常量,还可以用来定义数据类型。通过宏定义数据类型,可以使代码更具可读性和可维护性。下面是一个具体的示例,展示了如何使用宏定义来定义布尔类型。
#include <stdio.h>
// 宏定义布尔类型
#define BOOL int
#define TRUE 1
#define FALSE 0
int main() {
// 使用宏定义的布尔类型表示真假两种状态
BOOL isPass = FALSE;
BOOL isOk = TRUE;
if (isPass) {
printf("Pass\n");
} else {
printf("Not Pass\n");
}
if (isOk) {
printf("Ok\n");
} else {
printf("Not Ok\n");
}
return 0;
}
在上面的示例中,使用宏定义声明了 BOOL、TURE、FALSE,在后续代码中,每次出现 BOOL 都会被预处理器替换为 int,每次出现 TRUE 都会替换成 1,每次出现 FALSE 都会替换成 0。
3.4 宏定义的替换文本
宏定义的替换文本可以包含任何字符,它可以是字面量、表达式、if 语句、函数调用等。预处理程序对替换文本不作任何检查,直接进行文本替换【纯粹的文本替换】。如果有错误,只能在编译已被宏展开后的源程序时发现。
#include <stdio.h>
// 宏定义
#define M (n * n + 3 * n)
#define PRINT_SUM printf("sum=%d\n", sum)
int main()
{
int n = 3;
int sum = 3 * M + 4 * M + 5 * M;
// 宏展开 3 * (n * n + 3 * n) + 4 * (n * n + 3 * n) + 5 * (n * n + 3 * n);
PRINT_SUM;
// 宏展开 printf("sum=%d\n", sum);
return 0;
}
3.5 宏定义嵌套
宏定义允许嵌套,即在宏定义的替换文本中可以使用已经定义的宏名。在宏展开时,预处理程序会层层替换这些宏名。
#include <stdio.h>
// 定义常量 PI
#define PI 3.1415926
// 定义计算圆面积的宏,使用已定义的 PI
#define S PI * r * r
int main() {
int r = 2;
printf("%f\n", S); // 宏替换变为 printf("%f", 3.1415926 * r * r);
return 0;
}
可以在终端中输入预处理指令:gcc -E 源文件名.c -o 源文件名.i ,然后查看生成的 “源文件名.i” 的文件内容,如下所示:
3.6 取消宏定义
#undef 宏名
如需取消宏定义,可以使用 “#undef 宏名” 命令。“#undef 宏名” 命令用于取消先前通过 #define 指令定义过的宏,使得该宏在后续代码中不再有效。
如果尝试取消一个未定义的宏,#undef 指令将被忽略,不会产生错误。
#include <stdio.h>
// 定义常量 PI
#define PI 3.14159
void func1()
{
printf("PI=%f\n", PI); // 这里可以使用 PI
}
int main()
{
printf("PI=%f\n", PI); // 这里可以使用 PI
#undef PI // 取消宏定义
// 下面的代码会出错,因为 PI 已经被取消定义
printf("PI=%f\n", PI);
return 0;
}
void func2()
{
// 下面的代码会出错,因为 PI 已经被取消定义
printf("PI=%f\n", PI);
}
一旦宏被取消定义,之后在代码中再尝试使用该宏将导致编译错误,报错如下所示:
4 带参数的宏定义
4.1 语法格式
C 语言允许宏带有参数。在宏定义中的参数称为 “形式参数”,在宏调用中的参数称为 “实际参数”,这一点与函数类似。对带参数的宏,在展开过程中不仅要进行文本替换,还要用实际参数去替换形式参数。
带参宏定义的一般形式如下所示:
#define 宏名(形参列表) 替换文本
// 这里的“宏名”和紧随其后的左括号“(”之间不能有任何空格
- 宏名:宏的名称,通常使用大写字母表示。
- 形参列表:宏的形式参数列表,用逗号分隔。
- 替换文本:宏定义的替换文本,可以包含形式参数。
带参宏调用的一般形式如下所示:
宏名(实参列表);
- 实参列表:宏调用时的实际参数列表,用逗号分隔。
4.2 案例演示
使用宏定义,返回两数最大值。
#include <stdio.h>
// 定义带参数的宏
// 1. MAX 就是带参数的宏
// 2. (a,b) 就是形参
// 3. (a>b) ? a : b是带参数的宏对应字符串,该字符串中可以使用形参
// #define MAX(a, b) (a > b) ? a : b
#define MAX(a, b) ((a) > (b) ? (a) : (b)) // 建议使用括号包裹形参
int main()
{
int x, y, max;
printf("输入两个数字: ");
scanf("%d %d", &x, &y);
// 调用带参数的宏
// 1. MAX(x, y); 调用带参数宏定义
// 2. 在宏替换时,预处理器会进行文本替换,同时会使用实参去替换形参
// 3. 即 MAX(x, y) 宏替换成:((x) > (y) ? (x) : (y))
max = MAX(x, y);
printf("最大值: %d\n", max);
return 0;
}
4.3 注意事项
4.3.1 宏名和形参列表之间不能有空格
带参宏定义中,形参之间可以出现空格,但是宏名和形参列表之间不能有空格出现。否则会导致编译错误。因为:预处理器在处理宏定义时,会严格按照语法解析宏定义。如果宏名和形参列表之间有空格,预处理器会将空格之后的内容视为宏定义的替换文本的一部分,而不是形参列表。这会导致宏定义的语法错误或不符合预期的行为。
但是,对于函数而言,函数名和紧随其后的左括号 “(” 之间可以有空格。
#define MAX(a, b) ((a) > (b) ? (a) : (b)) // 正确
#define MAX (a, b) ((a) > (b) ? (a) : (b)) // 错误
4.3.2 可以省略形参的数据类型
在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型。但是,对于函数而言,在函数原型声明或定义时,形参名虽然可以省略,但是形参的数据类型不可以省略。
在带参宏调用中,实际参数包含了具体的数据,需要用它们去替换形式参数,因此实际参数必须指明数据类型,同函数调用时实参一样。
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 5, y = 10;
int max = MAX(x, y); // 正确
4.3.3 形参和表达式建议使用小括号包裹
在宏定义中,替换文本内的形参建议使用括号括起来以避免出错。特别是当形参参与复杂的表达式时,括号可以确保表达式的正确性,以避免因运算符优先级导致的错误。
宏定义可能会导致意外的副作用,特别是在带参数的宏中。为了避免副作用,可以使用括号包围宏定义中的表达式。
#include <stdio.h>
// 使用括号的宏定义
#define ADD_WITH_PARENS(a, b) ((a) + (b))
#define SQ_WITH_PARENS(y) ((y) * (y))
// 不使用括号的宏定义
#define ADD_WITHOUT_PARENS(a, b) a + b
#define SQ_WITHOUT_PARENS(y) y * y
int main()
{
int x = 3, y = 2;
// 使用括号的宏定义
int result1_with_parens = ADD_WITH_PARENS(x + 1, y + 1);
int result2_with_parens = SQ_WITH_PARENS(x + 1);
// 输出结果
printf("使用括号的宏定义:\n");
printf("result1_with_parens = %d\n", result1_with_parens); // 期望结果是 7,实际结果是 7
printf("result2_with_parens = %d\n", result2_with_parens); // 期望结果是 16,实际结果是 16
// 不使用括号的宏定义
int result1_without_parens = ADD_WITHOUT_PARENS(x + 1, y + 1);
int result2_without_parens = SQ_WITHOUT_PARENS(x + 1);
// 输出结果
printf("\n不使用括号的宏定义:\n");
printf("result1_without_parens = %d\n", result1_without_parens); // 期望结果是 7,实际结果是 7
// 替换过程:x + 1 + y + 1
printf("result2_without_parens = %d\n", result2_without_parens); // 期望结果是 16,实际结果是 7
// 替换过程:x + 1 * x + 1
return 0;
}
4.3.4 宏定义的优先级高于编译器的语法解析
宏定义的替换发生在编译之前,因此优先级高于编译器的语法解析。
#include <stdio.h>
#define X 10
#define Y 20
#define Z ((X) + (Y))
int main()
{
int result = Z * 10;
printf("%d", result); // 结果是 300,而不是 210,表达式不加括号的话就是 210
return 0;
}
4.3.5 不会进行语法检查
宏定义可以包含复杂的表达式、if 语句、函数调用等,但预处理器不会对这些内容进行语法检查,因此错误只能在编译阶段发现。
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define PRINT_IF_TRUE(cond, msg) if (cond) { printf("%s\n", msg); }
4.4 带参宏定义和函数的区别
4.4.1 对比总结表
特性 | 带参宏定义(Macro) | 函数(Function) |
---|---|---|
本质 | 文本替换 | 可重用的代码块 |
处理时机 | 编译前由预处理器处理 | 编译阶段由编译器处理 |
计算 | 不对表达式进行计算,仅是文本替换 | 会对表达式进行计算,执行代码 |
内存占用 | 不占用内存(在编译前已处理) | 会占用内存(代码段) |
开销 | 没有函数调用的开销(直接替换) | 有函数调用开销(参数传递、栈帧管理等) |
优化 | 无法享受编译器的优化(因为仅是文本替换) | 可以享受编译器的优化(类型检查、代码优化等) |
安全性 | 容易引入副作用,特别是在复杂表达式中 | 类型安全,编译阶段进行类型检查和优化 |
可读性 | 代码可读性较差,难以调试 | 代码可读性和可维护性更好,易于理解和调试 |
适用场景 | 适用于简单、高效的代码片段替换 | 适用于需要复用、类型安全、可调试的代码块 |
4.4.2 案例对比演示
分别使用函数和带参数的宏计算平方值。
函数实现:
#include <stdio.h>
// 定义计算平方的函数
int SQ(int y)
{
return y * y;
}
int main()
{
int i = 1;
while (i <= 5)
{
printf("%d\n", SQ(i++)); // 1 4 9 16 25
}
printf("i=%d", i); // i=6
return 0;
}
带参数的宏实现:
#include <stdio.h>
// 定义计算平方的宏
#define SQ(y) ((y) * (y))
int main()
{
int i = 1;
while (i <= 5)
{
// SQ(i++) 会被宏替换为 ((i++) * (i++)),
printf("%d\n", SQ(i++)); // 错误操作,i++ 会执行两次,最终无法得到我们想要的结果
// 修改成下面的就可以了
// printf("%d\n", SQ(i));
// i++;
}
printf("i=%d", i); // i=7
return 0;
}