C 语言预处理(下):宏与函数对比 +#/## 运算符 + 条件编译 + 头文件包含

🏠个人主页:黎雁

🎬作者简介:C/C++/JAVA后端开发学习者

❄️个人专栏:C语言数据结构(C语言)EasyX游戏规划

✨ 从来绝巘须孤往,万里同尘即玉京

文章目录

  • [C 语言预处理(下):宏与函数对比 +#/## 运算符 + 条件编译 + 头文件包含 ✨](#/## 运算符 + 条件编译 + 头文件包含 ✨)
    • [前景回顾:预处理基础速记 📝](#前景回顾:预处理基础速记 📝)
    • [一、宏与函数的终极对比 ------ 选对工具才高效 🆚](#一、宏与函数的终极对比 —— 选对工具才高效 🆚)
      • [1. 核心差异对比表](#1. 核心差异对比表)
      • [2. 宏的优势场景:简单运算+通用类型](#2. 宏的优势场景:简单运算+通用类型)
      • [3. 函数的优势场景:复杂逻辑+可调试](#3. 函数的优势场景:复杂逻辑+可调试)
      • [4. 命名约定:区分宏和函数](#4. 命名约定:区分宏和函数)
    • [二、`#`和`##`运算符 ------ 宏的高级玩法 🪄](###`运算符 —— 宏的高级玩法 🪄)
    • [三、`#undef` ------ 宏的开关 🎚️](#undef` —— 宏的开关 🎚️)
      • [1. 基本语法](#1. 基本语法)
      • [2. 使用示例](#2. 使用示例)
    • [四、命令行定义 ------ 编译时动态传参 🖥️](#四、命令行定义 —— 编译时动态传参 🖥️)
      • [1. 使用示例:动态指定数组大小](#1. 使用示例:动态指定数组大小)
      • [2. 编译并运行(Linux/macOS)](#2. 编译并运行(Linux/macOS))
    • [五、条件编译 ------ 按需编译代码 📦](#五、条件编译 —— 按需编译代码 📦)
      • [1. 单个分支:`#if + #endif`](#if + #endif`)
      • [2. 多个分支:`#if + #elif + #else + #endif`](#if + #elif + #else + #endif`)
      • 3. 判断宏是否定义:`#ifdef`/`#ifndef`
      • [4. 嵌套条件编译](#4. 嵌套条件编译)
    • [六、头文件包含 ------ 避免重复包含的技巧 📂](#六、头文件包含 —— 避免重复包含的技巧 📂)
      • [1. 头文件包含的两种方式](#1. 头文件包含的两种方式)
      • [2. 重复包含的问题](#2. 重复包含的问题)
      • [3. 解决方法:两种方案任选](#3. 解决方法:两种方案任选)
    • [七、其他预处理指令 ------ 拓展了解 📖](#七、其他预处理指令 —— 拓展了解 📖)
    • [写在最后 📝](#写在最后 📝)

C 语言预处理(下):宏与函数对比 +#/## 运算符 + 条件编译 + 头文件包含 ✨

在上一篇中,我们掌握了预定义符号、#define定义常量和宏的基础用法,这一篇我们继续深挖预处理的进阶知识点:宏与函数的核心对比、###运算符的妙用、条件编译的实战技巧,以及头文件包含的避坑指南,帮你彻底吃透预处理的所有核心考点!

前景回顾:预处理基础速记 📝

C 语言预处理核心(上):预定义符号 + #define 常量与宏全解析

回顾上一篇核心知识点,衔接本篇进阶内容:

  1. 预定义符号是C语言内置的调试工具,__FILE__/__LINE__等可快速定位代码位置;
  2. #define定义常量时结尾不加;,多行用\续行;
  3. 带参数宏必须给参数和整体加括号,避免优先级问题;
  4. 带副作用的表达式(如a++)不适合作为宏参数,易导致变量多次修改。

一、宏与函数的终极对比 ------ 选对工具才高效 🆚

宏和函数都能实现"代码复用",但适用场景和特性差异极大。很多初学者分不清二者的区别,我们从多个维度拆解对比,帮你精准选择。

1. 核心差异对比表

特性 #define定义宏 函数
代码长度 每次调用都会替换代码,除极小宏外,会大幅增加程序长度 代码只写一次,调用时跳转执行,程序长度更精简
执行速度 无函数调用开销(栈帧创建/销毁),速度更快 有参数传递、栈帧创建等额外开销,相对较慢
操作符优先级 易因优先级问题出错(需加括号规避) 只在调用时求值,无优先级问题
带副作用参数 参数多次替换,副作用会放大(如a++多次执行) 参数先求值再传递,副作用仅执行一次
参数类型 不关注类型,适配任意类型(通用) 严格匹配类型,不同类型需重载/重写
调试 预处理阶段完成替换,无法调试 可打断点,逐行调试
递归 不支持递归(预处理无法处理) 支持递归

2. 宏的优势场景:简单运算+通用类型

宏最适合实现简单、高频的运算逻辑,尤其是需要适配多种类型的场景:

c 复制代码
// 通用内存分配宏(适配任意类型)
#define MALLOC(n, type) (type*)malloc(n * sizeof(type))
// 使用示例:无需关心类型,宏自动适配
int* p1 = MALLOC(10, int);
double* p2 = MALLOC(5, double);
char* p3 = MALLOC(20, char);

如果用函数实现,需要为每种类型写一个函数,代码冗余且维护成本高。

3. 函数的优势场景:复杂逻辑+可调试

当逻辑复杂、需要调试,或参数有副作用时,优先用函数:

c 复制代码
// 求两数最大值(函数版,无副作用问题)
int Max(int x, int y) {
    return x > y ? x : y;
}

// 复杂运算(如排序、递归),函数更易维护
int Factorial(int n) {
    if (n <= 1) return 1;
    return n * Factorial(n-1); // 递归仅函数支持
}

4. 命名约定:区分宏和函数

为了让代码更易读,行业通用约定:

  • 宏名:全部大写(如MAXSQUAREMALLOC);
  • 函数名:小写/驼峰命名(如maxfactorialgetMax);
  • 特例:offsetof是宏但小写(历史原因,无需纠结)。

二、###运算符 ------ 宏的高级玩法 🪄

C语言为宏提供了两个特殊运算符:#(字符串化)和##(记号粘合),能大幅提升宏的灵活性,是预处理的进阶考点。

1. #运算符:参数字符串化

#的核心作用是将宏参数直接转换为字符串(双引号包裹),常用于简化打印逻辑:

c 复制代码
#include <stdio.h>
// 简化打印宏(#将参数转为字符串)
#define PRINT(n, format) printf("the value of "#n " is " format "\n", n)

int main() {
    int a = 10;
    float f = 3.14f;
    char c = 'A';
    // 宏替换后自动拼接字符串,无需重复写变量名
    PRINT(a, "%d");  // 等价于:printf("the value of a is %d\n", a);
    PRINT(f, "%f");  // 等价于:printf("the value of f is %f\n", f);
    PRINT(c, "%c");  // 等价于:printf("the value of c is %c\n", c);
    return 0;
}

输出结果:

复制代码
the value of a is 10
the value of f is 3.140000
the value of c is A

2. ##运算符:符号粘合

##的核心作用是将两个符号(变量名、函数名、类型名等)拼接成一个新符号,常用于自动生成代码:

c 复制代码
#include <stdio.h>
// 自动生成对应类型的max函数
#define GENERIC_MAX(type) type type##_max(type x, type y) { return x>y ? x:y; }

// 调用宏生成函数
GENERIC_MAX(int);    // 生成:int int_max(int x, int y) { ... }
GENERIC_MAX(float);  // 生成:float float_max(float x, float y) { ... }
GENERIC_MAX(short);  // 生成:short short_max(short x, short y) { ... }

int main() {
    printf("%d\n", int_max(10, 20));    // 输出20
    printf("%.1f\n", float_max(3.1f, 2.9f)); // 输出3.1
    return 0;
}

💡 注意:##拼接的符号必须是合法的标识符(不能以数字开头、不能是关键字),否则编译报错。

三、#undef ------ 宏的开关 🎚️

#undef用于移除已定义的宏,相当于"关闭"这个宏,移除后该宏名不再有效,可重新定义。

1. 基本语法

c 复制代码
#undef 宏名

2. 使用示例

c 复制代码
#include <stdio.h>
#define MAX 100

int main() {
    printf("MAX = %d\n", MAX); // 输出100
    
    #undef MAX // 移除MAX的定义
    // printf("MAX = %d\n", MAX); // 编译报错:MAX未定义
    
    #define MAX 200 // 重新定义MAX
    printf("MAX = %d\n", MAX); // 输出200
    return 0;
}

适用场景:临时关闭某个宏(如调试宏)、根据场景重新定义宏的值。

四、命令行定义 ------ 编译时动态传参 🖥️

部分编译器(如GCC、Clang)支持在命令行编译时,通过-D参数动态定义宏,无需修改源代码。

1. 使用示例:动态指定数组大小

c 复制代码
// test.c
#include <stdio.h>
int main() {
    int arr[SZ]; // SZ未在代码中定义
    for (int i=0; i<SZ; i++) {
        arr[i] = i+1;
        printf("%d ", arr[i]);
    }
    return 0;
}

2. 编译并运行(Linux/macOS)

bash 复制代码
# 编译时通过-D定义SZ=5
gcc test.c -DSZ=5 -o test
# 运行程序
./test

输出结果:1 2 3 4 5

💡 拓展:Windows下的MinGW编译器也支持-D参数,VS编译器可通过"项目属性→C/C++→预处理器→预处理器定义"添加。

五、条件编译 ------ 按需编译代码 📦

条件编译允许我们根据条件决定哪些代码参与编译,哪些代码被忽略,常用于调试、跨平台开发。

1. 单个分支:#if + #endif

c 复制代码
#define DEBUG 1 // 调试模式:1开启,0关闭

#include <stdio.h>
int main() {
    int a = 10;
#if DEBUG == 1 // 条件为真,代码参与编译
    printf("调试信息:a = %d\n", a); // 调试时打印
#endif
    printf("正常逻辑执行\n"); // 始终编译
    return 0;
}

DEBUG=0时,printf("调试信息:...")会被预处理阶段移除,不参与编译。

2. 多个分支:#if + #elif + #else + #endif

c 复制代码
#define PLATFORM 2 // 1=Windows,2=Linux,3=macOS

#include <stdio.h>
int main() {
#if PLATFORM == 1
    printf("Windows系统\n");
#elif PLATFORM == 2
    printf("Linux系统\n");
#else
    printf("macOS系统\n");
#endif
    return 0;
}

输出结果:Linux系统

3. 判断宏是否定义:#ifdef/#ifndef

c 复制代码
#include <stdio.h>
// #define TEST // 注释/取消注释,控制编译逻辑

#ifdef TEST // 等价于 #if defined(TEST)
    printf("TEST宏已定义\n");
#else
    printf("TEST宏未定义\n");
#endif

#ifndef DEBUG // 等价于 #if !defined(DEBUG)
    #define DEBUG 0 // 未定义则默认关闭调试
#endif

4. 嵌套条件编译

条件编译支持嵌套(类似if语句嵌套),大型项目中常见:

c 复制代码
#define OS 1
#define BIT 64

#if OS == 1
    #if BIT == 32
        printf("Windows 32位\n");
    #elif BIT == 64
        printf("Windows 64位\n");
    #endif
#else
    printf("其他系统\n");
#endif

六、头文件包含 ------ 避免重复包含的技巧 📂

#include是最常用的预处理指令,但多次包含同一个头文件会导致编译冗余(如重复定义、重复声明),我们需要掌握避坑方法。

1. 头文件包含的两种方式

方式 适用场景 查找策略
#include <xxx.h> 标准库头文件(如stdio.hstdlib.h 直接在标准库路径查找
#include "xxx.h" 自定义头文件(如test.hcommon.h 先找源文件所在目录 → 再找标准库路径

💡 建议:严格遵循"标准库用<>,自定义用""",提升代码可读性。

2. 重复包含的问题

比如:a.h包含test.hb.h也包含test.hmain.c包含a.hb.h,则test.h会被包含两次,导致重复定义报错。

3. 解决方法:两种方案任选

方案1:条件编译(跨编译器兼容)

在头文件开头和结尾添加条件编译指令,确保只编译一次:

c 复制代码
// test.h
#ifndef __TEST_H__ // 如果未定义__TEST_H__
#define __TEST_H__ // 定义__TEST_H__

// 头文件内容(函数声明、宏定义、结构体定义等)
#define PI 3.14159
void print_info();

#endif // __TEST_H__ // 结束条件编译
方案2:#pragma once(简洁高效)

部分编译器(GCC、VS、Clang)支持#pragma once,直接声明头文件只编译一次:

c 复制代码
// test.h
#pragma once // 核心指令,无需额外代码

// 头文件内容
#define PI 3.14159
void print_info();

💡 推荐:优先用#pragma once(简洁),如需兼容所有编译器,用条件编译。

七、其他预处理指令 ------ 拓展了解 📖

除了上述核心指令,还有一些优先级较低的预处理指令(如#error#line#pragma),感兴趣可参考《C语言深度解剖》深入学习:

  • #error:强制触发编译错误,输出自定义提示;
  • #line:修改当前行号和文件名;
  • #pragma:编译器特定指令(如#pragma pack调整内存对齐)。

写在最后 📝

到这里,C语言预处理的所有核心知识点就全部讲解完毕了!从基础的预定义符号、#define常量/宏,到进阶的宏与函数对比、#/##运算符、条件编译、头文件包含,我们形成了完整的知识体系。

预处理是C语言编译的第一步,也是写出"灵活、高效、可维护"代码的关键。重点记住:

  1. 宏适合简单运算、通用类型,函数适合复杂逻辑、可调试场景;
  2. 条件编译和头文件防重复包含是大型项目的必备技巧;
  3. 预处理指令发生在编译前,是"文本替换"而非"代码执行"。

至此,C语言的基础语法已全部学完!推荐拓展书籍:《C陷阱和缺陷》《高质量的C/C++编程》《剑指offer》,下一站可以进军《数据结构》啦~

核心要点总结

  1. 宏无调用开销但易出错,函数可调试但有开销,根据场景选择;
  2. #将参数字符串化,##拼接符号,是宏的高级用法;
  3. 条件编译可按需编译代码,常用于调试和跨平台开发;
  4. 头文件用#ifndef#pragma once避免重复包含;
  5. 命令行定义(-D)可动态修改宏的值,无需改源代码。

寄语:人和人最小的差距是智商,最大的差距是坚持~ 坚持敲代码、多实战,你一定能吃透C语言!

相关推荐
代码游侠2 小时前
应用——SQLite3 C 编程学习
linux·服务器·c语言·数据库·笔记·网络协议·sqlite
wjs20242 小时前
PHP 文件上传
开发语言
superman超哥2 小时前
Rust Feature Flags 功能特性:条件编译的精妙艺术
开发语言·后端·rust·条件编译·功能特性·feature flags
橙露2 小时前
Python 主流 GUI 库深度解析:优缺点与场景选型指南
开发语言·python
ss2732 小时前
Java Executor框架:从接口设计到线程池实战
开发语言·python
lsx2024062 小时前
PHP 包含
开发语言
花归去2 小时前
Promise 包含的属性
开发语言·javascript·ecmascript
2501_944446002 小时前
Flutter&OpenHarmony主题切换功能实现
开发语言·javascript·flutter
一路向北North2 小时前
java 下载文件中文名乱码
java·开发语言·python