[C++]宏定义

C/C++宏的基本使用方法

宏是C/C++所支持的一种语言特性,我对它最初的印象就是它可以替换代码中的符号,最常见的例子便是定义一个圆周率PI,之后在代码中使用 PI 来代替具体圆周率的值。

确实如此,宏提供了一种机制,能够使你在编译期替换代码中的符号或者语句。当你的代码中存在大量相似的、重复的代码时,使用宏可以极大的减少代码量,便于书写。

在很多书上以及网文上,宏都是不被推荐使用的,因为它会带来一些隐晦的坑,让你不经意间便受其所困。但是,正如世间的万千事物,没有什么是完全有害的,也没有什么是完全有益的,只在于如何看待它和使用它

宏的语法

定义

宏使用#define定义,一种简单的定义如下

// 定义圆周率
#define PI 3.14159265
// 定义一个空指针
#define NULL ((void*)0)
// 定义一个宏的名字为 SYSTEM_API,但是没有值
#define SYSTEM_API

上述例子中定义了一个圆周率PI,那么代码中需要用到圆周率的地方就可以使用PI来代替,比如语句

double perimeter = diameter * 3.14159265;

就可以写成

double perimeter = diameter * PI;

而该代码在编译时,编译器又会把PI替换为它所定义的值(3.14159265)进行编译,因此,这两条语句是等价的。

C语言中的NULL就是一个语言已经预定义的宏。预定义指的是你不必亲自定义,编译器在编译时,已经提前定义好了。

SYSTEM_API这个宏没有定义任何值,替换后等价于什么都没写,比如像下面两条语句就是等价的。

class SYSTEM_API CSystem;

class CSystem;

是等价的。做过Windows模块开发的同学,可能已经意识到,上述的宏经常和预处理指令#ifdef配合来控制模块的导出导入符号。

参数

宏还可以向函数一样携带参数,像下面这样

#define MUL(x, y) x * y
int ret = MUL(2, 3);   ==> int ret = 2 * 3;

这使它看起来特别像函数,它跟函数的区别有以下几点

  • 宏是简单的符号替换,不会检查参数类型,而函数会严格检查输入的参数类型
  • 因为宏是在编译期进行的符号替换,所以在运行时,不会带来额外的时间和空间开销,而函数会在运行时执行压栈出栈的操作,存在函数调用的开销
  • 宏是不可以调试的,而函数可以进行单步调试
  • 宏不支持递归,函数支持递归

在上例中,MUL携带有两个参数xy,当使用此宏时,只需将传入宏的两个参数直接的相乘即可。
那宏的参数是否支持表达式呢,答案是支持的,但由于宏只是简单的展开替换,因此我们就遇到了宏第一个容易出错的点

int ret = MUL(2 + 3, 4);

我们的本意是先计算出2加3的和,然后与4相乘,结果为20。但实际上该宏展开后的代码为

int ret = 2 + 3 * 4;

看到了吗,宏就是非常直接的把x换成2 + 3,把y换成4,由于运算符优先级的缘故,最终算的结果为14,一个非预期的结果。

如何修正这个问题呢,就是在定义时把参数都加上括号

#define MUL(x, y) ((x) * (y))

这样的话,上述例子就被展开成为

int ret = ((2 + 3) * (4));

从而保证了运算的顺序与期望的顺序一致。

符号###

#符号把一个宏参数直接转换为字符串,例如

#define STRING(x) #x
const char * str = STRING(test);
// str的内容就是"test"

##符号会连接两边的值,像一个粘合剂一样,将前后两部分粘合起来,从而产生一个新的值,例如

#define VAR(index) INT_##index
int VAR(1);
// 宏被展开后将成为 int INT_1;

可变参数

宏也可以支持可变长参数,这个特性可以用来对类似printf这样的函数进行封装,使用时,使用__VA_ARGS__这个系统预定义宏来代替printf的参数,例如

#define trace(fmt, ...) printf(fmt, ##__VA_ARGS__)
// 这样我们就可以使用我们自己定义的宏 trace 来打印日志了
trace("got a number %d", 34);

至于为什么要在__VA_ARGS__之前添加##符号,主要是因为,如果不添加的话,当只有fmt参数,__VA_ARGS__为空时,之前的逗号不会删除

trace("got a number");   ==>  trace("got a number",);

从而导致编译错误,而加上##符号的话,将使预处理器去除掉它前面的那个逗号。

多行的宏

如果宏的内容很长,很多,那么可以写成多行,每行的末尾添加\,以表明后面的一行依然是宏的内容。比如

#define ADD(x, y) do { int sum = (x) + (y); return sum; } while (0)
// 宏的内容比较长,也没有缩进,易读性较差,因此转为多行
#define ADD(x, y) \
do \
{\
    int sum = (x) + (y);\
    return sum;\
} while (0)

取消宏定义

如果想要取消对一个宏的定义,可以使用#undef预处理指令,比如要取消之前定义的ADD宏,只要像下面即可

#undef ADD

编译器参数定义以及预定义宏

除了使用#define预处理器来定义宏之外,也可以通过编译器参数来定义宏,具体可参考各平台的编译器参数。编译器也会在编译某文件时预定义一些宏供使用,常见的有以下几个:

宏的调试

宏不支持在运行时调试,但如果宏太过于复杂的话,出错也是难免的,因此,可以利用宏自身的特性把宏展开后的内容打印出来,来方便我们查错。

这里有一个技术前提,如果想要在编译时打印一些信息,可以使用如下预处理指令:

#pragma message (“will print this message”)

但是,如果想要打印某个宏的内容,会发现编译器会报错。比如我们想要打印宏SOMEMACRO的内容。直接使用#pragam message (SOMEMACRO)是不行的,原因是该指令必须接收一个字符串,可使用如下代码协助输出SOMEMACRO的内容。

#define SOMEMACRO 123456
#define MACROTOSTR2(x) #x
#define PRINTMACRO(x) #x " = " MACROTOSTR2(x)
#pragma message(PRINTMACRO(SOMEMACRO))

编译上述代码便会在输出窗口打印SOMEMACRO = 123456的内容。

对于带参数的宏也是适用的:

#define SOMEMACRO 123456
#define MACROPARAM(x) new int(x);
#define MACROTOSTR2(x) #x
#define PRINTMACRO(x) #x " = " MACROTOSTR2(x)
#pragma message(PRINTMACRO(MACROPARAM(SOMEMACRO)))

上述代码块在编译时会打印出MACROPARAM(SOMEMACRO) = new int(123456);,也就是宏展开后的内容。

因此,当宏出现问题时,可以使用该方法打印出宏展开后的内容,然后调试展开后的内容,找到错误原因,接着同步修正宏本身的错误。

常见的使用场景

和#ifdef和#if等预处理指令配合

通过和预处理指令配合,达到一定的代码开关控制,常见的比如在跨平台开发时,对不同的操作系统启用不同的代码。

#ifdef _WIN32 // 查看是否定义了该宏,Windows默认会定义该宏
    // 如果是Windows系统则会编译此段代码
    OutputDebugString("this is a Windows log");
#else
    // 如果是mac,则会编译此段代码
    NSLog(@"this is a mac log");
#endif

如果要查看多个宏是否定义过,可使用下面的预处理指令

#if defined(_WIN32) || defined(WIN32)
    // 如果是Windows系统则会编译此段代码
    OutputDebugString("this is a Windows log");
#endif

#ifdef之后的宏只要定义过就会满足条件,而#if则会看后面的宏的内容是否为真了。

#define ENABLE_LOG 1
#if ENABLE_LOG
    trace("when enabled then print this log")
#endif

如果把宏的定义改成#define ENABLE_LOG 0,那么就不会满足条件了,也就不会打印日志了。在使用#if时,后面的宏ENABLE_LOG必须定义为整数才行,定义为其他的会报编译错误。

防止重复包含头文件

在C、C++中如果重复包含了同一个头文件,有可能会带来编译错误,所以我们应当避免这种事情发生,利用预处理指令和宏可以有效防止此类错误发生。具体措施为,在每一个头文件的开始和结束,加上如下的语句

#ifndef __SYSTEM_API_H__
#define __SYSTEM_API_H__

// 头文件的内容
...

#endif

第一次包含此文件时,__SYSTEM_API_H__还没有被定义,因此,头文件的内容被顺利的包含进来,同时,定义了该宏,如果此头文件被重复包含了,那么文件第一行的预处理指令将不会满足,因此文件也就不会被重复包含了。

打印错误信息

在输出日志时,除了输出错误信息外,如果能够把当前的文件名和行号一并打印出来,那就好了,这样的话就可以更快的定位问题了,之前说过,编译器已经为我们预定义了当前文件名和当前行号的宏,我们只要在输出日志时输出这些信息即可。比如

printf("%s %d printf message %s\n", __FILE__, __LINE__, "some reason");

这样有一个问题,如果每次输出信息都这么写,太繁琐了,而且,大部分都一样,因此,我们可以用宏来封装一下

#define trace(fmt, ...) printf("%s %d "fmt, __FILE__, __LINE__, ##__VA_ARGS__)
// 这样在使用时可以这么写,同样可以输出当前行号和文件名
trace("printf message %s\n", "some reason");

如此,就可以把注意力集中在要输出的信息上,而不被__FILE__,__LINE__干扰了,同时也少写了一些繁琐的代码。

减少重复代码

如果有一个类,它携带有很多的属性,而每一个属性都必须进行实现setget函数,那么就可以使用宏来减少代码的输入。

// 类Widget拥有非常多的属性,但每一个属性的相应函数实现是类似的
class Widget
{
public:
    // Width属性
    int getWidth() const
    {
        return _Width;
    }
    void setWidth(int Width)
    {
        // 当设置新值时,打印一条日志,方便调试
        printf("setWidth %d\n", Width);
        _Width = Width;
    }
    // Height属性
    int getHeight() const
    {
        return _Height;
    }
    void setHeight(int Height)
    {
        // 当设置新值时,打印一条日志,方便调试
        printf("setHeight %d\n", Height);
        _Height = Height;
    }
    // 之后还有其他的属性定义......
};

可以发现,虽然属性很多,但是属性的处理基本是一致的,因此可以使用宏封装一下

// 定义一个PROPERTY宏来生成相应的函数实现
#define PROPERTY(Name)\
int get##Name() const\
{\
    return _##Name;\
}\
void set##Name()\
{\
    printf("set"#Name" %d\n", Name);\
    _##Name = Name;\
}

// 接下来就可以重新定义Widget类了
class Widget
{
public:
    // Width属性
    PROPERTY(Width)
    // Height属性
    PROPERTY(Height)
    // 其他的属性
    PROPERTY(Color)
    PROPERTY(BackgroundColor)
    // ......
};

这样是不是简单多了,需要注意的是,上述例子的属性类型固定为了int,实际中可以扩展PROPERTY宏来支持不同的参数类型。而由于宏不支持调试,因此,使用宏生成的函数将不能在IDE中单步调试,因此,如果函数实现复杂的话,还是少用为妙。

不能武断说用宏好还是用宏不好,应该依据实际情况而定。

易出问题的地方

优先级的改变

由于宏只是简单的替换,所以在某些情况下会不知不觉的改变运算的优先级。比如,如果定义了下面这样的宏

#define ADD(x, y) x + y
int value = ADD(2, 3) * ADD(4, 5);

我们期望先分别计算2和3,4和5的和,然后相乘得出45。但实际宏展开后的代码为

int value = 2 + 3 * 4 + 5;

由于乘号的优先级大于加号,所以是先计算了3和4的积,然后再与2和5相加,得出了不期望的结果19。导致错误,这种问题的修改策略就是在宏定义时加上括号,包括参数都加上括号。即

#define ADD(x, y) ((x) + (y))

宏名称的冲突

如果定义的宏名称不小心和其他源码中的名称冲突的话,也会造成编译错误,比如定义了一个宏time,那么就有可能会和标准库函数中的time函数冲突。

宏参数中含有逗号

宏可以携带参数,而参数并没有什么要求,宏只是拿到参数的值去替换之后的内容,但如果宏参数中含有逗号,那么就会带来歧义了。比如

// 该宏本身没什么实际使用意义,只是为了说明问题
#define segment(seg) seg
// 没有问题
segment(int x = 1; int y = 3);
// 编译错误,因为宏展开时把","视为参数间的分隔符
segment(int x = 1, y = 3);
// 解决办法就是给宏参数加上括号,使其为一体
segment((int x = 1, y = 3));

宏定义中常见的 do{ }while(0)

在阅读第三方源码时,经常见到宏定义中有一个do{ }while(0)语句,这是为什么呢?比如我们定义一个交换两个值的宏

#define swapint(x, y) int tmp = x; x = y; y = tmp;

在大部分情况下可以工作,但是如果之前已经定义了tmp这个变量,则就会出错了,那我们可以把tmp换成平时不常用的名字,就大大降低了重名的概率了,这确实是一个办法,但不完美,因为即使这样,依然无法用在switch语句中

int x = 1, y = 2;
switch (value)
{
    case 1:
        // 编译出错,因为case语句中不允许声明变量
        swapint(x, y);
        break;
}

那我们想,是否可以定义宏的时候,加上一层大括号,嗯,确实可以。

#define swapint(x, y) {int tmp = x; x = y; y = tmp;}

这样便可以用在switch语句中了。是否就完美了呢,依然不行,因为还可能会影响if语句的执行,看下面的例子

int x = 1, y = 2;
if (x < y)
    swapint(x, y);
else
    someaction();
// 上面的代码展开
if (x < y)
    {int tmp = x; x = y; y = tmp;};
else
    someaction();
// 编译出错,因为在else之前多了一个分号,导致语法错误,那么能不能不加分号
// 可以,但是C++程序员一般都习惯在末尾添加分号,而且不过不加分号,也会影响
// IDE的自动代码格式化

这时,就要祭出do{ }while(0)大杀器了,

使用do{….}while(0) 把它包裹起来,成为一个独立的语法单元,从而不会与上下文发生混淆。

同时因为绝大多数的编译器都能够识别do{…}while(0)这种无用的循环并进行优化,所以使用这种方法也不会导致程序的性能降低。

总结

宏有时能够方便我们编程,有时又能使我们陷入无底深渊。有时我们赞赏宏的优点从而责怪某些语言里为什么没有宏,有时又唾沫横飞大骂宏的缺点。真是又爱又恨。有句话叫做 存在即合理,我想宏也适用,宏是否有益,一是取决于本身,更多的是取决于使用它的人吧!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

动力学约束下的运动规划算法——两点边界值最优控制问题 OBVP

OBVP 即 optimal bundary value problem&#xff0c;即最优的BVP&#xff0c; BVP 问题其实就是解决 state sampled lattice planning 的基本操作方法。 如果&#xff0c;我们期望无人机从一个状态移动到另一个状态&#xff0c;即给定初始状态和终点状态&#xff0c;求解两个状…

每日五道java面试题之spring篇(五)

目录&#xff1a; 第一题. 使用 Spring 有哪些方式&#xff1f;第二题. 什么是Spring IOC 容器&#xff1f;第三题. 控制反转(IoC)有什么作用?第四题. IOC的优点是什么&#xff1f;第五题. BeanFactory 和 ApplicationContext有什么区别&#xff1f; 第一题. 使用 Spring 有哪…

【Web】CTFSHOW 常用姿势刷题记录(全)

目录 web801 web802 web803 web804 web805 web806 web807 法一&#xff1a;反弹shell 法二&#xff1a;vps外带 web808 web809 web810 web811 web812 web813 web814 web815 web816 web817 web818 web819 web820 web821 web822 web823 web824 web825…

03|Order by与Group by优化

索引顺序依次是 &#xff1a; name,age,position 案例1 EXPLAIN SELECT * FROM employees WHERE name LiLei AND position dev ORDER BY age;分析: 联合索引中只是用到了name字段做等值查询[通过key_len 74可以看出因为name字段的len74]&#xff0c;在这个基础上使用了age进…

Javaweb之SpringBootWeb案例之配置优先级的详细解析

1. 配置优先级 在我们前面的课程当中&#xff0c;我们已经讲解了SpringBoot项目当中支持的三类配置文件&#xff1a; application.properties application.yml application.yaml 在SpringBoot项目当中&#xff0c;我们要想配置一个属性&#xff0c;可以通过这三种方式当中…

什么是MapReduce

1.1 MapReduce到底是什么 Hadoop MapReduce是一个软件框架&#xff0c;基于该框架能够容易地编写应用程序&#xff0c;这些应用程序能够运行在由上千个商用机器组成的大集群上&#xff0c;并以一种可靠的&#xff0c;具有容错能力的方式并行地处理上TB级别的海量数据集。这个定…

docker build基本命令

背景 我们经常会构建属于我们应用自己的镜像&#xff0c;这种情况下编写dockerfile文件不可避免&#xff0c;本文就来看一下常用的dockerfile的指令 常用的dockerfile的指令 首先我们看一下docker build的执行过程 ENV指令&#xff1a; env指令用于设置shell的环境变量&am…

DBAPI如何使用数组类型参数

DBAPI如何使用数组类型参数 需求 根据多个id去查询学生信息 API创建 在基本信息标签&#xff0c;创建参数ids &#xff0c;参数类型选择 Array<bigint> 在执行器标签&#xff0c;填写sql&#xff0c;使用in查询 select * from student where id in <foreach ope…

《Docker 简易速速上手小册》第6章 Docker 网络与安全(2024 最新版)

文章目录 6.1 Docker 网络概念6.1.1 重点基础知识6.1.2 重点案例&#xff1a;基于 Flask 的微服务6.1.3 拓展案例 1&#xff1a;容器间的直接通信6.1.4 拓展案例 2&#xff1a;跨主机容器通信 6.2 配置与管理网络6.2.1 重点基础知识6.2.2 重点案例&#xff1a;配置 Flask 应用的…

设计模式学习笔记 - 面向对象 - 7.为什么要多用组合少用继承?如何决定该用组合还是继承?

前言 在面向对象编程中&#xff0c;有一条非常经典的设计原则&#xff1a;组合优于继承&#xff0c;多用组合少用继承。 为什么不推荐使用继承&#xff1f; 组合比继承有哪些优势&#xff1f; 如何判断该用组合还是继承&#xff1f; 为什么不推荐使用继承&#xff1f; 继承…

企业微信怎么变更企业名称?

企业微信变更主体有什么作用&#xff1f;现在很多公司都用企业微信来加客户&#xff0c;有时候辛辛苦苦积累了很多客户&#xff0c;但是公司却因为各种各样的原因需要注销&#xff0c;那么就需要通过企业微信变更主体的方法&#xff0c;把企业微信绑定的公司更改为最新的。企业…

内核解读之内存管理(8)什么是page cache

文章目录 0. 文件系统的层次结构1.什么是page cache2.感观认识page cache3. Page Cache的优缺点3.1 Page Cache 的优势3.2 Page Cache 的劣势 0. 文件系统的层次结构 在了解page cache之前&#xff0c;我们先看下文件系统的层次结构。 1 VFS 层 VFS &#xff08; Virtual Fi…

Gitflow:一种依据 Git 构建的分支管理工作流程模式

文章目录 前言Gitflow 背景Gitflow 中的分支模型Gitflow 的版本号管理简单模拟 Gitflow 工作流 前言 Gitflow 工作流是一种版本控制流程&#xff0c;主要适用于较大规模的团队。这个流程在团队中进行合作时可以避免冲突&#xff0c;并能快速地完成项目&#xff0c;因此在很多软…

人工智能与机器学习行业新闻:颠覆企业运营方式的 AI 趋势

AI 推动业务转型 人工智能 (AI) 和机器学习已经在重塑各行各业的业务模式。AI 通过处理和整合数据支持战略决策的制定&#xff0c;其规模和速度远远超过了人脑。无疑&#xff0c;未来我们还将在 AI 领域取得许多重大突破&#xff0c;而拥有大量数据的行业可能会从人工智能革命…

Mac OS 下载安装与破解Typora

文章目录 下载Typora破解Typora1. 进入安装目录2. 找到并打开Lincense文件3. 修改激活状态4. 重新打开Typora 下载Typora 官网地址&#xff1a;typora官网 下载最新Mac版&#xff0c;正常安装即可 破解Typora 打开typora,可以看到由于未激活&#xff0c;提示使用期限还剩下15…

Three.js-01快速入门

1.导入three.js库 说明&#xff1a;资源在主页里面能够找到&#xff0c;如果不想使用本地的three.module.js文件&#xff0c;也可以使用在线的文件。 import * as THREE from "../three.module.js"// import * as THREE from https://unpkg.com/three/build/three.m…

学习 LangChain 的 LCEL

学习 LangChain 的 LCEL 0. 引言1. 基本示例&#xff1a;提示模型输出解析器​1-1. Prompt​1-2. Model1-3. Output parser1-4. Entire Pipeline 0. 引言 LCEL(LangChain Expression Language) 可以轻松地从基本组件构建复杂的链&#xff0c;并支持开箱即用的功能&#xff0c;…

mongoose httpserver浅析

文章目录 前言一、结构体及其功能二、函数MG_LOGmg_http_listenmg_mgr_poll question参考链接 前言 mongoose是一款基于C/C的网络库&#xff0c;可以实现TCP, UDP, HTTP, WebSocket, MQTT通讯。mongoose是的嵌入式网络程序更快、健壮&#xff0c;易于实现。 mongoose只有mong…

【网络编程】okhttp源码解析

文章目录 配置清单框架结构解析 配置清单 首先了解一下okHttp的配置清单&#xff1a; Dispatcher dispatcher &#xff1a;调度器&#xff0c;⽤于调度后台发起的⽹络请求&#xff0c;有后台总请求数和单主机总请求数的控制。List<Protocol> protocols &#xff1a;⽀持…

idea 打jar包、lib文件夹

idea目录文件 idea四层级结构 idea操作Java文件的基本单位&#xff1a;项目&#xff08;Project&#xff09;。对应四级结构 第1层级架构&#xff1a;项目&#xff08;project&#xff09; 在 IntelliJ IDEA 中Project是最顶级的结构单元&#xff0c;然后就是Module&#xf…