C语言Day21

第二十一天 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 ⭐ 运行时手动加载 .sodlopendlsym→调用→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 预处理器功能

  1. 宏替换#define 宏名替换为宏体
  2. 文件包含#include 头文件内容拷贝进来
  3. 条件编译:按条件决定哪些代码参与编译
  4. 删除注释 :去掉 ///**/
  5. 处理其他指令#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_VALUEPI)。

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.confldconfig

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
  • 根因:目标文件不是位置无关代码,无法在各进程任意地址加载
  • 解决 :编译时加 -fPICgcc -fPIC -c mylib.c -o mylib.o

坑7:动态库程序运行时报"找不到共享库"

  • 现象error while loading shared libraries: libmylib.so
  • 根因.so 不在系统库搜索路径中(与静态库不同,动态库是运行时才找)
  • 解决LD_LIBRARY_PATH=. ./app 临时指定,或拷到 /usr/libldconfig

⑨ 小结

今天完成预处理概述(编译四步骤 + 预处理器五大功能 + 12 个 # 指令)→ 宏定义(无参宏 / 带参宏 / 与函数对比 / 四大副作用坑)→ 文件包含<> vs "" 搜索路径差异 + 卫哨宏与 #pragma once)→ 条件编译#ifdef 做 debug 开关、#if/#elif 做版本裁剪、跨平台适配、编译时断言)→ 其他指令#undef/#pragma/#error/#line 与预定义宏)→ 库文件(静态库 vs 动态库五维对比、.a.so 的完整生成使用流程、dlopen/dlsym/dlclose 运行时手动加载)。

下一篇 :文件流------fopen/fclose 打开关闭、单字符/行/数据块三种顺序读写、fscanf/fprintfsscanf/sprintf 格式化读写、fseek/ftell/rewind 随机访问。


本文首发于 CSDN,转载请注明出处。

相关推荐
kcuwu.1 小时前
第 1 课 · Hello, World 与一个 Go 程序的诞生
开发语言·后端·golang
php@king1 小时前
golang入门到精通
开发语言·后端·golang
Leo.yuan1 小时前
Flink + Kafka + Doris 之外的另一条路:FineDataLink 一体化实时数仓方案
开发语言·javascript·ecmascript
只睡四小时1 小时前
端侧推理实战:face-api.js 零后端人脸识别踩坑记
开发语言·javascript·ecmascript
某不知名網友1 小时前
C++ 深浅拷贝:从默认拷贝到 Rule of Five
java·开发语言
君顾11 小时前
外卖CPS小程序开发实战指南:从零到上线的完整流程
java·开发语言·外卖
blue_ice .1 小时前
DDS原理及简易实现
开发语言·经验分享·笔记·嵌入式硬件·fpga开发
励志不掉头发的内向程序员1 小时前
【LibreCAD 2D架构】鼠标点下的坐标为什么会被“吸”走?LibreCAD 对象捕捉系统解析
开发语言·c++·qt·学习·系统架构
Generalzy1 小时前
像 gofmt 一样格式化 Python:Black、Ruff、YAPF、autopep8 谁才是 2026 年的首选?
开发语言·python