C语言的预处理指令

文章目录

  • 宏定义
    • 简单的宏
    • 带参数的宏
    • 宏的通用属性
    • 实际编程中,遵守的一些规范
    • 预定义宏
    • 参数个数可变的宏
    • #运算符与##运算符(了解即可,用的不多)
  • 条件编译
    • #if指令和#endif指令
    • defined运算符
    • #ifdef指令和#ifndef指令
    • #elif指令和#else指令

C语言中,预处理指令是由#字符开头的一些命令。大多数预处理指令属于下面3种类型之一:

  • 宏定义#define指令定义一个宏,#undef指令删除一个宏定义。
  • 文件包含#include指令导致一个指定文件的内容被包含到程序中。
  • 条件编译#if#ifdef#ifndef#elif#else#endif指令能根据预处理器可以测试的条件来确定,是将一段文本块包含到程序中,还是将其排除在程序之外。

剩下的#error#line#pragma指令较少用到。

本文主要讲宏定义和条件编译。

宏定义

在C语言中,宏定义(Macro Definition)是一种在预处理阶段进行的文本替换机制。它允许程序员为一段代码或数据定义一个别名(即宏),以便在程序的后续部分中通过简单地引用这个别名来使用该代码或数据。宏定义通常使用#define指令来实现。

简单的宏

简单的宏定义格式如下:

#define 标识符 替换列表

举例:

#define PI 3.14159#define N 100
...
int a[N];

注意,在宏定义的末尾不要添加分号,下面的语句是错误的:

#define N 100;
...
int a[N];

带参数的宏

带参数的宏的定义格式如下:

#define 标识符(x1,x2,..,xn) 替换列表

例如:

#define MAX(x,y) ((x)>(y)?(x):(y))

宏的通用属性

  • 宏的替换列表可以包含对其他宏的调用。例如,我们可以用宏PI来定义宏TWO_PI

    #define PI 3.1415926
    #define TWO_PI (2*PI)
    

    当预处理器在后面的程序中遇到TWO_PI时,会将它替换成(2*PI)。接着,预处理器会重新检查替换列表,看它是否包含其他宏的调用(在这个例子中,调用了宏PI)。预处理器会不断重新检查替换列表,直到将所有的宏名字都替换完为止。

  • 宏定义的作用范围通常到出现这个宏的文件末尾。由于宏是由预处理器处理的,它们不遵从通常的作用域规则。定义在函数中的宏并不是仅在函数内起作用,而是作用到文件末尾。

  • 宏不可以被定义两遍,除非新的定义与旧的定义是一样的

  • 宏可以使用#undef指令取消定义#undef指令有如下形式:

    #undef 标识符
    

    其中,标识符是一个宏名。例如,指令#undef N会删除宏N当前的定义。(如果N没有被定义成一个宏,则#undef指令没有任何作用。)#undef指令的一个用途是取消宏的现有定义,以便于重新给出新的定义。

实际编程中,遵守的一些规范

在实际编程中,对于宏的使用,良好的编程习惯至少需要遵循以下规范:

  • 使用宏定义表达式时,要使用完备的括号,以避免运算优先级问题

    示例:如下定义的宏都存在一定的风险

    #define SQUARE(x) x * x
    

    当运行一下代码时:

    a = 5;
    printf("%d\n", SQUARE(a + 1));
    

    预期结果是36,但是实际上我们将用到的宏的地方替换,其实是:

    printf("%d\n", a+1 * a + 1);
    

    即输出结果为11。

    为了解决这个问题,只要在宏参数中加上两个括号就行:

    #define SQUARE(x) (x) * (x)
    

    但是现在有另一个宏:

    #define DOUBLE(x) (x) + (x)
    

    又会出现另一个问题。例如:

    a = 5;
    printf("%d\n", 10 * DOUBLE(a));
    

    我们期望的值是 100。但是通过宏展开得到:

    printf("%d\n", 10 *(x) + (x));
    

    得到的结果是 55。
    这个错误也很容易修正:只要在整个表达式两边加上一对括号即可:

    #define DOUBLE(x) ((x) + (x))
    

    所以,一般在定义宏时,首先每个宏参数都加上括号,其次整体表达式也要加上一对括号。

  • 不要使用带副作用的宏参数。当宏参数在宏定义中出现的次数超过一次时,如果这个参数有副作用,那么当你使用这个宏时就可能出现危险,导致不可预料的结果。

    例如:

    #define MAX(a, b) ((a) > (b) ? (a) : (b))
    ....
    x = 5;
    y = 8;
    z = MAX(x++, y++);
    printf("x=%d, y=%d, z= %d\n", x, y, z);
    

    第一个表达式是条件表达式,用于确定执行两个表达式中的哪一个,剩余的那个表达式将不会执行。
    那上面这段代码的输出是多少呢?
    x =6, y=10, z= 9;
    为什么呢?
    我们将宏定义展开:

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

    首先是比较两个表达式,比较完后,x= 6, y = 9.并且由于 y 比 x 大,所以在比较完后 y 还会再执行一次 y++。所以最终的结果是 y =10。

  • 当宏定义中包含多条语句时,最好使用do-while(9)的结构来包裹这些语句,以避免在使用宏时产生意外的副作用。

    比如:

    #define SWAP(x, y) do { \(x)->buffer = (y)->buffer; \(x)->orig_buffer = (y)->orig_buffer; \
    } while(0)
    

预定义宏

C语言中有一些预定义宏,每个宏表示一个整型常量或字面串。下面是一些常用的预定义宏(注意,下面的__是两个下划线_):

  • __LINE__:表示当前宏所在行的行号
  • __FILE__:当前文件的名字
  • __DATE__:编译的日期(格式"mm dd yyyy")
  • __TIME__:编译的时间(格式"hh:mm:ss")
  • __STDC__:如果编译器符合C标准(C89或C99),那么值为1.

上述宏中,__LINE__FILE__是用得最多的两个,而且通常两个一起用,用来定位实际问题。

zld@zld:~/Codes/C_TEST$ cat -n test6.c 1  #include <stdio.h>23  #define LOG_PRINT(message) do \4  { \5          printf("Debug: %s at %s:%d\n", message, __FILE__, __LINE__); \6  } while (0)789  int main()10  {11          LOG_PRINT("Start of the program");121314          LOG_PRINT("End of the program");1516          return 0;17  }

运行结果:

zld@zld:~/Codes/C_TEST$ ./test6 
Debug: Start of the program at test6.c:11
Debug: End of the program at test6.c:14

从上面的结果可以看出,通过使用__FILE____LINE__两个预定义宏,能够正确显示文件名和行号。

但是没有显示所在的函数名。

C99中定义了一个新特性__func__标识符。__func__与预处理器无关,但是由于它也是一般用于调试,所以经常和__FILE以及__LINE__一起使用。

zld@zld:~/Codes/C_TEST$ cat -n test7.c 1  #include <stdio.h>23  #define LOG_PRINT(message) do \4  { \5          printf("Debug: %s at %s:%d in function:%s\n", message, __FILE__, __LINE__, __func__); \6  } while (0)78  void Foo()9  {10          LOG_PRINT("Start of the Foo function");11  }1213  int main()14  {15          LOG_PRINT("Start of the program");1617          Foo();1819          LOG_PRINT("End of the program");2021          return 0;22  }

运行结果:

zld@zld:~/Codes/C_TEST$ ./test7
Debug: Start of the program at test7.c:15 in function:main
Debug: Start of the Foo function at test7.c:10 in function:Foo
Debug: End of the program at test7.c:19 in function:main

参数个数可变的宏

我们知道,C语言中,函数的参数个数是支持可变的。而对于宏,在C89标准中,如果宏有参数,那么参数的个数是固定的。在C99中,这个条件被放宽了,允许宏具有可变长度的参数列表。

C99引入了一个特殊的预定义宏__VA_ARGS__,它允许你定义接受参数可变数量参数的宏。这是通过宏定义中的省略号(...)来实现的。注意省略号(...)只能出现在宏参数列表的最后,前面是普通参数。

举例:

zld@zld:~/Codes/C_TEST$ cat test8.c 
#include <stdio.h>#define DEBUG(fmt, ...) printf(fmt, ## __VA_ARGS__)int main()
{int a = 5;float b = 3.14;DEBUG("Integer: %d\n", a);DEBUG("Float:%f\n", b);DEBUG("No extra args\n");return 0;
}

在这个例子中,DEBUG宏接受一个格式字符串fmt和任意数量的额外参数(由...表示)。在宏定义中,__VA_ARGS__被替换为传递给宏的所有额外参数。

运行结果:

zld@zld:~/Codes/C_TEST$ ./test8
Integer: 5
Float:3.140000
No extra args

#运算符与##运算符(了解即可,用的不多)

宏定义可以包含两个专用的运算符:###。编译器不会识别这两种运算符,它们会在预处理时被处理。

(1) 字符串常量化运算符(#):在宏定义中,当需要把一个宏的参数转换成字符串常量时,可以使用字符串常量运算符(#):

#define PRINT_INT(n) printf(#n " = %d\n", n)

n之前的#运算符通知预处理器根据PRINT_INT的参数创建一个字面串。因此,调用PRINT_INT(i/j);会变为:

printf("i/j" " = %d\n", i/j);

在C语言中,相邻的字面串会被合并,因此上面的语句等价于:

printf("i/j = %d\n", i/j);

当程序执行时,printf函数会同时显示表达式i/j和它的值。例如,如果i是11,j是2,则输出为:

i/j = 5

(2)标记粘贴运算符(##):宏定义内的标记粘贴运算符(##)会合并两个参数。

#include <stdio.h>
#define P(A, B) printf("%d##%d = %d", A, B, A##B)
int main() {P(5, 6);return 0;
}

输出:

5##6 = 56

注意:当宏参数是另一个宏的时候,需要注意的是凡宏定义中有用#或者##的地方宏参数时不会再展开:

#include <stdio.h>
#define f(x, y) x##y
#define g(x) #x
#define h(x) g(x)int main()
{printf("%s, %s\n", g(f(1, 2)), h(f(1,2)));return 0;
}

输出:

zld@zld:~/Codes/C_TEST$ ./test5 
f(1, 2), 12

解析:第一个表达式 g(f(1,2)),g(x)的定义中有#,不展开 f(x,y)的宏,直接替换成#f(1,2),打印输出为 f(1,2)。
第二个表达式 h(f(1, 2)),h(x)的定义中没有#或者##,需要展开 f(x, y)的宏, 即 1##2,即 h(12)->g(12), 最终结果是 12。

条件编译

C语言的条件编译时一种预处理功能,它允许程序在编译时根据特定的条件包含或排除代码段。这种功能通过预处理指令来实现,最常用的预处理指令有**#if指令和#endif指令**、#ifdef指令和#ifndef指令#elif指令和#else指令

#if指令和#endif指令

一般来说,#if指令的格式如下:

#if 常量表达式

#endif指令的格式如下:#endif

当预处理器遇到#if指令时,会计算常量表达式的值。如果常量表达式的值为0,那么#if#endif之间的行将在预处理过程中从程序中删除;否则,#if#endif之间的行会被保留在程序中,继续留给编译器处理——这时#if#endif对程序没有任何影响。

zld@zld:~/Codes/C_TEST$ cat -n test1.c 1  #include <stdio.h>23  #define VERSION 14  // #define VERSION 25  int main()6  {7  #if VERSION == 18          printf("Running version 1 of the program.\n");9  #endif1011          printf("Program execution continues...\n");12          return 0;13  }

上述代码,当第3行没有注释,第4行注释了时,运行结果如下:

zld@zld:~/Codes/C_TEST$ ./test1 
Running version 1 of the program.
Program execution continues...

当地3行注释,第4行没有注释时,运行结果:

zld@zld:~/Codes/C_TEST$ ./test1 
Program execution continues...

在实际项目开发中,如果有些代码当前没有用,但是又不想删,以后可能又会用到这段代码。相比于用/* */来注释这段代码,其实用#if 0更方便。这是因为在C语言中,注释不能嵌套,会导致编译错误:

初始时的一段程序如下:

zld@zld:~/Codes/C_TEST$ cat -n test2.c 1  #include <stdio.h>23  int main()4  {5          /*6           * This is a comment7           */8          printf("Hello world.\n");910          /*11           * This is another comment12           */13           printf("Hello C.\n");1415          /* This is the third comment */16          printf("Hello C++\n");17           return 0;1819  }
zld@zld:~/Codes/C_TEST$ 

上述程序,如果我第13行到第16行的程序想注释掉(大家想象一下这个代码有很多行,中间穿插着注释)。如果我用/* */注释的话将会报错:

zld@zld:~/Codes/C_TEST$ cat -n test2.c 1  #include <stdio.h>23  int main()4  {5          /*6           * This is a comment7           */8          printf("Hello world.\n");910          /*11          /*12           * This is another comment13           */14           printf("Hello C.\n");1516          /* This is the third comment */17          printf("Hello C++\n");18          */19           return 0;2021  }zld@zld:~/Codes/C_TEST$ gcc test2.c -o test2
test2.c: In function ‘main’:
test2.c:18:10: error: expected expression before ‘/’ token18 |         */|          ^

而用#if 0注释则不会有这个问题:

zld@zld:~/Codes/C_TEST$ cat -n test2.c 1  #include <stdio.h>23  int main()4  {5          /*6           * This is a comment7           */8          printf("Hello world.\n");910          #if 011          /*12           * This is another comment13           */14           printf("Hello C.\n");1516          /* This is the third comment */17          printf("Hello C++\n");18          #endif19           return 0;2021  }zld@zld:~/Codes/C_TEST$ ./test2 
Hello world.

defined运算符

在C语言中,defined运算符是一个预处理运算符,如果标识符是一个定义过的宏则返回1,否则返回0。

通常和#if指令结合使用,可以这样写:

#if defined(DEBUG)
...
#endif

仅当DEBUG被定义成宏时,#if#endif之间的代码会被保留在程序中,DEBUG两侧的括号不是必须的,因此可以简单地写成:

#if defined DEBUG

因为defined运算符仅检测DEBUG是否有定义,所以不需要给DEBUG赋值:

#define DEBUG

比如在头文件中为了防止被重复include,我们可以在头文件中这么写:

// 头文件的开始处
#if !defined XXX_XXX
#define XXX_XXX
...
#头文件的结束处
#endif

#ifdef指令和#ifndef指令

#ifdef指令用于测试一个标识符是否已经定义为宏,其格式如下:

#ifdef 标识符

#ifdef指令的使用与#if指令类似:

#ifdef 标识符
...
#endif

严格地说,并不需要#ifdef指令,因为可以结合#if指令和defined运算符来得到相同的效果。换言之,指令

#ifdef 标识符

等价于:

#if defined(标识符)

#ifndef指令与#ifdef指令类似,但测试的是标识符是否没有被定义成宏,其格式如下:

#ifndef 标识符

上述指令等价于:

#if !defined(标识符)

在C语言的头文件中,为了防止头文件被重复include,在头文件中更多的是用#ifndef的形式:

#ifndef __SDS_H
#define __SDS_H
...
#endif

#elif指令和#else指令

为了提供更多的遍历,预处理器还支持#elif#else指令。它们的格式如下:

#elif 常量表达式
#else

#elif指令和#else指令可以与#if指令、#ifdef指令、#ifndef指令结合使用,来测试一系列条件:

zld@zld:~/Codes/C_TEST$ cat test4.c 
#include <stdio.h>// 假设我们用一些宏来标识不同的操作系统
// #define __WIN32    // Windows操作系统
// #define __Linux__ // Linux操作系统
// #define __APPLE__ // macOS操作系统int main ()
{
#if defined(__WIN32)printf("This is Windows.\n");// 处理Windows特有的流程#elif defined(__Linux__)printf("This is Linux.\n");// 处理Linux特有的流程#elif defined(__APPLE__)printf("This is macOS.\n");// 处理macOS特有的流程#elseprintf("Unknown operating system.\n");// 处理其他未知操作系统的流程
#endif// 通用流程printf("Program continues...\n");return 0;
}

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

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

相关文章

SwiftUI 6.0(iOS 18)自定义容器值(Container Values)让容器布局渐入佳境(上)

概述 我们在之前多篇博文中已经介绍过 SwiftUI 6.0&#xff08;iOS 18&#xff09;新增的自定义容器布局机制。现在&#xff0c;如何利用它们对容器内容进行“探囊取物”和“聚沙成塔”&#xff0c;我们已然胸有成竹了。 然而&#xff0c;除了上述鬼工雷斧般的新技巧之外&…

finereport 数据下钻

目标&#xff1a;点击某块汇总的单元格&#xff0c;然后直接在原表的位置下钻到明细表&#xff0c;且不会影响整个大屏的结构&#xff0c;同时又支持明细表再回退到汇总表的功能 1、新建tab组件 1、新建决策报表 将 body 的布局方式改为「绝对布局」 2、将 Tab 块拖入 body…

@Id、@GeneratedValue的作用,以及@GeneratedValue的使用

在Java持久化API&#xff08;JPA&#xff09;中&#xff0c;Id和GeneratedValue注解是用于定义实体类的主键字段和主键生成策略的。这两个注解在构建基于JPA的ORM&#xff08;对象关系映射&#xff09;框架&#xff08;如Hibernate&#xff09;的应用时非常关键。 Id Id注解用…

小白都来用这款AI绘画神器,IDEOGRAM2.0,轻松画出高质量图片

大家好&#xff01;我是宇航&#xff0c;一位喜欢AI绘画的10年技术专家&#xff0c;专注于输出AI绘画与视频内容 今天给大家介绍一款绝对的生图神器——Ideogram2.0! 不论你是AI小白&#xff0c;手残党还是资深玩家&#xff0c;无论你是做网页设计&#xff0c;电商&#xff0c…

【Python爬虫实战】正则:从基础字符匹配到复杂文本处理的全面指南

&#x1f308;个人主页&#xff1a;https://blog.csdn.net/2401_86688088?typeblog &#x1f525; 系列专栏&#xff1a;https://blog.csdn.net/2401_86688088/category_12797772.html 目录 前言 一、正则表达式 &#xff08;一&#xff09;正则表达式的基本作用 &#xf…

The Android SDK location cannot be at the filesystem root

win11&#xff0c; 安装启动完Android Studio后&#xff0c;一直显示 The Android SDK location cannot be at the filesystem root因此需要下载SDK包&#xff0c;必须开启代理。 开启代理后&#xff0c;在System下开启自动检测代理&#xff0c;如图 重启Android Studio&a…

尚硅谷rabbitmq 2024 消息可靠性答疑二 第22节

returnedMessage()只有失败才调用&#xff0c;confirm()成功失败了都会调用&#xff0c;为什么&#xff1f; 在RabbitMQ中&#xff0c;消息的确认和返回机制是为了确保消息的可靠传递和处理。confirm和returnedMessage方法的调用时机和目的不同&#xff0c;因此它们的行为也有…

Java微信支付接入(8) - API V3 Native 用户取消订单API

官方文档&#xff1a;https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_3.shtml 实现用户主动取消订单的功能 定义取消订单接口 /*** 用户取消订单* param orderNo* return* throws Exception*/ ApiOperation("用户取消订单") PostMapping("/cance…

swoole框架有哪些呢

基于 Swoole 的 PHP 框架有很多&#xff0c;以下是一些比较流行和常用的框架&#xff1a; Hyperf&#xff1a;高性能企业级协程框架&#xff0c;基于 Swoole 4.4 实现。提供丰富的组件&#xff0c;如协程版的 MySQL 客户端、Redis 客户端、WebSocket 服务端及客户端等1。 Swof…

【微信小程序_11_全局配置】

摘要:本文介绍了微信小程序全局配置文件 app.json 中的常用配置项,重点阐述了 window 节点的各项配置,包括导航栏标题文字、背景色、标题颜色,窗口背景色、下拉刷新样式以及上拉触底距离等。通过这些配置可实现小程序窗口外观的个性化设置,提升用户体验。 微信小程序_11_全…

C语言 | Leetcode C语言题解之第462题最小操作次数使数组元素相等II

题目&#xff1a; 题解&#xff1a; static inline void swap(int *a, int *b) {int c *a;*a *b;*b c; }static inline int partition(int *nums, int left, int right) {int x nums[right], i left - 1;for (int j left; j < right; j) {if (nums[j] < x) {swap(…

树莓派应用--AI项目实战篇来啦-5.OpenCV绘画函数的使用

1. 介绍 OpenCV作为一款功能强大的计算机视觉库&#xff0c;被广泛地应用于图像处理和计算机视觉领域。 除了在机器视觉和人工智能领域有者广泛的应用&#xff0c;OpenCV 还能够媲美艺术家的创造力&#xff0c;通过其强大的绘图函数&#xff0c;绘制出令人叹为观止的艺术画作。…

flask项目框架搭建

目录结构 blueprints python包&#xff0c;蓝图文件&#xff0c;相当于路由组的概念,方便模块化开发 例如auth.py文件 from flask import Blueprint, render_templatebp Blueprint("auth", __name__, url_prefix"/auth")bp.route("/login") d…

Python数据可视化常用工具,值得收藏!!!

我们了解了如何使用 Pandas 进行简单的绘图,使用 Pandas 自带的绘图功能能够快速地生成一些基本的图表,例如折线图、柱状图等.但为了实现更复杂或专业的可视化效果,我们通常还需要借助更为强大的绘图库——Matplotlib. 本篇文章将详细介绍如何结合 Matplotlib 和 Pandas 实现数…

Redis-缓存一致性

缓存双写一致性 更新策略探讨 面试题 缓存设计要求 缓存分类&#xff1a; 只读缓存&#xff1a;&#xff08;脚本批量写入&#xff0c;canal 等&#xff09;读写缓存 同步直写&#xff1a;vip数据等即时数据异步缓写&#xff1a;允许延时&#xff08;仓库&#xff0c;物流&a…

C++: AVL树的实现

一.AVL树的旋转 AVL树是平衡搜索二叉树的一种。 平衡因子&#xff1a;节点右树的高度减左树的高度&#xff0c;AVL树规定平衡因子的绝对值小于2。若不在这个范围内&#xff0c;说明该树不平衡。 AVL树节点&#xff1a; struct AVLTreeNode {AVLTreeNode(const T& data …

数据结构--堆的深度解析

目录 引言 一、基本概念 1.1堆的概念 1.2堆的存储结构 1.3堆的特点 二、 堆的基本操作 2.1初始化 2.2创建堆 2.3插入元素 2.4删除元素 2.5堆化操作 2.6堆的判空 2.7获取堆顶元素 三、堆的常见应用 1. 优先队列 2. 堆排序 3. Top-k 问题 4. 图论中的应用 四…

rom定制系列------小米5x_miui12安卓11定制固件界面预览 小米5x第三方固件

&#x1f49d;&#x1f49d;&#x1f49d;此固件来源于客户卡刷固件定制。客户需要修改为线刷。并且修改账号锁功能。 可以让客户使用官方平台批量进行刷写。方便操作。 定制机型以及功能预览&#x1f49d;&#x1f49d;&#x1f49d; 小米5x版本miui12.5.8安卓11固件。此机型…

中国网络隐私保护:机遇与挑战并存的未来

随着数字经济的蓬勃发展&#xff0c;中国已进入大数据和互联网高速发展的时代。伴随而来的&#xff0c;是公众对网络隐私保护的强烈需求。从电子支付到社交平台&#xff0c;从智能家居到人脸识别&#xff0c;网络数据正在全面渗透到人们生活的方方面面。然而&#xff0c;数据隐…

MySQL 连接

使用MySQL二进制方式连接 使用MySQL二进制方式进入到MySQL命令提示符下来连接MySQL数据库。 实例 以下是从命令行中连接MySQL服务器的简单实例&#xff1a; [roothost]# mysql -u root -p Enter password:******在登录成功后会出现 mysql> 命令提示窗口&#xff0c;你可以在…