【金丹·62】预处理:#include和#define的真相

【金丹·62】预处理:#include和#define的真相

码农修仙传 · 金丹期 · 第62篇
我是玄芯散人,带你从炼气修到大乘。


境界标识

╔══════════════════════════════════╗
║     金丹期 · 第62篇              ║
║     预处理                        ║
║     #include和#define的真相       ║
║     预计阅读:12分钟              ║
╚══════════════════════════════════╝

修仙引入

你在C文件开头写了一行 #include <stdio.h>,然后就能用 printf。

你觉得这行代码"引入了stdio库"。实际上#include做的事很简单:把stdio.h的全部内容原封不动地复制到你这行代码的位置。如果你用 gcc -E main.c -o main.i 看预处理结果,会发现一个简单的hello world变成了上千行。

但预处理不止是#include。宏定义、条件编译、字符串化,这些都是预处理的活。很多人只会用#include,对宏的陷阱和条件编译的能力一知半解。

金丹期要看透编译器的四段流水线。今天拆第一段:预处理。


硬核主体

预处理器是什么

C语言的编译过程分四步:预处理,编译,汇编,链接。预处理器是第一步,它在编译器真正干活之前先跑一遍,把你的源代码做一些纯文本层面的处理。

处理完之后,输出一个纯文本文件(通常叫 .i 文件),这个文件里不再有任何 # 开头的指令,全部被展开或删除了。

# 只做预处理,不编译
gcc -E main.c -o main.i

# 看看预处理后的文件有多大
wc -l main.i
# 一个简单的hello world,预处理后可能有上千行

预处理器认识所有以 # 开头的指令,主要有四类:文件包含(#include),宏定义(#define),条件编译(#ifdef/#ifndef/#endif),行控制(#line,很少用)。日常打交道最多的是前三类。

#include:不是"引入库",是"复制粘贴"

#include <stdio.h> 的功能是:找到 stdio.h 这个文件,把它里面的全部内容,原封不动地复制到你写 #include 的那个位置。

注意,是复制文件内容,不是"链接库"。库的链接是第四步(链接器)干的活,跟预处理无关。预处理只负责把头文件的声明搬到你的代码里,让你后面编译的时候"知道printf长什么样"。

两种写法的区别:

#include <stdio.h>    // 尖括号:在系统目录里找(/usr/include/)
#include "myheader.h" // 双引号:先在当前目录找,找不到再去系统目录

main.c 中写了
#include

预处理器找到 stdio.h
读取全部内容

把 stdio.h 内容
粘贴到 #include 位置

输出 main.i
stdio.h声明 + 你的代码

一个常见的坑:头文件循环包含。a.h包含了b.h,b.h又包含了a.h。预处理器会无限循环展开吗?不会,但会报错。解决方法是用条件编译防止重复包含,下面会讲。

#define:文本替换的威力与陷阱

#define 告诉预处理器:在后面的代码里,凡是遇到这个名字,就把它替换成你指定的内容。

#define MAX_SIZE 100

int buffer[MAX_SIZE];  // 预处理后变成 int buffer[100];

注意,这是纯文本替换,不是"定义常量"。预处理器不理解类型,不理解变量范围,不理解任何语义。它只看到MAX_SIZE这个字符串,就换成100。

带参数的宏更像"文本模板":

#define SQUARE(x) ((x) * (x))

int a = SQUARE(3);     // 变成 int a = ((3) * (3));
int b = SQUARE(1+2);   // 变成 int b = ((1+2) * (1+2));  = 9

括号必须加。如果不加括号:

#define SQUARE_BAD(x) x * x

int b = SQUARE_BAD(1+2);  // 变成 int b = 1+2 * 1+2;  = 5(不是9!)

因为乘法优先级高于加法,1+2*1+2 先算 2*1=2,再算 1+2+2=5。这就是宏的典型陷阱:你以为传的是"3",实际上传的是"1+2"这三个字符。

宏的另一个坑是变量被多次求值:

#define MAX(a, b) ((a) > (b) ? (a) : (b))

int x = 5, y = 3;
int z = MAX(x+1, y+1);
// 展开后:int z = ((x+1) > (y+1) ? (x+1) : (y+1));
// x+1被求值了两次!如果x+1有副作用(比如是一个函数调用),就会执行两次

修仙类比:宏是"符箓模板",你把材料塞进去,它按模板加工。但符箓不理解材料是什么,你塞"1+2"进去,它不会帮你先算成3,而是把"1+2"原样填进模板的每个位置。

条件编译:一套代码适配多个平台

嵌入式开发里,同一套代码可能要跑在不同的芯片上。STM32F1和STM32F4的寄存器地址不同,你不能写两份代码,但可以用条件编译切换:

#ifdef STM32F1
    #include "stm32f1xx.h"
    #define CLOCK_FREQ 72000000  // F1系列最高72MHz
#elif defined(STM32F4)
    #include "stm32f4xx.h"
    #define CLOCK_FREQ 168000000 // F4系列最高168MHz
#else
    #error "未定义目标芯片"
#endif

预处理器会根据编译时传入的宏定义选择只编译其中一个分支,其余分支的代码直接丢弃。上面这段代码里的 STM32F1STM32F4 不是 GCC 自带的预定义宏,它们由芯片厂商的头文件(STM32CubeMX 生成的 stm32xxxx.h)或者编译命令行参数 -DSTM32F1 传入。GCC 自己预定义的跟 ARM 架构相关的宏叫 __ARM_ARCH_7M__(Cortex-M3)和 __ARM_ARCH_7EM__(Cortex-M4),但日常开发中厂商宏用得更多,因为更直观。

头文件防重复包含也是靠条件编译:

// myheader.h
#ifndef MYHEADER_H
#define MYHEADER_H

// 头文件内容

#endif

第一次包含这个头文件时,MYHEADER_H还没定义,所以进入ifndef块,定义MYHEADER_H,复制内容。第二次再被包含时,MYHEADER_H已经定义了,ifndef条件为假,整个块被跳过。这样就不会重复包含。

还有一种更简洁的写法:

// myheader.h
#pragma once

大多数现代编译器都支持。效果跟上面的ifndef一样,但更简洁,不用起宏名。缺点是非标准语法,不过实际项目中几乎都支持。

elif路径

已定义

未定义

满足

不满足

预处理器遇到#ifdef

宏是否已定义

编译此分支代码

是否有#elif

elif条件满足?

是否有#else

编译else分支代码

跳过整个块

预处理器的能力边界

预处理器能做的不多。它不理解C语言的语法,不知道什么是类型,什么是函数,什么是变量。它只是一个"文本搜索替换工具"。

它不能做的事:

  • 算术运算(#define ONE 1#define TWO 2,预处理器不会帮你算 ONE + TWO
  • 类型检查(宏没有类型概念)
  • 范围控制(宏定义后全局生效,除非用#undef取消)
  • 访问变量值(宏在预处理阶段展开,那时变量还不存在)

它能做的事:

  • 文本替换(#define)
  • 文件包含(#include)
  • 条件选择(#ifdef)
  • 字符串化(#运算符,把宏参数变成字符串)
  • 拼接标记(##运算符,把两个标记拼成一个)
#define STRINGIFY(x) #x
#define CONCAT(a, b) a##b

const char *s = STRINGIFY(Hello);  // 变成 "Hello"
int CONCAT(var, 1) = 10;           // 变成 int var1 = 10;

一个全的预处理实验

写一个简单的C文件,亲手看看预处理做了什么:

// test.c
#include <stdio.h>

#define VERSION 2
#define PRINT_VAL(x) printf(#x " = %d\n", x)

int main(void)
{
    int a = 10;
    PRINT_VAL(a);
    return 0;
}

预处理后的结果(简化):

// stdio.h展开的上千行声明...
// ...
extern int printf (const char *__restrict, ...);

int main(void)
{
    int a = 10;
    printf("a" " = %d\n", a);
    return 0;
}

你看,PRINT_VAL(a) 变成了 printf("a" " = %d\n", a)。#运算符把参数a变成了字符串"a",然后跟后面的字符串拼接。C语言里相邻的字符串字面量会自动拼接,所以 "a" " = %d\n" 等于 "a = %d\n"


修仙术语对照表

修仙术语技术现实本篇位置
功法长老预处理器引入
文本替换#define宏展开#define节
复制功法#include头文件包含#include节
符箓模板带参数的宏#define节
分支选择#ifdef条件编译条件编译节
防重阵法#ifndef防重复包含条件编译节
符文转化#运算符字符串化能力边界节
符文拼接##运算符标记拼接能力边界节
功法边界预处理器能力限制能力边界节
第一道关卡预处理是编译第一步引入

进阶条件

会写#include和理解预处理器的边界之间,差这几条:

  • 能用 gcc -E 查看预处理结果,而不是只猜它做了什么
  • 知道#include是复制文件内容,不是"引入库"
  • 能解释为什么宏 SQUARE(x) 必须加括号
  • 能用#ifndef或#pragma once防止头文件重复包含
  • 能用#ifdef切换不同平台的代码
  • 知道预处理器不理解类型和语法,只做文本替换

最后一条是金丹期的认知分水岭。理解了"预处理器只是文本工具",你就能解释那些奇怪的编译错误到底从哪来。


下期预告 + 互动

下一篇:【金丹·63】编译:C代码怎么变成汇编

预处理后的.i文件还是纯文本,CPU看不懂。
编译器要把它翻译成汇编语言,这一步会建语法树,做类型检查,还会偷偷改你的代码。
下一篇拆开编译器,看看它怎么把C代码变成汇编。

现在问你:

🔍 你踩过宏的坑吗?比如宏不加括号导致计算结果不对?

📌 你项目里用#pragma once还是#ifndef防重复包含?

评论区聊聊你遇到过的预处理坑。

我是玄芯散人,带你从炼气修到大乘。


本文是「码农修仙传」系列第62篇。系列导航见 xren.ren

下载代码方式:https://pan.quark.cn/s/2f5b5de24682 课程设计开题报告文档总共包含21页,共计8044字,其源代码构成整个工程文件(使用VS2019环境)。 <实验课题>部分详细记录了每位学生的具体资料,包括学号、姓名、性别、家庭住址、联系电话,以及语文、数学、外语三门的单科成绩、考试平均成绩、考试名次、同学互评成绩、品德评价、任课教师评分综合测评总分名次。 <功能要求>部分具体阐述了如下功能: 1、学生信息管理: (1) 学生信息的录入:需要输入学号、姓名、性别、家庭住址、联系电话,并按照学号由小到大的顺序将信息存储至文件中。 建议:学生信息可先暂存于数组中,完成排序后再写入文件。 (2) 学生信息的修改与删除:允许修改除学号以外的其他信息,删除时需输入学生学号,系统将读取该学生的信息,并要求用户确认以决定是否执行删除操作。 2、学生数据管理: (1) 学生成绩的录入:按照考试科目录入学生成绩,并根据公式:考试成绩=(语文成绩+数学成绩+外语成绩)/3,计算得出学生记录后写入一个文件中。 (2) 学生综合测评数据的录入与计算:输入学生测评数据,计算综合测评总分及名次。 提示:综合测评总分=考试成绩*0.6+同学互评成绩*0.1+品德成绩*0.1+任课教师评分*0.2。 3、菜单系统的设计:实现功能选项的选择; 4、数据同步与文件读取:学生的信息录入、修改、删除操作均可实时同步至文件中,系统亦可通过读取已记录的文件来获取数据。
代码转载自:https://pan.quark.cn/s/b92216efb941 在Windows 11的操作系统环境中,Microsoft Terminal Services Client (MSTSC) 被视作执行远程桌面连接的核心工具,其功能在于使得用户能够访问并操控远端的计算机设备。文档所提及的更新是专针对Win11版本的MSTSC,其具体版本标识为10.0.22621,这表明其属于一个较新阶的补丁或升级,其中或许囊括了效能的增强、安全性的修补以及其他功能的优化。在描述中列出的17个文件,或包含有MSTSC组件的整体或部分更新资料,这些文件能够直接用以替换现有的系统文件,从而达成升级的目标。 1. **远程桌面协议 (RDP)**: RDP是由Microsoft设计的一种协议,其目的是让用户可以通过网络对远程的计算机实施图形化的操作。RDP 10.11版本提供了更迅捷的连接速度、更优越的用户体验以及更为坚实的安保保障。这一版本或许集成了图像编码的优化,旨在提升对延迟敏感型应用的效能表现,以及对高分辨率显示设备的支持。 2. **MSTSC更新**: 对MSTSC进行更新意在修正已知的技术缺陷,强化功能表现,并提升安全性。例如,可能对多显示器环境的配置进行了改善,优化了网络带宽的利用效率,加强了身份验证的机制,或引入了新的配置选项。 3. **文件替换**: 用户在实施文件替换时需持谨慎态度,务必备份原有的文件以防止意外情况发生。通常,这些文件存放在系统目录,例如`C:\Windows\System32`。在替换之前,应关闭所有相关的系统服务,以避免因文件正在被使用而导致替换操作无法进行。 4. **安全性与稳定性**: 新版...
代码下载链接: https://pan.quark.cn/s/43da52c0acfb 在信息技术领域中,串行接口数据交换是一种广泛应用且构成基础的设备间信息交互途径,尤其在嵌入式技术及工业自动化控制方面具有显著地位。当前项目的研究核心为“串行接口图像传输”,具体涉及运用串行接口摄像头(例如OV7670型号)进行图像采集,并将采集到的图像以.bmp文件格式借助串行连接途径传递至上位计算机系统进行可视化呈现。接下来,我们将对相关技术要点进行详尽剖析。 1. **OV7670摄像头单元**:OV7670作为一款常见的CMOS图像感应元件,适用于低能耗、紧凑型嵌入式系统设计。该元件能够输出VGA(640x480)像素级别的图像质量,并且支持多种图像编码方式,包括YUV、RGB以及JPEG等类型。在本次项目实践中,OV7670被配置用于获取.bmp规格的图像资料。 2. **串行数据交换机制**:串行通信技术,亦称作UART(通用异步收发传输器)交换模式,是一种点对点的数据交换方案,多见于设备间短距离的通信场景。串行接口通常涵盖RS-232、RS-485以及USB转串行等不同标准接口类型。在此案例中,OV7670通过串行接口路径与上位计算机系统进行图像数据的交互。 3. **.bmp图像文件格式**:.bmp格式为Windows操作系统环境下的一种位图图像文件编码方式,该格式直接存储像素色彩信息的原始数据,未实施任何压缩处理,因此能够保持较高的图像保真度但会导致文件体积相对较大。OV7670采集到的图像资料被编码为.bmp格式,以便于上位计算机系统能够直接识别并进行显示操作。 4. **上位计算机应用程序**:上位计算机通常定义为负责控制或监测下位设备(...
内容概要:本文合辑收录27篇工业自动化领域中AI技术真实落地的应用实践记录,聚焦于工控上位机开发场景,涵盖C#/.NET平台下的PLC通信、视觉标定、设备调试、日志分析、安全红线、架构设计、远程运维等多个核心技术方向。作者结合真实产线项目经验,系统性展示了如何利用AI编程工具(如Cursor、Claude Code、GitHub Copilot等)提升开发效率,包括生成通信协议代码、重构老旧WinForms工程、构建调试工具、自动化日志根因分析等,同时深刻剖析了AI在工控领域的应用边界与安全禁区,强调AI作为“实习生”应辅助而非替代工程师进行关键决策。; 适合人群:具备一定工业自动化或上位机开发经验的研发人员,尤其是从事非标自动化、机器视觉、PLC集成等方向,希望借助AI提升工作效率的工程师技术管理者。; 使用场景及目标:①学习如何在C#/.NET老项目中高效应用AI工具,解决读旧代码、写样板逻辑、调试排错等实际问题;②掌握AI辅助下的工控软件架构设计、单元测试、版本管理与安全规范;③明确AI在工控领域的“能”与“不能”,规避安全风险,建立人机协同的高效工作流。; 阅读建议:此资源以真实项目复盘为核心,内容极具实战性场景针对性。建议读者结合自身项目痛点,选择相应章节深入研读,并尝试将文中的提示词模板、Rules配置、分层架构安全红线迁移到实际工作中,边实践边反思,真正实现“让AI干活,让人负责”。
内容概要:本文提出了一种融合多尺度时序卷积网络(MS-TCN)与TiDE稠密编码器的深度学习模型,用于实现长周期电力负荷的直接多步预测。该模型通过MS-TCN模块在多个时间尺度上捕捉负荷序列中的局部模式、周期性与趋势等复杂时序特征,同时利用TiDE模型的编码-解码结构对历史信息进行高效压缩与未来序列的端到端生成,增强了对周级乃至更长周期负荷变化的建模能力。研究详细阐述了模型的整体架构设计、损失函数选择、训练策略优化以及在真实电力负荷数据集上的实验验证过程。结果表明,该混合模型在MAE、RMSE等多个评价指标上显著优于传统ARIMA、LSTM、Seq2Seq等基准模型,尤其在应对负荷波动剧烈与季节性强的场景下表现出更强的鲁棒性与预测精度。; 适合人群:具备一定深度学习与时间序列分析基础,从事电力系统负荷预测、能源管理、智能电网等相关领域的科研人员、工程师及高校研究生。; 使用场景及目标:①应用于电网调度、能源交易、需求响应等需要提前数天至数周进行负荷预估的业务场景;②为研究者提供一种高性能的直接多步预测范式,探索如何结合CNN的局部特征提取能力与现代序列模型的全局建模优势,提升长周期预测的准确性与实用性; 阅读建议:此资源不仅提供了完整的Python代码实现,还涵盖了从问题建模、特征工程到模型训练与评估的全流程技术细节,建议读者在学习过程中结合代码实践,深入理解各模块的设计动机,并尝试在自有数据集上复现与调优,以掌握其在实际项目中的应用方法。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值