news 2026/5/25 3:09:00

C语言学习:预处理详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言学习:预处理详解

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自动补全break

3.#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次,结果异常

解决方案

尽量用函数替代带副作用的宏;若必须用宏,避免传入自增 / 自减类参数。


总结

  1. 预定义符号用于快速获取编译信息,适合调试;
  2. #define常量禁止加分号
  3. 宏定义必须给参数、整体表达式加括号
  4. 宏避免使用带自增 / 自减的参数,防止副作用。

5. 宏替换规则(3 步 + 2 个注意点)

3 步替换流程

  1. 参数预处理:调用宏时,先把参数内部的#define符号替换完成
  2. 文本插入替换将宏文本插入原位置,参数名替换为传入的实参
  3. 重复扫描:整体再次扫描,继续处理剩余#define符号(递归替换)

2 条核心注意

  1. 宏可以嵌套调用其他宏,但不能递归调用自身
  2. 字符串常量内的内容,不会被预处理搜索替换

6. 宏 vs 函数 完整对比(考点重点)

1. 宏的优势

  1. 速度更快无函数调用、返回的栈开销,直接文本替换执行
  2. 类型无关不限制参数类型,整形、浮点型通用,不用重载多个函数;
  3. 可传类型参数:能把int/char这种类型当参数传入,函数做不到。
// 示例:宏实现动态内存分配 #define MALLOC(num, type) (type*)malloc(num * sizeof(type)) // 使用 MALLOC(10, int); // 替换后:(int*)malloc(10 * sizeof(int));

2. 宏的劣势

  1. 代码膨胀:每次调用都会复制代码,频繁使用会让程序体积变大;
  2. 无法调试:预处理阶段就完成替换,不能打断点逐行调试;
  3. 运算符优先级坑不加括号易出错
  4. 副作用风险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 语言语法本身无法区分函数,行业通用规范:

  1. 宏名全部大写(如PRINTMAX),用于区分普通标识符
  2. 函数名不全部大写,一般使用小驼峰 / 下划线命名(如getMaxget_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
  • -DDefine(定义)
  • 效果:等价于在代码开头写#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 symbolsymbol已定义,编译代码#if defined(symbol)
#ifndef symbolsymbol未定义,编译代码#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 #endif

13、头文件的两种包含方式

1. 本地文件包含:#include "filename"

  • 查找策略
    1. 优先在当前源文件所在目录查找;
    2. 找不到再去系统标准库路径查找;
    3. 均无则报编译错误
  • 适用场景:包含自定义头文件(自己编写的.h文件)。
  • 标准路径示例
    • Linux:/usr/include
    • VS2013:C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include

2. 库文件包含:#include <filename.h>

  • 查找策略:直接去系统标准库路径查找,不检索当前目录。
  • 适用场景:包含系统标准头文件(如stdio.hstring.h)。
  • 补充:语法上可用""包含库文件,但查找效率更低,语义不清晰,不推荐。

14. 嵌套文件包含与重复包含问题

问题本质

#include的本质是文本替换:预处理器直接把头文件内容复制到源文件中。若多次包含同一个头文件,会导致内容重复拷贝,造成:

  1. 预编译后代码量冗余;
  2. 结构体、宏、函数声明重复定义,触发编译报错。

示例: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().........

要自己去学习和牢固知识

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/25 3:08:02

文章三:Elasticsearch 集群恢复和索引分布

集群恢复网关与集群索引分布必要性了解在 Elasticsearch&#xff08;简称 ES&#xff09;集群运维中&#xff0c;集群重启恢复、残余索引处理、索引分片分布是保障集群稳定性、数据完整性、读写性能的三大核心基础能力。多数集群故障、数据丢失、分片异常、读写卡顿问题&#x…

作者头像 李华
网站建设 2026/5/25 3:05:01

qemu和gcc编译

编译qemu-arm 公司的系统中没有这个软件&#xff0c;设置外部源也下载不了&#xff0c;只能自己编译qenu-arm。 1. 安装编译依赖 sudo dnf install git gcc make ninja-build glib2-devel pixman-devel zlib-devel python3 2. 克隆并编译&#xff08;仅构建 ARM 目标&#xff0…

作者头像 李华
网站建设 2026/5/25 2:57:22

Titanic数据集分析避坑指南:新手常犯的3个错误及如何修正

Titanic数据集分析避坑指南&#xff1a;新手常犯的3个错误及如何修正泰坦尼克号数据集是机器学习领域的"Hello World"&#xff0c;但看似简单的数据背后藏着无数陷阱。许多初学者在Kaggle等平台提交分析时&#xff0c;常常陷入三个典型误区&#xff1a;用均值粗暴填充…

作者头像 李华