【C语言程序设计】第39篇:预处理器与宏定义

1 引言

C语言程序从源代码到可执行文件经历了四个阶段:预处理、编译、汇编、链接。预处理器是第一个阶段,它处理以#开头的指令,对源代码进行文本替换。

c

复制代码
#include <stdio.h>    /* 文件包含 */
#define PI 3.14159     /* 宏定义 */
#define SQUARE(x) ((x)*(x))  /* 带参数的宏 */

int main(void)
{
    printf("PI = %f\n", PI);
    printf("5的平方 = %d\n", SQUARE(5));
    return 0;
}

预处理后的代码:

c

复制代码
/* 展开 #include <stdio.h> 的内容(约800行) */
/* 展开 PI → 3.14159 */
/* 展开 SQUARE(5) → ((5)*(5)) */

int main(void)
{
    printf("PI = %f\n", 3.14159);
    printf("5的平方 = %d\n", ((5)*(5)));
    return 0;
}

本章我们将深入探讨预处理器的各种用法。


2 #define定义符号常量

2.1 基本用法

#define用于定义符号常量,在预处理阶段将符号替换为指定的文本。

c

复制代码
#define MAX_SIZE 100
#define PI 3.14159
#define FILE_NAME "data.txt"

int arr[MAX_SIZE];  /* 预处理后 → int arr[100]; */
double area = PI * r * r;  /* → 3.14159 * r * r */

2.2 命名规范

  • 通常使用全大写字母,单词间用下划线分隔

  • 这样可以与变量名区分,提高可读性

c

复制代码
#define MAX_BUFFER 1024
#define DEFAULT_PORT 8080
#define ERROR_FILE_NOT_FOUND -1

2.3 使用const还是#define?

特性 #define const
类型检查 无(纯文本替换)
内存占用 无(替换后消失) 有(占用内存)
调试可见 调试器看不到宏名 调试器可见变量名
作用域 从定义到文件结束 遵循C语言作用域规则

建议

  • 简单的数值常量可以用#define

  • 需要类型检查或调试的场景用const

  • C++推荐用constconstexpr

2.4 注意事项

c

复制代码
/* 错误:不要加分号 */
#define MAX 100;   /* 展开后 arr[100;]; 语法错误! */

/* 正确 */
#define MAX 100

/* 错误:宏定义中不要使用等号 */
#define PI = 3.14159  /* 展开后 area = = 3.14159 * r * r; 语法错误! */

3 宏函数(带参数的宏)

3.1 基本语法

宏函数可以带参数,参数在替换时被展开。

c

复制代码
#define SQUARE(x) ((x)*(x))
#define MAX(a,b) ((a)>(b)?(a):(b))
#define ABS(x) ((x)<0?-(x):(x))

int main(void)
{
    int a = SQUARE(5);      /* → ((5)*(5)) = 25 */
    int b = MAX(10, 20);    /* → ((10)>(20)?(10):(20)) = 20 */
    int c = ABS(-5);        /* → ((-5)<0?-(-5):(-5)) = 5 */
    return 0;
}

3.2 为什么要加这么多括号

宏是文本替换,不加括号可能导致运算优先级问题。

c

复制代码
/* 错误:缺少括号 */
#define SQUARE(x) x*x
int r = SQUARE(2+3);  /* 展开为 2+3*2+3 = 2+6+3 = 11,不是25! */

/* 正确:加上括号 */
#define SQUARE(x) ((x)*(x))
int r = SQUARE(2+3);  /* 展开为 ((2+3)*(2+3)) = 25 */

c

复制代码
/* 错误:缺少外层括号 */
#define DOUBLE(x) (x)+(x)
int r = 2 * DOUBLE(5);  /* 展开为 2 * (5)+(5) = 10+5 = 15,不是20! */

/* 正确 */
#define DOUBLE(x) ((x)+(x))
int r = 2 * DOUBLE(5);  /* 展开为 2 * ((5)+(5)) = 20 */

规则:宏定义中的每个参数都要用括号括起来,整个表达式也要用括号括起来。

3.3 宏函数 vs 函数

特性 宏函数 普通函数
执行效率 无调用开销(直接展开) 有调用开销(压栈、跳转)
代码体积 每次调用都展开,体积增大 代码只出现一次
类型检查 无(任何类型都可传入) 有(严格类型检查)
副作用 参数可能被多次求值 参数只求值一次
调试 难以调试 可设断点

c

复制代码
/* 宏函数的副作用问题 */
#define SQUARE(x) ((x)*(x))
int a = 5;
int r = SQUARE(a++);  /* 展开为 ((a++)*(a++)),a被自增两次! */
printf("r=%d, a=%d\n", r, a);  /* 结果不确定(未定义行为) */

3.4 多语句宏

如果宏函数包含多条语句,需要用do { ... } while(0)包裹。

c

复制代码
/* 错误:多条语句在if中展开会有问题 */
#define SWAP(a,b) { int t=a; a=b; b=t; }

if (x > y)
    SWAP(x, y);
else
    x = 0;
/* 展开后:
if (x > y)
    { int t=x; x=y; y=t; };
else
    x = 0;
/* 分号导致else孤立,编译错误! */

c

复制代码
/* 正确:用 do-while(0) 包裹 */
#define SWAP(a,b) do { int t=a; a=b; b=t; } while(0)

/* 展开后:
if (x > y)
    do { int t=x; x=y; y=t; } while(0);
else
    x = 0;
/* 语法正确,且不影响流程 */

4 条件编译

4.1 #ifdef / #ifndef / #endif

条件编译用于根据条件决定哪些代码被编译。

c

复制代码
#define DEBUG 1

#ifdef DEBUG          /* 如果 DEBUG 被定义 */
    printf("调试信息:x=%d\n", x);
#endif

#ifndef NDEBUG        /* 如果 NDEBUG 未被定义 */
    printf("调试信息\n");
#endif

4.2 #if / #elif / #else

可以进行更复杂的条件判断。

c

复制代码
#define VERSION 2

#if VERSION == 1
    printf("版本1的功能\n");
#elif VERSION == 2
    printf("版本2的新功能\n");
#else
    printf("未知版本\n");
#endif

4.3 防止头文件重复包含

这是条件编译最常见的应用------头文件守卫。

c

复制代码
/* myheader.h */
#ifndef MYHEADER_H
#define MYHEADER_H

/* 头文件内容 */

#endif

4.4 调试代码

c

复制代码
#include <stdio.h>

#define DEBUG 1

int main(void)
{
    int x = 10;
    
    #ifdef DEBUG
        printf("调试模式:x = %d\n", x);
    #endif
    
    #if DEBUG > 1
        printf("详细调试信息\n");
    #endif
    
    return 0;
}

4.5 跨平台代码

c

复制代码
#if defined(_WIN32) || defined(_WIN64)
    #define PLATFORM_WINDOWS
    #include <windows.h>
#elif defined(__linux__)
    #define PLATFORM_LINUX
    #include <unistd.h>
#elif defined(__APPLE__)
    #define PLATFORM_MACOS
    #include <mach/clock.h>
#else
    #error "Unsupported platform"
#endif

void sleep_ms(int ms)
{
    #ifdef PLATFORM_WINDOWS
        Sleep(ms);
    #else
        usleep(ms * 1000);
    #endif
}

5 #和##运算符

5.1 #运算符:字符串化

# 将宏参数转换为字符串字面量。

c

复制代码
#define STR(x) #x
#define PRINT_INT(x) printf(#x " = %d\n", x)

int main(void)
{
    printf("%s\n", STR(Hello));      /* 输出 "Hello" */
    printf("%s\n", STR(123));        /* 输出 "123" */
    
    int a = 10;
    PRINT_INT(a);                    /* 输出 "a = 10" */
    PRINT_INT(a + 5);                /* 输出 "a + 5 = 15" */
    
    return 0;
}

5.2 ##运算符:连接

## 将两个记号连接成一个新记号。

c

复制代码
#define CONCAT(a,b) a##b
#define MAKE_VAR(name, num) name##num

int main(void)
{
    int var1 = 10;
    int var2 = 20;
    
复制代码
printf("%d\n", CONCAT(var, 1));   /* 输出 10 */
    printf("%d\n", MAKE_VAR(var, 2)); /* 输出 20 */
    
    return 0;
}

5.3 综合应用:泛型宏

c

复制代码
#include <stdio.h>

#define PRINT_INT(x) printf(#x " = %d\n", x)
#define PRINT_FLOAT(x) printf(#x " = %f\n", x)
#define PRINT_STRING(x) printf(#x " = %s\n", x)

/* 根据类型选择不同的打印宏 */
#define PRINT(x, type) \
    (type == 1 ? PRINT_INT(x) : \
     type == 2 ? PRINT_FLOAT(x) : \
     PRINT_STRING(x))

/* 更好的方法:利用C11的_Generic(这里不展开) */
int main(void)
{
    int a = 10;
    float b = 3.14;
    char *c = "Hello";
    
    PRINT(a, 1);
    PRINT(b, 2);
    PRINT(c, 3);
    
    return 0;
}

5.4 应用:自动生成枚举和字符串映射

c

复制代码
#include <stdio.h>

/* 定义宏来自动生成枚举和字符串数组 */
#define COLORS \
    X(RED)   \
    X(GREEN) \
    X(BLUE)  \
    X(YELLOW)

/* 生成枚举 */
typedef enum {
    #define X(name) name,
    COLORS
    #undef X
} Color;

/* 生成字符串数组 */
const char* color_names[] = {
    #define X(name) #name,
    COLORS
    #undef X
};

int main(void)
{
    for (int i = 0; i < 4; i++) {
        printf("Color %d: %s\n", i, color_names[i]);
    }
    return 0;
}

6 预定义宏

C语言预定义了一些有用的宏:

含义
__LINE__ 当前行号
__FILE__ 当前文件名
__DATE__ 编译日期("MMM DD YYYY")
__TIME__ 编译时间("HH:MM:SS")
__func__ 当前函数名(C99)
__STDC__ 是否遵循ANSI C标准

c

复制代码
#include <stdio.h>

void debug(const char *msg)
{
    printf("[%s:%d] %s\n", __FILE__, __LINE__, msg);
}

int main(void)
{
    printf("编译日期:%s\n", __DATE__);
    printf("编译时间:%s\n", __TIME__);
    debug("程序开始");
    return 0;
}

7 常见错误与注意事项

7.1 宏定义后加分号

c

复制代码
#define MAX 100;  /* 错误! */
int arr[MAX];     /* 展开为 int arr[100;]; 语法错误 */

7.2 宏函数参数缺少括号

c

复制代码
#define SQUARE(x) x*x  /* 错误!缺少括号 */
int r = SQUARE(2+3);   /* 展开为 2+3*2+3 = 11 */

7.3 宏函数重复求值副作用

c

复制代码
#define MAX(a,b) ((a)>(b)?(a):(b))
int x = 5, y = 10;
int m = MAX(x++, y++);  /* x++ 和 y++ 各被求值两次,不确定行为 */

7.4 宏名与变量名冲突

c

复制代码
#define MAX 100
int MAX = 200;  /* 错误!MAX已被定义为宏,展开后 int 100 = 200; */

7.5 忘记#undef

c

复制代码
#define SIZE 100
/* ... 使用SIZE ... */
/* 之后想用SIZE作为变量名,但宏还存在 */
#undef SIZE  /* 取消宏定义 */
int SIZE = 50;  /* 现在合法 */

7.6 条件编译的#else忘记处理

c

复制代码
#ifdef DEBUG
    printf("调试信息\n");
/* 忘记 #else 或 #endif,后续代码被意外包含 */
#endif

8 本章小结

本章系统介绍了预处理器的各种功能:

1. #define定义符号常量

  • 语法:#define 名称 替换文本

  • 用于定义常量、数组大小、错误码等

  • 避免使用分号,使用全大写命名

2. 宏函数

  • 语法:#define 宏名(参数) 替换文本

  • 每个参数都要加括号,整个表达式也要加括号

  • 多语句宏用 do { ... } while(0) 包裹

  • 注意副作用问题

3. 条件编译

  • #ifdef / #ifndef / #endif:检查宏是否定义

  • #if / #elif / #else:条件判断

  • 用途:头文件守卫、调试代码、跨平台开发

4. #和##运算符

  • #参数:将参数转换为字符串

  • 参数1##参数2:连接两个记号

  • 用于生成代码、泛型宏、自动生成枚举/字符串映射

5. 预定义宏

  • __LINE____FILE____DATE____TIME____func__

6. 注意事项

  • 宏定义后不要加分号

  • 宏参数必须加括号

  • 注意宏的副作用

  • 避免宏名冲突

  • 使用#undef取消宏定义

相关推荐
巧妹儿2 小时前
Python 配置管理封神技:pydantic_settings+@lru_cache,支持优先级,安全又高效,杜绝重复加载!
开发语言·python·ai·配置管理
独隅2 小时前
Python AI 全面使用指南:从数据基石到智能决策
开发语言·人工智能·python
m0_569881472 小时前
C++中的装饰器模式变体
开发语言·c++·算法
笒鬼鬼2 小时前
【API接口】最新可用红果短剧接口
算法·api·笒鬼鬼·红果短剧·接口源码
weixin_421922692 小时前
C++与边缘计算
开发语言·c++·算法
2401_831920742 小时前
C++编译期数组操作
开发语言·c++·算法
殷紫川2 小时前
秒杀系统高并发核心优化与落地全指南
算法·架构
野犬寒鸦2 小时前
JVM垃圾回收机制面试常问问题及详解
java·服务器·开发语言·jvm·后端·算法·面试
风酥糖2 小时前
Godot游戏练习01-第16节-游戏中的状态机
算法·游戏·godot