【C语言】深入理解预处理

文章目录

  • 一、预定义符号
  • 二、#define定义常量:便捷的符号替换
    • 常见用法示例:
    • 注意事项:
  • 三、#define定义宏:带参数的文本替换
    • 关键注意点:
  • 四、带有副作用的宏参数
  • 五、宏替换的规则:预处理的执行步骤
    • 重要注意:
  • 六、宏与函数的对比:各有优劣
    • 宏的独特优势:
  • 七、#和##运算符:字符串化与记号粘合
    • 7.1 #运算符:字符串化
    • 7.2 ##运算符:记号粘合
  • 八、命名约定:区分宏与函数
  • 九、#undef:移除宏定义
  • 十、命令行定义:编译时动态配置
  • 十一、条件编译:选择性编译代码
    • 常见条件编译指令:
    • 应用示例:调试代码控制
  • 十二、头文件的包含:正确引入外部代码
    • 12.1 包含方式及查找策略
    • 12.2 避免头文件重复包含
  • 十三、其他预处理指令

C语言的预处理阶段是代码编译前的重要环节,它负责处理以 #开头的各种指令,为后续的编译过程做好准备。预处理看似简单,实则包含了丰富的功能和细节,掌握这些知识能让我们写出更高效、更灵活的代码。

一、预定义符号

C语言为我们内置了一些预定义符号,它们在预处理期间就会被处理,我们可以直接在代码中使用,无需额外定义。这些符号能为我们提供很多有用的信息:

  • __FILE__:进行编译的源文件的文件名
  • __LINE__:当前代码在文件中的行号
  • __DATE__:文件被编译的日期(格式为“Mmm dd yyyy”)
  • __TIME__:文件被编译的时间(格式为“hh:mm:ss”)
  • __STDC__:如果编译器遵循ANSI C标准,其值为1;否则未定义
    在这里插入图片描述

这些符号在调试代码时非常有用,例如:

printf("Error in file: %s at line: %d\n", __FILE__, __LINE__);

当程序运行到这里时,会自动打印出错误所在的文件名和行号,帮助我们快速定位问题。

二、#define定义常量:便捷的符号替换

#define最基本的用法是定义常量,其基本语法为:

#define name stuff

常见用法示例:

  • 定义数值常量:#define MAX 1000
  • 为关键字创建简短别名:#define reg register
  • 替换复杂实现为更形象的符号:#define do_forever for(;;)
  • 简化代码编写:#define CASE break;case(在switch语句中自动添加break)

注意事项:

  1. 不要随意添加分号
    例如#define MAX 1000;这样的定义是不推荐的。当在
if(condition)max = MAX; else max = 0;

中使用时,替换后会变成

if(condition) max = 1000;; 
else max = 0;

导致if和else之间出现两条语句,引发语法错误。

  1. 长内容的分行处理
    如果定义的内容过长,可以分成多行书写,除最后一行外,每行末尾都加上反斜杠(续行符)。反斜杠后面不能有任何内容
   #define DEBUG_PRINT printf("file:%s\tline:%d\t \date:%s\ttime:%s\n", \__FILE__, __LINE__, \__DATE__, __TIME__)

三、#define定义宏:带参数的文本替换

#define还允许我们定义带参数的宏(macro),实现更灵活的文本替换。宏的声明方式为:

#define name(parameter-list) stuff

其中parameter-list是由逗号分隔的参数列表,它们会出现在stuff中。

关键注意点:

  • 参数列表与名称紧连:参数列表的左括号必须与name紧邻,中间不能有空白,否则参数列表会被解释为stuff的一部分。

  • 运算符优先级问题
    例如#define SQUARE(x) x * x这个宏,当传入SQUARE(a + 1)时,会被替换为a + 1 * a + 1,结果为a + a + 1而非预期的(a+1)^2。解决方法是给参数和整体加上括号:#define SQUARE(x) (x) * (x)

    更复杂的情况:#define DOUBLE(x) (x) + (x),当调用10 * DOUBLE(5)时,会被替换为10 * (5) + (5),结果为55而非100。正确的定义应为#define DOUBLE(x) ((x) + (x))

    结论:用于数值表达式求值的宏,应给参数和整体都加上括号,避免运算符优先级导致的意外结果。

四、带有副作用的宏参数

当宏参数在宏定义中出现多次,且参数带有副作用时,可能会产生不可预测的结果。副作用指表达式求值时产生的永久性效果(如x++会改变x的值,而x+1则不会)。

例如:

#define MAX(a, b) ((a) > (b) ? (a) : (b))int x = 5, y = 8, z;
z = MAX(x++, y++);

预处理后会变成:

z = ((x++) > (y++) ? (x++) : (y++));

执行后,x的值变为6,y变为10,z变为9,与直观预期可能不符。这就是副作用参数带来的问题。

五、宏替换的规则:预处理的执行步骤

在扩展#define定义的符号和宏时,预处理器遵循以下步骤:

  1. 调用宏时,首先检查参数是否包含#define定义的符号,若有则先替换。
  2. 将替换文本插入到原位置,宏的参数名被其值替换。
  3. 再次扫描结果文件,若包含#define定义的符号,重复上述过程。

重要注意:

  • 宏参数中可以包含其他#define定义的符号,但宏不能递归。
  • 预处理器不会搜索字符串常量的内容(即字符串中的符号不会被替换)。

六、宏与函数的对比:各有优劣

宏和函数都能实现代码复用,但它们在多个方面存在差异:

属性#define定义宏函数
代码长度每次使用都会插入代码,可能大幅增加程序长度(除非宏很短)代码只出现一次,每次调用都使用同一份代码
执行速度更快(无函数调用和返回开销)较慢(存在函数调用和返回的额外开销)
操作符优先级可能受周围表达式优先级影响,需谨慎使用括号参数只在传参时求值,结果传递给函数,表达式求值可预测
副作用参数参数若多次出现,副作用可能导致不可预料的结果参数只在传参时求值一次,副作用易控制
参数类型与类型无关,只要操作合法即可用于任何类型与类型相关,不同类型可能需要不同函数(即使功能相同)
调试不方便调试(预处理阶段已替换)可逐语句调试
递归不能递归可以递归

宏的独特优势:

宏可以接受类型作为参数,而函数无法做到。例如:

#define MALLOC(num, type) (type *)malloc(num * sizeof(type))// 使用
int *p = MALLOC(10, int);
// 替换后:(int *)malloc(10 * sizeof(int));

七、#和##运算符:字符串化与记号粘合

7.1 #运算符:字符串化

#运算符能将宏的参数转换为字符串字面量,仅用于带参数的宏的替换列表中。

例如:

#define PRINT(n) printf("the value of "#n " is %d", n);// 调用
int a = 10;
PRINT(a); // 替换后:printf("the value of ""a"" is %d", a);

输出结果为:the value of a is 10

7.2 ##运算符:记号粘合

##可以将两边的符号合并为一个符号,允许从分离的文本片段创建标识符(称为“记号粘合”)。

为不同类型定义求最大值的函数:

#define GENERIC_MAX(type)\
type type##_max(type a,type b)\
{\return a > b ? a : b;\
}GENERIC_MAX(int);//int_max()
GENERIC_MAX(float);//float_max()int main()
{int r1 = int_max(3, 9);float r2 = float_max(4.6, 8.9);printf("%d  %f", r1, r2);return 0;
}

八、命名约定:区分宏与函数

宏和函数的使用语法相似,为了区分二者,通常遵循以下约定:

  • 宏名全部大写(如MAXSQUARE
  • 函数名不要全部大写(如int_maxadd

九、#undef:移除宏定义

#undef指令用于移除已有的宏定义,语法为:

#undef NAME

如果需要重新定义一个已存在的宏,应先使用#undef移除其旧定义。

十、命令行定义:编译时动态配置

许多C编译器允许在命令行中定义符号,用于启动编译过程。这在根据同一源文件编译程序的不同版本时非常有用。

根据内存大小动态调整数组长度:

// 源文件program.c
#include <stdio.h>
int main() {int array[ARRAY_SIZE];for (int i = 0; i < ARRAY_SIZE; i++) {array[i] = i;printf("%d ", array[i]);}return 0;
}

编译时在命令行定义ARRAY_SIZE

# Linux环境
gcc -D ARRAY_SIZE=10 program.c  # 定义数组长度为10

十一、条件编译:选择性编译代码

条件编译指令允许我们选择性地编译或放弃某些语句,常用于调试代码的开关控制。

常见条件编译指令:

  1. 基本形式
   #if 常量表达式// 代码段#endif

预处理器会求值常量表达式,为真则编译代码段。

  1. 多分支形式
   #if 常量表达式// 代码段1#elif 常量表达式// 代码段2#else// 代码段3#endif
  1. 判断符号是否定义:只判断是否被定义,不判断真假
   #if defined(symbol)   // 等价于 #ifdef symbol// 代码段#endif#if !defined(symbol)  // 等价于 #ifndef symbol// 代码段#endif
  1. 嵌套指令
   #if defined(OS_UNIX)#ifdef OPTION1unix_version_option1();#endif#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2();#endif#endif

应用示例:调试代码控制

#define __DEBUG__ 1  // 定义调试符号int main() {int arr[10] = {0};for (int i = 0; i < 10; i++) {arr[i] = i;#ifdef __DEBUG__printf("arr[%d] = %d\n", i, arr[i]);  // 仅调试时编译#endif}return 0;
}

十二、头文件的包含:正确引入外部代码

头文件包含是预处理阶段的重要操作,用于将其他文件的内容插入到当前文件中。

12.1 包含方式及查找策略

  • 本地文件包含#include "filename"
    查找策略:先在源文件所在目录查找,若未找到,再到标准库目录查找。

  • 库文件包含#include <filename.h>
    查找策略:直接到标准库目录查找,效率更高。

注意:库文件也可以用""包含,但会降低查找效率,且不易区分是本地文件还是库文件。

12.2 避免头文件重复包含

头文件被多次包含会导致代码冗余、编译时间增加,甚至出现重复定义错误。解决方法是使用条件编译:

  1. 方式一:ifndef/define/endif
   // test.h#ifndef __TEST_H__  // 如果未定义__TEST_H__#define __TEST_H__  // 定义__TEST_H__// 头文件内容(函数声明、结构体定义等)void test();struct Stu { int id; char name[20]; };#endif  // __TEST_H__
  1. 方式二:#pragma once
    更简洁的方式,直接在头文件开头添加:
   #pragma once  // 确保头文件只被包含一次// 头文件内容

十三、其他预处理指令

除了上述内容,C语言还有一些其他预处理指令,如:

  • #error:在编译时输出错误信息,终止编译
  • #pragma:用于向编译器提供额外信息(如#pragma pack()控制结构体对齐)
  • #line:修改当前行号和文件名

预处理是C语言编译过程中的第一个环节,它通过#define#include、条件编译等指令,为代码的编译做好准备。掌握预处理的各种特性,不仅能帮助我们写出更高效、更灵活的代码,还能让我们在调试和维护程序时更得心应手。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/918366.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/918366.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

展锐平台(Android15)WLAN热点名称修改不生效问题分析

前言 在展锐Android V项目开发中&#xff0c;需要修改softAp/P2P热点名称时&#xff0c;发现集成GMS后直接修改framework层代码无效。具体表现为&#xff1a; 修改packages/modules/Wifi/WifiApConfigStore中的getDefaultApConfiguration方法编译烧录后修改不生效 问题根源在…

wsl ubuntu访问(挂载)vmware vmdk磁盘教程

之前使用VMware Workstation 虚拟机跑了个ubuntu&#xff0c;现在改用wsl了&#xff0c; 想把vmware的磁盘挂载到wsl ubuntu。一、磁盘合并我原先的vmware跑的ubuntu存在多个vmdk文件&#xff08;磁盘文件&#xff09;&#xff0c;需要先将磁盘合并成一个才方便挂载。首先你电脑…

UGUI源码剖析(3):布局的“原子”——RectTransform的核心数据模型与几何学

UGUI源码剖析&#xff08;第三章&#xff09;&#xff1a;布局的“原子”——RectTransform的核心数据模型与几何学 在前几章中&#xff0c;我们了解了UGUI的组件规范和更新调度机制。现在&#xff0c;我们将深入到这个系统的“几何学”核心&#xff0c;去剖析那个我们每天都在…

c++注意点(15)----设计模式(桥接模式与适配器模式)

一、结构型设计模式两者有点相似&#xff0c;都是为了做到解耦的功能。适配器模式是一种结构型设计模式&#xff0c; 它能使接口不兼容的对象能够相互合作。桥接模式是一种结构型设计模式&#xff0c; 可将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构&…

DuoPlus支持导入文件批量配置云手机参数,还优化了批量操作和搜索功能!

作为我常用的一款还不错的跨境工具&#xff0c;DuoPlus云手机帮我高效完成了很多跨境工作&#xff0c;它的功能也在逐步完善和优化&#xff0c;今天来聊聊它最近新更新的一些功能。功能更新一览新增导入文件配置参数&#xff1a;批量初始化代理、批量修改参数支持导入文件一键配…

PLC如何实现通过MQTT协议物联网网关接入管理云平台

在工业4.0与智能制造浪潮下&#xff0c;企业亟需实现设备数据的高效采集与云端协同&#xff0c;以支撑远程监控、预测性维护等场景。工业智能网关凭借其强大的协议解析能力、边缘计算功能及安全传输机制&#xff0c;成为PLC接入云平台的核心解决方案。本文将从技术架构、功能模…

通过sealos工具在ubuntu 24.02上安装k8s集群

一、系统准备&#xff08;1&#xff09;安装openssh服务 sudo apt install openssh-server sudo systemctl start ssh sudo systemctl enable ssh&#xff08;2&#xff09;放通防火墙 sudo ufw allow ssh&#xff08;3&#xff09;开通root直接登录 vim /etc/ssh/sshd_config#…

nginx+Lua环境集成、nginx+Lua应用

nginxluaredis实践 概述 nginx、lua访问redis的三种方式&#xff1a; 1。 HttpRedis模块。 指令少&#xff0c;功能单一 &#xff0c;适合简单的缓存。只支持get 、select命令。 2。 HttpRedis2Module模块。 功能强大&#xff0c;比较灵活。 3。 lua-resty-redis库 OpenResty。…

机器学习 K-Means聚类 无监督学习

目录 K-Means 聚类&#xff1a;从原理到实践的完整指南 什么是 K-Means 聚类&#xff1f; 应用场景举例 K-Means 算法的核心原理 K-Means 算法的步骤详解 可视化理解 K-Means 的优缺点分析 优点 缺点 如何选择合适的 K 值&#xff1f; 1. 肘部法&#xff08;Elbow Me…

RabbitMQ面试精讲 Day 16:生产者优化策略与实践

【RabbitMQ面试精讲 Day 16】生产者优化策略与实践 开篇 欢迎来到"RabbitMQ面试精讲"系列第16天&#xff0c;今天我们聚焦RabbitMQ生产者优化策略与实践。在消息队列系统中&#xff0c;生产者的性能表现直接影响整个系统的吞吐量和可靠性。掌握生产者优化技巧不仅能…

Android 系统的安全 和 三星安全的区别

维度Android&#xff08;AOSP 通用&#xff09;Samsung&#xff08;Knox 强化&#xff09;本质差异一句话信任根标准 Verified Boot&#xff08;公钥由谷歌或 OEM 托管&#xff09;额外在 自家 SoC 里烧录 Knox 密钥 熔丝位&#xff0c;一旦解锁即触发 Knox 0x1 熔断&#xff…

开源大模型实战:GPT-OSS本地部署与全面测评

文章目录一、引言二、安装Ollama三、Linux部署GPT-OSS-20B模型四、模型测试4.1 AI幻觉检测题题目1&#xff1a;虚假历史事件题目2&#xff1a;不存在的科学概念题目3&#xff1a;虚构的地理信息题目4&#xff1a;错误的数学常识题目5&#xff1a;虚假的生物学事实4.2 算法题测试…

【无标题】命名管道(Named Pipe)是一种在操作系统中用于**进程间通信(IPC)** 的机制

命名管道&#xff08;Named Pipe&#xff09;是一种在操作系统中用于进程间通信&#xff08;IPC&#xff09; 的机制&#xff0c;它允许不相关的进程&#xff08;甚至不同用户的进程&#xff09;通过一个可见的文件系统路径进行数据交换。与匿名管道&#xff08;仅存在于内存&a…

Baumer相机如何通过YoloV8深度学习模型实现危险区域人员的实时检测识别(C#代码UI界面版)

《------往期经典推荐------》 AI应用软件开发实战专栏【链接】 序号 项目名称 项目名称 1 1.工业相机 + YOLOv8 实现人物检测识别:(C#代码,UI界面版) 2.工业相机 + YOLOv8 实现PCB的缺陷检测:(C#代码,UI界面版) 2 3.工业相机 + YOLOv8 实现动物分类识别:(C#代码,U…

本文章分享一个本地录音和实时传输录音给app的功能(杰理)

我用的是杰理手表sdk&#xff0c;该功能学会就可自行在任何杰里sdk上做&#xff0c;库函数大致一样&#xff0c;学会运用这个方向就好。1.我们要验证这个喇叭和麦是否正常最简单的的办法&#xff0c;就是直接万用表测试&#xff0c;直接接正负极&#xff0c;看看是否通路&#…

Netty-Rest搭建笔记

0.相关知识Component、Repository、ServiceRepository //Scope设置bean的作用范围 Scope("singleton")//单例 prototype每次创建都会给一个新实例。 public class BookDaoImpl implements BookDao { //生命周期public void save() {System.out.println("book d…

工作笔记-----lwip网络任务初始化问题排查

工作笔记-----基于FreeRTOS的lwIP网络任务初始化问题排查 Author&#xff1a;明月清了个风Date&#xff1a; 2025/8/10PS&#xff1a;新项目中在STMF7开发板上基于freeRTOS和lwIP开发网口相关任务&#xff0c;开发过程中遇到了网口无法连接的问题&#xff0c;进行了一系列的排查…

Kotlin动态代理池+无头浏览器协程化实战

我看到了很多作者展示了Kotlin在爬虫领域的各种高级用法。我需要从中提取出最"牛叉"的操作&#xff0c;也就是那些充分利用Kotlin语言特性&#xff0c;使爬虫开发更高效、更强大的技巧。 我准备用几个主要部分来组织内容&#xff0c;每个部分会突出Kotlin特有的"…

PDF编辑工具,免费OCR识别表单

软件介绍 今天推荐一款功能全面的PDF编辑工具——PDF XChange Editor&#xff0c;支持文本、图片编辑及OCR识别&#xff0c;还能一键提取表单信息&#xff0c;满足多样化PDF处理需求。 软件优势 该软件完全免费&#xff0c;下载后双击图标即可直接运行&#xff0c;无需安装&…

OpenEnler等Linux系统中安装git工具的方法

在欧拉系统中安装 Git使用 yum 包管理器安装&#xff08;推荐&#xff0c;适用于欧拉等基于 RPM 的系统&#xff09;&#xff1a;# 切换到 root 用户&#xff08;若当前不是&#xff09; su - root# 安装 Git yum install -y git验证安装是否成功&#xff1a;git --version若输…