1. 预定义符号
由编译器内置,预处理阶段直接生效,可直接使用,常用于日志 / 调试信息输出:
表格
| 符号 | 含义 |
|---|---|
__FILE__ | 当前编译的源文件名称 |
__LINE__ | 当前代码所在行号 |
__DATE__ | 文件编译的日期 |
__TIME__ | 文件编译的时间 |
__STDC__ | 编译器遵循ANSI C 标准时值为 1,否则未定义 |
示例:
printf("file:%s line:%d\n", __FILE__, __LINE__);而gcc是完全支持ANSI C标准
2.#define定义常量
基础语法
#define name stuff- 本质:纯文本替换,预处理阶段直接把
name替换为stuff - 换行:过长内容可换行,行尾加反斜杠
\(续航符)
关键避坑:不要加分号;
错误写法:#define MAX 1000;
- 风险:替换后会多出分号,破坏语法结构,例如
if-else语句中会出现语法错误。 - 正确写法:
#define MAX 1000
示例拓展:
#define reg register // 关键字别名 #define do_forever for(;;) // 无限循环简写 #define CASE break;case // case自动补全break3.#define定义宏(带参数替换)
基础语法
#define name(参数列表) stuff⚠️ 强制要求:宏名与左括号之间不能有空格,否则会被识别为常量而非宏。
核心坑点:运算符优先级问题
坑 1:参数未加括号
#define SQUARE(x) x*x // 调用 SQUARE(a+1) → 替换为 a+1*a+1,运算顺序错误,结果不符合预期✅ 解决:每个参数都加括号
#define SQUARE(x) (x)*(x)坑 2:整体表达式未加括号
#define DOUBLE(x) (x)+(x) // 调用 10 * DOUBLE(a) → 10*(a)+(a),乘法优先级高于加法,结果错误✅ 解决:整体表达式外层再加一层括号
#define DOUBLE(x) ((x)+(x))💡 通用规则:宏的参数、整体表达式都必须加括号,规避运算符优先级问题。
4. 带有副作用的宏参数
副作用定义
参数表达式求值后产生永久变化(如x++、x--)。
风险
参数在宏内被多次使用时,会被重复执行,产生意外结果。示例:
#define SQUARE(x) (x)*(x) int a=5; SQUARE(a++); // 替换后:(a++)*(a++),a会自增2次,结果异常解决方案
尽量用函数替代带副作用的宏;若必须用宏,避免传入自增 / 自减类参数。
总结
- 预定义符号用于快速获取编译信息,适合调试;
#define常量禁止加分号;- 宏定义必须给参数、整体表达式加括号;
- 宏避免使用带自增 / 自减的参数,防止副作用。
5. 宏替换规则(3 步 + 2 个注意点)
3 步替换流程
- 参数预处理:调用宏时,先把参数内部的
#define符号替换完成 - 文本插入替换:将宏文本插入原位置,参数名替换为传入的实参
- 重复扫描:整体再次扫描,继续处理剩余
#define符号(递归替换)
2 条核心注意
- 宏可以嵌套调用其他宏,但不能递归调用自身;
- 字符串常量内的内容,不会被预处理搜索替换。
6. 宏 vs 函数 完整对比(考点重点)
1. 宏的优势
- 速度更快:无函数调用、返回的栈开销,直接文本替换执行;
- 类型无关:不限制参数类型,整形、浮点型通用,不用重载多个函数;
- 可传类型参数:能把
int/char这种类型当参数传入,函数做不到。
// 示例:宏实现动态内存分配 #define MALLOC(num, type) (type*)malloc(num * sizeof(type)) // 使用 MALLOC(10, int); // 替换后:(int*)malloc(10 * sizeof(int));2. 宏的劣势
- 代码膨胀:每次调用都会复制代码,频繁使用会让程序体积变大;
- 无法调试:预处理阶段就完成替换,不能打断点逐行调试;
- 运算符优先级坑:不加括号易出错;
- 副作用风险:
x++这类参数会被多次执行,结果异常。
3. 对比表格(必背)
| 属性 | #define 宏 | 函数 |
|---|---|---|
| 代码长度 | 每次调用复制代码,易膨胀 | 代码仅 1 份,调用跳转执行 |
| 执行速度 | 快,无调用开销 | 慢,有调用 / 返回开销 |
| 运算符优先级 | 易出错,需手动加括号 | 参数仅求值 1 次,结果稳定 |
| 副作用参数 | 会多次执行,结果异常 | 参数只求值 1 次,安全 |
| 参数类型 | 类型无关,通用性强 | 类型严格,需重载 |
| 调试 | 不可调试 | 可逐行调试 |
| 递归 | 禁止递归 | 支持递归 |
4. 总结
计算简单的情况下可以使用宏
计算相对复杂,就使用函数
ps:有时候宏可以做到的事函数做不到 比如宏的参数可以出现类型 函数却做不到
inline 内联函数可以把函数声明为内联函数 就在函数前加个inline
内联函数具有了函数和宏的优点
基本语法
inline int add(int a, int b) { return a + b; }调用时:
int c = add(1,2);编译器预处理 / 编译后,直接替换成:
int c = 1 + 2;7. # 运算符(字符串化运算符)📌
核心作用
将宏参数直接转换为字符串字面量,仅能用于带参数的宏,操作俗称字符串化。
示例解析
#define PRINT(n) printf("the value of "#n " is %d", n);#include<stdio.h> //#运算符 字符串化 #define PRINT(val,format) printf("the value of "#val " is "format"\n",val) //val前面加上引号 就能把参数转换成字符串 int main() { int a = 10; double f = 3.15f; PRINT(a, "%d"); PRINT(f, "%.2f"); }- 调用:
PRINT(a); - 预处理展开:
printf("the value of ""a"" is %d", a); - 原理:
#n把传入的参数a转为字符串"a",C 语言中相邻双引号字符串会自动拼接,最终输出the value of a is 10。
8. ## 运算符(连接运算符)📌
核心作用
将宏的两个参数直接拼接成一个标识符,常用来批量生成不同类型 / 名称的函数、变量。
示例解析
#define type##_max(type, x, y) return (x>y?x:y);//##连接运算符 #include<stdio.h>//这里宏定义的是一个函数体 //经过##连接 max_type 是函数名 (type x, type y)是参数 type是返回类型 #define GENRIC_MAX(type) type max_##type(type x, type y)\ {\ return x>y?x:y;\ } GENRIC_MAX(char) GENRIC_MAX(int) GENRIC_MAX(float) int main() { int a = 10; int b = 20; int m1 = max_int(a, b); printf("%d\n", m1); return 0; }- 传入
int:生成int_max函数;传入float:生成float_max函数 - 图中输出
3(int 类型)、4.500000(float 类型),就是该宏批量生成函数的运行结果。
补充说明
##实际开发使用较少,多用于底层框架、泛型封装、批量代码生成场景。
9. 宏与函数的命名约定💡
C 语言语法本身无法区分宏和函数,行业通用规范:
- 宏名:全部大写(如
PRINT、MAX),用于区分普通标识符 - 函数名:不全部大写,一般使用小驼峰 / 下划线命名(如
getMax、get_max)
10. #undef 取消宏定义 📌
作用
删除已定义的宏,让宏失效;如果要重新定义同名宏,必须先用#undef清除旧定义。
语法
#undef 宏名示例
#define MAX 100 #undef MAX // 取消MAX的定义 #define MAX 200 // 重新定义,合法用途
- 避免不同头文件的宏名冲突
- 局部禁用某个宏
- 重新定义宏
11. 命令行定义宏(-D 选项)💡
核心作用
不用在代码里写 #define,直接在编译命令行定义宏,灵活切换程序版本。
代码示例
#include <stdio.h> int main() { int array[ARRAY_SIZE]; // ARRAY_SIZE 代码里没定义,靠编译命令传值 int i = 0; for(i = 0; i < ARRAY_SIZE; i++) array[i] = i; for(i = 0; i < ARRAY_SIZE; i++) printf("%d ",array[i]); printf("\n"); return 0; }编译指令(Linux/gcc)
gcc -D ARRAY_SIZE=10 program.c-D:Define(定义)- 效果:等价于在代码开头写
#define ARRAY_SIZE 10
应用场景
同一套代码,编译出不同版本:
- 内存小的机器:
-D ARRAY_SIZE=5(小数组) - 内存大的机器:
-D ARRAY_SIZE=100(大数组)
12. 条件编译
核心作用
选择性编译代码,可灵活控制部分代码是否参与编译,常用于调试代码开关、跨平台兼容、版本区分,无需删除注释代码,修改宏定义即可切换。
1. 示例代码解读
#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; }- 开启调试:保留
#define __DEBUG__,编译时会执行printf; - 关闭调试:注释 / 删除
#define __DEBUG__,printf代码直接被预处理器忽略,不参与编译。
2. 类常用条件编译指令
(1)基础单分支:#if ... #endif(不能用变量)
根据常量表达式真假控制编译,表达式由预处理器提前求值:
#define __DEBUG__ 1 #if __DEBUG__ // 代码 #endif(2)多分支:#if...#elif...#else...#endif
类似if- else if -else,支持多条件判断:
#if 常量表达式1 // 代码1 #elif 常量表达式2 // 代码2 #else // 代码3 #endif(3)判断宏是否定义(高频用法)
| 指令 | 含义 | 等价写法 |
|---|---|---|
#ifdef symbol | 若symbol已定义,编译代码 | #if defined(symbol) |
#ifndef symbol | 若symbol未定义,编译代码 | #if !defined(symbol) |
(4)嵌套条件编译
用于跨平台适配(Windows/Linux/Unix),多层条件叠加:
#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 #endif13、头文件的两种包含方式
1. 本地文件包含:#include "filename"
- 查找策略:
- 优先在当前源文件所在目录查找;
- 找不到再去系统标准库路径查找;
- 均无则报编译错误。
- 适用场景:包含自定义头文件(自己编写的
.h文件)。 - 标准路径示例
- Linux:
/usr/include - VS2013:
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
- Linux:
2. 库文件包含:#include <filename.h>
- 查找策略:直接去系统标准库路径查找,不检索当前目录。
- 适用场景:包含系统标准头文件(如
stdio.h、string.h)。 - 补充:语法上可用
""包含库文件,但查找效率更低,语义不清晰,不推荐。
14. 嵌套文件包含与重复包含问题
问题本质
#include的本质是文本替换:预处理器直接把头文件内容复制到源文件中。若多次包含同一个头文件,会导致内容重复拷贝,造成:
- 预编译后代码量冗余;
- 结构体、宏、函数声明重复定义,触发编译报错。
示例:test.c多次#include "test.h",test.h内容会被复制多份。
两种解决方案
方案 1:条件编译守卫(通用、跨平台,笔试高频考点)
在头文件开头结尾添加:
#ifndef __TEST_H__ #define __TEST_H__ // 头文件核心内容 void test(); struct Stu { int id; char name[20]; }; #endif //__TEST_H__- 原理:第一次包含时定义宏
__TEST_H__,后续再次包含时,#ifndef判定宏已定义,跳过头文件内容。
方案 2:#pragma once(编译器指令,便捷但兼容性略差)
#pragma once // 头文件内容- 原理:编译器保证该头文件只被包含 1 次,语法更简洁。
15. 高频笔试真题解
1. 头文件中#ifndef/#define/#endif的作用?
防止头文件被重复包含,避免结构体、宏、函数声明重复定义引发编译错误,是跨平台通用的头文件保护机制。
2.#include <filename.h>和#include "filename.h"的区别?
<>:直接在系统标准库路径查找,用于标准库头文件;"":先在当前目录查找,再查找系统路径,用于自定义头文件。
总结:
还有很多其他预处理指令 #error #pragma #line #pragma pack().........
要自己去学习和牢固知识