第二十一天 C语言 Day21:预处理(宏定义/文件包含/条件编译/其他指令)· 库文件(静态库与动态库 · dlopen 手动加载)
系列 :粤嵌嵌入式培训学习笔记 (C语言篇-第21天)
上一篇 :第二十天 C语言 Day20:结构体字节对齐 · 柔性数组 · 共用体union · 枚举enum · typedef · 斗地主发牌综合案例
环境:Ubuntu 22.04 · GCC · Vim | 2026年8月31日
① 概览
从"写代码"回到"编译的前一步"。预处理 是编译四步骤的第一环------在真正编译前,预处理器先干四件事:宏替换、文件包含、条件编译、删注释 。今天把 12 个
#指令过一遍:#define的宏替换陷阱(参数必须加括号、++多次求值、不加分号);#include的尖括号 vs 双引号,以及头文件重复包含的两种解决方案(#ifndef卫哨 /#pragma once);条件编译 做 debug 开关、跨平台适配、版本裁剪、编译时断言;再顺手过#undef/#pragma/#error/#line和几个预定义宏。最后进入库文件 ------把代码编译成.a(静态库)/.so(动态库)供他人调用,并用dlopen/dlsym/dlclose在运行时手动加载。一句话总结今天:程序还没跑起来之前,预处理器已经先动过手了。
| 模块 | 一句话 |
|---|---|
| 编译四步骤 ⭐ | 预处理 .i → 编译 .s → 汇编 .o → 链接 可执行 |
| 无参宏 ⭐ | #define PI 3.14,纯文本替换,行末不加分号 |
| 带参宏 ⭐ | #define max(a,b) ((a)>(b)?(a):(b)),参数和宏体都要加括号 |
| 宏 vs 函数 | 宏无类型检查/无调用开销/体积大;函数相反 |
| 文件包含 ⭐ | <> 找系统目录,"" 先找当前目录 |
| 防重复包含 ⭐ | #ifndef + #define + #endif 卫哨,或 #pragma once |
| 条件编译 ⭐ | #ifdef/#ifndef/#if/#elif/#else/#endif,按条件裁剪代码 |
| 其他指令 | #undef 取消宏、#pragma 编译器指令、#error 报错、#line 改行号 |
| 预定义宏 | __LINE__ __FILE__ __DATE__ __TIME__ __STDC__ |
| 库文件 ⭐ | 静态库 libxxx.a 链接期复制;动态库 libxxx.so 运行期加载 |
| dlopen ⭐ | 运行时手动加载 .so:dlopen→dlsym→调用→dlclose |
② 预处理概述
2.1 编译四步骤中的位置
预处理是编译的第一步,预处理器在真正的编译开始前对源码做"准备工作"。
| 步骤 | 命令 | 文件转换 | 说明 |
|---|---|---|---|
| 1. 预处理 | gcc -E *.c -o *.i |
*.c → *.i |
宏替换、头文件包含、删注释 |
| 2. 编译 | gcc -S *.c |
*.c/*.i → *.s |
生成汇编代码 |
| 3. 汇编 | gcc -c *.c |
*.c → *.o |
生成目标文件(机器码) |
| 4. 链接 | gcc *.c -o app |
*.o → 可执行文件 |
合并目标文件和库 |
2.2 预处理器功能
- 宏替换 :
#define宏名替换为宏体 - 文件包含 :
#include头文件内容拷贝进来 - 条件编译:按条件决定哪些代码参与编译
- 删除注释 :去掉
//和/**/ - 处理其他指令 :
#undef、#pragma、#error、#line
C 语言共 12 个预处理指令,全部以 # 开头:
c
#define #endif #elif #error #line #include
#ifdef #ifndef #pragma #undef #if #else
💡 调试小技巧 :想验证宏到底被替换成了什么?
gcc -E demo.c | tail -20直接看预处理结果,比对着代码猜快得多。
③ 宏定义 #define ⭐
3.1 无参宏
c
#define 宏名称 宏值
规则:宏名与宏值用空格隔开;行末不能加分号 ;宏名建议全大写(MAX_VALUE、PI)。
c
#include <stdio.h>
#define PI 3.1415926
#define R 20
int main(int argc, char *argv[])
{
// 预处理后替换为:2 * 3.1415926 * 20
printf("圆的周长:%f\n", 2 * PI * R);
// 预处理后替换为:3.1415926 * 20 * 20
printf("圆的面积:%f\n", PI * R * R);
return 0;
}
圆的周长:125.663704
圆的面积:1256.637040
3.2 带参宏
c
#define 宏名称(参数列表) 宏体
带参宏在预处理阶段是纯文本替换:先替换参数,再展开宏体。
c
#include <stdio.h>
#define square(x) ((x) * (x))
#define max(a, b) ((a) > (b) ? (a) : (b))
#define test(a, b) ((a) * (b))
int main(int argc, char *argv[])
{
int num = 5;
int a = 10, b = 20;
printf("%d 的平方 = %d\n", num, square(num)); // ((num) * (num))
printf("%d 和 %d 的最大值 = %d\n", a, b, max(a,b)); // ((a) > (b) ? (a) : (b))
printf("%d\n", test(3 + 5, 4)); // ((3 + 5) * (4))
return 0;
}
5 的平方 = 25
10 和 20 的最大值 = 20
32
3.3 带参宏 vs 函数
| 对比项 | 带参宏 | 函数 |
|---|---|---|
| 处理阶段 | 预处理(纯文本替换) | 编译/运行(函数调用) |
| 参数类型 | 无类型检查 | 严格类型检查 |
| 执行开销 | 无调用开销(内联展开) | 有栈帧开销 |
| 代码体积 | 每次展开复制一份,大 | 仅一份,小 |
| 调试 | 无法单步进入 | 可断点调试 |
3.4 宏的副作用(三大坑)
坑一:参数不加括号
c
#include <stdio.h>
#define SQUARE_BAD(x) x * x // 错误:参数未加括号
#define SQUARE_GOOD(x) ((x) * (x)) // 正确:参数加括号 + 外层括号
int main(int argc, char *argv[])
{
// bad 展开: 3 + 2 * 3 + 2 = 3 + 6 + 2 = 11(错误!)
printf("SQUARE_BAD(3+2) = %d (期望25,实际11)\n", SQUARE_BAD(3+2));
// good 展开: ((3+2) * (3+2)) = 25(正确)
printf("SQUARE_GOOD(3+2) = %d (正确)\n", SQUARE_GOOD(3+2));
return 0;
}
SQUARE_BAD(3+2) = 11 (期望25,实际11)
SQUARE_GOOD(3+2) = 25 (正确)
口诀 :定义带参宏时,每个参数都加小括号,整个宏体也加小括号。
坑二:自增/自减导致多次求值
c
#include <stdio.h>
#define SQUARE(x) ((x) * (x))
int main(int argc, char *argv[])
{
int i = 3;
// 展开为:((i++) * (i++)) → 3 * 4 = 12,而非 3 * 3 = 9
printf("SQUARE(i++) = %d (i = %d)\n", SQUARE(i++), i);
return 0;
}
SQUARE(i++) = 12 (i = 5)
⚠️ 这是未定义行为(UB) ------同一表达式内多次修改同一变量且无序列点,结果不可预期。坚决避免 在宏参数中使用
++/--。
坑三:宏定义末尾加分号
c
#define PI 3.14; // 错误!展开后变成 printf("%f", 3.14;); → 语法错误
坑四(多行宏) :行末用反斜杠 \ 续行,多语句用 do { ... } while(0) 包裹,防止 if 悬空。
c
#define PRINT_VALUES(a, b) do { \
printf("a = %d\n", (a)); \
printf("b = %d\n", (b)); \
} while(0)
④ 文件包含 #include ⭐
4.1 两种包含方式
| 包含方式 | 写法 | 搜索路径 | 适用场景 |
|---|---|---|---|
| 尖括号 | #include <文件名> |
系统标准头文件目录 | 系统标准头文件 |
| 双引号 | #include "文件名" |
先当前目录,再系统目录 | 用户自定义头文件 |
c
#include <stdio.h> // 系统标准库
#include "dclist.h" // 用户自定义头文件
4.2 重复包含问题与解决方案
头文件被多次间接包含会导致重复定义错误。两种解决方案:
方案一:条件编译卫哨(传统方式,兼容性最好)
c
// dlist.h - 使用条件编译防止重复包含
#ifndef _DLIST_H
#define _DLIST_H
#define MAX_SIZE 100
struct Point { int x, y; };
#endif // _DLIST_H
方案二:#pragma once(现代编译器,写法简单)
c
// dlist.h - 使用 #pragma once 防止重复包含
#pragma once
#define MAX_SIZE 100
struct Point { int x, y; };
💡 工程里推荐用卫哨宏 (
#ifndef),因为它是标准 C,所有编译器都支持;#pragma once是编译器扩展,但 GCC/Clang/MSVC 都认,且能避免宏名冲突。
⑤ 条件编译 ⭐
5.1 指令概览
| 指令 | 说明 |
|---|---|
#ifdef MACRO |
如果定义了 MACRO |
#ifndef MACRO |
如果未定义 MACRO |
#if 表达式 |
如果整型常量表达式为真 |
#else / #elif |
否则 / 否则如果 |
#endif |
结束条件编译块 |
5.2 #ifdef / #ifndef:debug 开关
c
#include <stdio.h>
#define DEBUG // 开启调试模式(注释掉这行则关闭)
int main(int argc, char *argv[])
{
int a = 10, b = 20;
int result = a + b;
#ifdef DEBUG
printf("[DEBUG] a = %d, b = %d\n", a, b);
printf("[DEBUG] result = %d\n", result);
#endif
printf("计算结果:%d\n", result);
return 0;
}
[DEBUG] a = 10, b = 20
[DEBUG] result = 30
计算结果:30
5.3 #if / #elif / #else:版本裁剪
c
#include <stdio.h>
#define VERSION 3 // 1=基础版,2=专业版,3=旗舰版
int main(int argc, char *argv[])
{
#if VERSION == 1
printf("基础版:仅支持基础功能\n");
#elif VERSION == 2
printf("专业版:支持高级功能\n");
#elif VERSION == 3
printf("旗舰版:支持全部功能\n");
#else
printf("未知版本\n");
#endif
return 0;
}
旗舰版:支持全部功能
5.4 场景一:跨平台适配
c
#include <stdio.h>
int main(int argc, char *argv[])
{
#ifdef _WIN32
printf("当前平台:Windows\n");
#elif defined(__linux__)
printf("当前平台:Linux\n");
#elif defined(__APPLE__)
printf("当前平台:macOS\n");
#else
printf("当前平台:未知\n");
#endif
return 0;
}
当前平台:Linux
5.5 场景二:编译时断言
c
#include <stdio.h>
#include <limits.h>
#if __STDC_VERSION__ < 199901L
#error "此程序需要 C99 或更高版本的标准支持!"
#endif
// 注意:不能用 #if sizeof(int) < 4,因为 sizeof 在预处理阶段不可用!
_Static_assert(sizeof(int) >= 4, "此程序要求 int 类型至少占用 4 字节!");
int main(int argc, char *argv[])
{
printf("编译环境满足要求,程序正常运行。\n");
printf("int 类型占用 %d 字节\n", (int)sizeof(int));
return 0;
}
⚠️ 关键区别 :
#if只认宏和整型常量表达式;sizeof是编译期 运算符,预处理阶段不可用(此时类型都还没分析)。要做编译期大小检查,用 C11 的_Static_assert。
⑥ 其他预处理指令
6.1 #undef --- 取消宏定义
c
#include <stdio.h>
#define PI 3.1415926
int main(int argc, char *argv[])
{
printf("PI = %f\n", PI); // 3.141593
#undef PI
// printf("PI = %f\n", PI); // 取消注释会报错:PI 未定义
#define PI 3.14 // 重新定义
printf("PI = %f\n", PI); // 3.140000
return 0;
}
PI = 3.141593
PI = 3.140000
6.2 #pragma --- 编译器特定指令
c
#include <stdio.h>
#pragma message("正在编译 demo10.c ...") // 编译时提示
#pragma pack(1) // 1 字节对齐(紧凑存储)
struct Packet {
char type; // 1 字节
int length; // 4 字节
short checksum; // 2 字节
};
#pragma pack() // 恢复默认对齐
int main(int argc, char *argv[])
{
printf("Packet 结构体大小:%d 字节\n", (int)sizeof(struct Packet));
// 按 1 字节对齐输出 7,默认对齐输出 12
return 0;
}
Packet 结构体大小:7 字节
6.3 #error --- 生成编译错误
c
#ifdef __STDC__
// 标准 C 编译器,正常编译
#else
#error "此程序必须使用支持标准 C 的编译器!"
#endif
6.4 #line --- 修改行号和文件名
c
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("当前行号:%d\n", __LINE__);
#line 100 "custom_file.c"
printf("修改后行号:%d\n", __LINE__); // 101
printf("修改后文件名:%s\n", __FILE__); // custom_file.c
return 0;
}
当前行号:11
修改后行号:101
修改后文件名:custom_file.c
💡 常用预定义宏:
__LINE__(行号)、__FILE__(文件名)、__DATE__/__TIME__(编译日期时间)、__STDC__(是否标准 C)。写日志时printf("[%s:%d] ", __FILE__, __LINE__)是标配。
⑦ 库文件 ⭐
7.1 什么是库文件
库文件是编译后的二进制代码集合,不能独立运行,供其他程序调用,用于复用代码、隐藏实现细节。
| 类型 | Windows | Linux | 核心特性 |
|---|---|---|---|
| 静态库 | .lib |
libxxx.a |
链接时复制进可执行文件 |
| 动态库 | .dll |
libxxx.so |
运行时加载 |
7.2 静态库 vs 动态库
| 特性 | 静态库 | 动态库 |
|---|---|---|
| 链接时机 | 编译阶段 | 运行阶段 |
| 代码复制 | 复制进可执行文件 | 不复制,仅记录符号 |
| 文件体积 | 较大 | 较小 |
| 依赖 | 无(可独立运行) | 有(缺库跑不起来) |
| 更新 | 需重新编译主程序 | 直接替换库文件 |
7.3 静态库的生成与使用
步骤 1:编写源文件 mylib.c
c
#include <stdio.h>
int sum(const int *arr, int len)
{
int total = 0;
for (int i = 0; i < len; i++) total += arr[i]; // *(arr+i)
return total;
}
步骤 2:编译目标文件 + 打包静态库
bash
gcc -c mylib.c -o mylib.o # 只编译不链接
ar -csr libmylib.a mylib.o # r 替换 c 创建 s 生成索引
步骤 3:编写对外头文件 mylib.h 和主程序
c
// mylib.h
#ifndef _MYLIB_H
#define _MYLIB_H
int sum(const int *arr, int len);
#endif //_MYLIB_H
c
// main_a.c
#include <stdio.h>
#include "mylib.h"
int main(int argc, char *argv[])
{
int arr[] = {11, 22, 33, 44, 55};
int len = sizeof(arr) / sizeof(arr[0]);
// 根据 mylib.h 提供的函数声明,访问库中的 sum 函数
int total = sum(arr, len);
printf("数组和:%d\n", total);
return 0;
}
步骤 4:编译链接 + 运行
bash
gcc main_a.c -L. -lmylib -o main_static # -L. 指定库目录,-lmylib 链接 libmylib.a
./main_static
# 数组和:165
7.4 动态库的生成与使用
bash
# 步骤1:生成位置无关目标文件(-fPIC 动态库必须)
gcc -fPIC -c mylib.c -o mylib.o
# 步骤2:生成动态库
gcc -shared -o libmylib.so mylib.o
# 步骤3:编译主程序
gcc main.c -L. -lmylib -o main_dynamic
# 步骤4:运行(需指定库搜索路径)
LD_LIBRARY_PATH=. ./main_dynamic
⚠️ 动态库编译必须加
-fPIC,否则链接报relocation R_X86_64_32 against ... can not be used when making a shared object。⚠️ 运行时报
error while loading shared libraries: libmylib.so: cannot open shared object file,是因为系统找不到库,三种解法:LD_LIBRARY_PATH=.(临时)、把库拷到/usr/lib、或写进/etc/ld.so.conf后ldconfig。
7.5 动态库手动加载(dlopen / dlsym / dlclose)
不链接进程序,而是运行时按需加载,这是插件系统的基础。
c
#include <stdio.h>
#include <dlfcn.h> // 动态库操作头文件
int main(int argc, char *argv[])
{
// 1. 加载动态库(RTLD_LAZY:延迟绑定)
void *handle = dlopen("./libmylib.so", RTLD_LAZY);
if (handle == NULL)
{
fprintf(stderr, "加载动态库失败: %s\n", dlerror());
return -1;
}
// 2. 查找动态库中的 sum 函数符号
int (*sum_func)(const int *, int) =
(int (*)(const int *, int))dlsym(handle, "sum");
if (sum_func == NULL)
{
fprintf(stderr, "查找 sum 函数失败: %s\n", dlerror());
dlclose(handle);
return -1;
}
// 3. 调用动态库中的 sum 函数
int arr[5] = {11, 12, 13, 14, 15};
int len = sizeof(arr) / sizeof(arr[0]);
int total = sum_func(arr, len);
printf("sum=%d\n", total); // 输出:65
// 4. 关闭动态库
dlclose(handle);
return 0;
}
bash
gcc dl_demo.c -ldl -o dl_demo # 注意链接 libdl
./dl_demo
# sum=65
💡 三步口诀:
dlopen拿句柄 →dlsym取函数指针 →dlclose释放 。dlsym返回void*,必须强转成正确的函数指针类型再调用,转错类型会直接段错误。
⑧ 踩坑记录 ⭐
坑1:带参宏参数不加括号,结果算错
- 现象 :
SQUARE(3+2)得 11 而不是 25 - 根因 :宏是纯文本替换,展开成
3 + 2 * 3 + 2,乘法优先级高于加法 - 解决 :每个参数加括号,整个宏体再加一层括号
((x) * (x))
坑2:宏参数里带自增,变量被加了两次
- 现象 :
SQUARE(i++)输出 12,且i变成 5 - 根因 :
i++在宏体里出现了两次,被求值两次(UB) - 解决 :不在宏参数里用
++/--;能写成函数就写函数
坑3:头文件重复包含导致重复定义
- 现象 :
redefinition of 'struct Point'编译报错 - 根因:头文件被多条包含路径间接包含多次
- 解决 :头文件加卫哨
#ifndef _X_H / #define _X_H / #endif,或#pragma once
坑4:#if 里用 sizeof 判断类型大小
- 现象 :
#if sizeof(int) < 4报错或永远为 0 - 根因 :预处理阶段在编译之前,
sizeof依赖类型信息,此时不可用(未定义标识符按 0 处理) - 解决 :改用
_Static_assert(sizeof(int) >= 4, "...")做编译期断言
坑5:#define 行末加了分号
- 现象 :
printf("%f", PI);展开成printf("%f", 3.14;);语法错误 - 根因:宏体是文本替换,分号一起被替换进去了
- 解决 :
#define行末永远不加分号
坑6:动态库忘记 -fPIC
- 现象 :
gcc -shared时报relocation R_X86_64_32 ... can not be used when making a shared object - 根因:目标文件不是位置无关代码,无法在各进程任意地址加载
- 解决 :编译时加
-fPIC:gcc -fPIC -c mylib.c -o mylib.o
坑7:动态库程序运行时报"找不到共享库"
- 现象 :
error while loading shared libraries: libmylib.so - 根因 :
.so不在系统库搜索路径中(与静态库不同,动态库是运行时才找) - 解决 :
LD_LIBRARY_PATH=. ./app临时指定,或拷到/usr/lib后ldconfig
⑨ 小结
今天完成 :预处理概述(编译四步骤 + 预处理器五大功能 + 12 个 # 指令)→ 宏定义(无参宏 / 带参宏 / 与函数对比 / 四大副作用坑)→ 文件包含(<> vs "" 搜索路径差异 + 卫哨宏与 #pragma once)→ 条件编译(#ifdef 做 debug 开关、#if/#elif 做版本裁剪、跨平台适配、编译时断言)→ 其他指令(#undef/#pragma/#error/#line 与预定义宏)→ 库文件(静态库 vs 动态库五维对比、.a 与 .so 的完整生成使用流程、dlopen/dlsym/dlclose 运行时手动加载)。
下一篇 :文件流------fopen/fclose 打开关闭、单字符/行/数据块三种顺序读写、fscanf/fprintf 与 sscanf/sprintf 格式化读写、fseek/ftell/rewind 随机访问。
本文首发于 CSDN,转载请注明出处。