c++宏定义详解

发生在编译阶段
预定义的宏
与预处理指令搭配
应用场景
宏函数的优点

宏的语法

定义

通常宏定义的格式为:#define 标识符 字符串

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

使用实例:

double perimeter = diameter * PI;

  • C语言中的NULL就是一个语言已经预定义的宏。预定义指的是你不必亲自定义,编译器在编译时,已经提前定义好了。
  • 宏经常和预处理指令#ifdef配合来控制模块的导出导入符号。

参数

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

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

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

int ret = MUL(2 + 3, 4);
//展开为
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);
//【展开为:】
printf("got a number %d",__VA_ARGS__);

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

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

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

__VA_ARGS__是什么?【待整理】

多行的宏

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

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

类型含义
FILEconst char *当前所编译文件名称(绝对路径)
LINEint当前所在的行号
FUNCTIONconst char *当前所在的函数名称
DATEconst char *当前的日期
TIMEconst char *当前的时间

宏的调试

宏不支持在运行时调试,但如果宏太过于复杂的话,出错也是难免的,因此,可以利用宏自身的特性把宏展开后的内容打印出来,来方便我们查错。
这里有一个技术前提,如果想要在编译时打印一些信息,可以使用如下预处理指令:

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

#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");

减少重复代码

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

// 类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);

展开为:

int value = 2 + 3 * 4 + 5;

导致错误,这种问题的修改策略就是在宏定义时加上括号,包括参数都加上括号。即

#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)这种无用的循环并进行优化,所以使用这种方法也不会导致程序的性能降低。

和函数的区别

它跟函数的区别有以下几点:

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

为什么要用宏函数?

  • 宏函数虽然在处理复杂的函数(例如递归函数)时宏会降低代码的执行效率,【为什么】
  • 但是对于逻辑简单的函数来说,准确的使用宏函数往往能提高程序的执行效率,因为在主函数中调用普通函数的时候需要进行入栈跟出栈操作,而宏函数只是进行简单的直接替换,所以能对代码的执行效率有一定的提升。
  • 当然也有不好的一面,使用了宏函数跟使用普通函数比较起来生成的目标文件会比较大,因为宏函数的直接插入导致在代码中多了一份拷贝。

什么是宏函数?

  • 宏函数其实就是上面的字符串写成一个完整的函数体。
#define MIN(x, y) ((x < y) ? x : y)
  • 宏函数和普通函数的对比
#include <iostream>
// 宏函数
// 写法1 (调用该宏前必须存在min变量)
#define MYFUNC(x, y)\
{\
    min = x < y ? x : y;\
}\

// 写法2
#define MYFUNC2(x, y, min)\
{\
    min = x < y ? x : y;\
}\

// 普通函数
void MyFunc(const int x, const int y, int &min)
{
    // 通过引用获取x跟y的最小值
    min = x < y ? x : y;   
}

int main(void)
{
    int x = 1, y = 2, min = 0;
    MyFunc(x, y, min);
    std::cout << "MyFunc: " << min << std::endl;

    x = 3, y = 4;
    MYFUNC(x, y);
    std::cout << "MYFUNC: " << min << std::endl;

    x = 5, y = 6;
    MYFUNC2(x, y, min);
    std::cout << "MYFUNC2: " << min << std::endl; 

    return 0;
}

输出结果:
MyFunc: 1
MYFUNC: 3
MYFUNC2: 5

因为宏的参数替换是直接替换的,所以写法一是可行的。宏函数中虽然没有声明min,但只要再此之前有min的定义也可执行,因为从字符的角度看,这是完整的函数执行流程描述。
上面的对比还无法看出宏函数的多少优势,那接下来我们进行对函数进行改造,将函数作为参数传入到我们的参数里面。

#include <iostream>
#include <string>

// 写法1
// 调用该宏前必须存在min变量
#define MYFUNC(x, y, fun1)\
{\
    min = x < y ? x : y;\
    fun1("MYFUNC", x, y);\
}\

// 写法2
#define MYFUNC2(x, y, min)\
{\
    min = x < y ? x : y;\
    printfun("MYFUNC2", x, y);\
}\

typedef void(*Fun1)(const std::string, const int, const int);
void printfun(const std::string str, const int x, const int y)
{
    // 用于输出x+y的值
    int sum = x + y;
    std::cout << str << " sum is : " << sum << std::endl;
}

// 普通函数
void MyFunc(const int x, const int y, int &min, Fun1 fun1)
{
    // 通过引用获取x跟y的最小值
    min = x < y ? x : y;   

    fun1("MyFunc", x, y);
}


int main(void)
{
    int x = 1, y = 2, min = 0;
    MyFunc(x, y, min, printfun);
    std::cout << "MyFunc: " << min << std::endl;

    x = 3, y = 4;
    MYFUNC(x, y, printfun);
    std::cout << "MYFUNC: " << min << std::endl;

    x = 5, y = 6;
    MYFUNC2(x, y, min);
    std::cout << "MYFUNC2: " << min << std::endl; 

    return 0;
}

运行结果:
MyFunc sum is : 3
MyFunc: 1
MYFUNC sum is : 7
MYFUNC: 3
MYFUNC2 sum is : 11
MYFUNC2: 5

  • 因为要将函数作为参数进行传递,所以用到了函数模板Fun1,可以发现如果使用宏函数的话是不需要使用函数模板的,
  • 但是使用普通函数的话是必须使用的,因为这里必须将函数作为参数进行传递,而宏函数因为是直接替换到代码里面的所以这里可以直接使用,
  • 当然也可以将函数传到宏的参数里,但是宏的参数是不需要参数类型的,所以这里使用宏函数的话可以帮我们减少挺多的工作量,与此同时还能提高程序的效率。


  • 10
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值