C/C++预处理指令和宏定义#define及 do while(0)

本文详细介绍了C/C++中的预处理指令,包括条件编译命令和宏定义。特别是宏定义,通过示例展示了其在代码效率和类型无关性方面的优势。此外,文章探讨了do while(0)在编写复杂宏时如何避免错误,保持代码一致性的关键作用。最后,提到了do while(0)在避免使用goto和处理编译警告等方面的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文主要写了预处理指定和#define 宏替换及宏函数,以及为什么会用到 do/while(0); 读者需要那部分知识可以直接点击目录里面的链接

本文参考:https://www.cnblogs.com/flowingwind/p/8304668.html
https://www.cnblogs.com/bytebee/p/8205707.html
https://www.cnblogs.com/wuweierzhi/p/11591999.html

目录

一、预编译指令

二、宏定义

三、do while(0);


一、预编译指令

常见的预编译指令
命令命令效果
#空指令无任何效果
#include包含一个源代码文件
#define定义宏
#undef取消已定义的宏
#if如果给定条件为真,则编译下面代码
#ifdef如果宏已经定义,则编译下面代码
#ifndef如果宏没有定义,则编译下面代码
#elif如果前面的#if给定条件不为真,当前条件为真,则编译下面代码
#endif结束一个#if……#else条件编译块
#error停止编译并显示错误信息

   条件编译命令最常见的形式为:

  • #ifdef 标识符 
  • 程序段1 
  • #else 
  • 程序段2 
  • #endif

例:

#ifndef bool
#define ture 1
#define false 0
#endif

在早期vc中bool变量用1,0表示,即可以这么定义,保证程序的兼容性

在头文件中使用#ifdef和#ifndef是非常重要的,可以防止双重定义的错误。

//main.cpp文件

#include "cput.h"
#include "put.h"
int main()
{
    cput();
    put();
    cout << "Hello World!" << endl;
    return 0;
}
//cput.h 头文件

#include <iostream>
using namespace std;
int cput()
{
    cout << "Hello World!" << endl;
    return 0;
}
//put.h头文件

#include "cput.h"
int put()
{
    cput();
    return 0;
}

编译出错;在main.cpp中两次包含了cput.h

尝试模拟还原编译过程;

当编译器编译main.cpp时

//预编译先将头文件展开加载到main.cpp文件中

//展开#include "cput.h"内容
#include <iostream>
using namespace std;
int cput()
{
    cout << "Hello World!" << endl;
    return 0;
}

//展开#include "put.h"内容
//put.h包含了cput.h先展开
#include <iostream>
using namespace std;
int cput()
{
    cout << "Hello World!" << endl;
    return 0;
}
int put()
{
    cput();
    return 0;
}

int main()
{
    cput();
    put();
    cout << "Hello World!" << endl;
    return 0;
}

很明显合并展开后的代码,定义了两次cput()函数;

如果将cput.h改成下面形式:

#ifndef _CPUT_H_
#define _CPUT_H_
#include <iostream>
using namespace std;
int cput()
{
    cout << "Hello World!" << endl;
    return 0;
}
#endif

当编译器编译main.cpp时合并后的main.cpp文件将会是这样的:

#ifndef _CPUT_H_
#define _CPUT_H_
#include <iostream>
using namespace std;
int cput()
{
    cout << "Hello World!" << endl;
    return 0;
}
#endif

#ifndef _CPUT_H_
#define _CPUT_H_
#include <iostream>
using namespace std;
int cput()
{
    cout << "Hello World!" << endl;
    return 0;
}
#endif
int put()
{
    cput();
    return 0;
}

int main()
{
    cput();
    put();
    cout << "Hello World!" << endl;
    return 0;
}

 这次编译通过运行成功;因为在展开put.h中包含的cput.h,会不生效,前面已经定义了宏_CPUT_H_

 

二、宏定义

#define 宏定义,简单理解就是直接替换。

宏定义可以帮助我们防止出错,提高代码的可移植性和可读性等。
  在软件开发过程中,经常有一些常用或者通用的功能或者代码段,这些功能既可以写成函数,也可以封装成为宏定义。那么究竟是用函数好,还是宏定义好?这就要求我们对二者进行合理的取舍。
  我们来看一个例子,比较两个数或者表达式大小,首先我们把它写成宏定义:

#define MAX(a, b) ((a)>(b) ? (a):(b))

其次,把它用函数来实现:

int max(int a, int b)
{
  return ((a > b) ? a : b)
}

很显然,我们不会选择用函数来完成这个任务,原因有两个:

1.函数调用会带来额外的开销,它需要开辟一片栈空间,记录返回地址,将形参压栈,从函数返回还要释放堆栈。这种开销不仅会降低代码效率,而且代码量也会大大增加,而使用宏定义则在代码规模和速度方面都比函数更胜一筹;

2.函数的参数必须被声明为一种特定的类型,所以它只能在类型合适的表达式上使用,我们如果要比较两个浮点型的大小,就不得不再写一个专门针对浮点型的比较函数。反之,上面的那个宏定义可以用于整形、长整形、单浮点型、双浮点型以及其他任何可以用“>”操作符比较值大小的类型,也就是说,宏是与类型无关的。

和使用函数相比,使用宏的不利之处在于每次使用宏时,一份宏定义代码的拷贝都会插入到程序中。除非宏非常短,否则使用宏会大幅度增加程序的长度。

还有一些任务根本无法用函数实现,但是用宏定义却很好实现。比如参数类型没法作为参数传递给函数,但是可以把参数类型传递给带参的宏。看下面的例子:

/* \ 表示换行符*/
#define MALLOC(n, type) \ 
    ((type *) malloc((n) * sizeof(type))   \*强制类型转换*\

利用这个宏,我们就可以为任何类型分配一段我们指定的空间大小,并返回指向这段空间的指针。我们可以观察一下这个宏确切的工作过程:

int *ptr;
  ptr = MALLOC(5, int);

将这宏展开以后的结果:

ptr = (int *)malloc((5) * sizeof(int));

这个例子是宏定义的经典应用之一,完成了函数不能完成的功能,但是宏定义也不能滥用,通常,如果相同的代码需要出现在程序的几个地方,更好的方法是把它实现为一个函数。

example:

define的单行定义:

#define maxi(a,b) ((a>b) ? a:b)

define的多行定义

define可以替代多行的代码,例如MFC中的宏定义:

#define MACRO(arg1, arg2) do{\
                             \
    stmt1;                   \
    stmt2;                   \
                             \
}while(0)  

宏定义写出swap(x,y)交换函数

#define swap(x, y)\
x = x + y;        \
y = x - y;        \
x = x - y;

zigbee里多行define有如下例子

#define FillAndSendTxOptions(TRANSSEQ, ADDR, ID, LEN, TxO ){ \
afStatus_t stat;                                             \
ZDP_TxOptions = (TxO);                                       \
stat = fillAndSend( (TRANSSEQ), (ADDR), (ID), (LEN) );       \
ZDP_TxOptions = AF_TX_OPTIONS_NONE;                          \
return stat;                                                 \
}

 

三、do while(0);

1.帮助定义复杂的宏以避免错误

举例来说,假设你需要定义这样一个宏:

#define DOSOMETHING(x) foo1(x); foo2(x)

有如下调用语句

DOSOMETHING(value);

这时将宏展开为:

fool(value); fool(value);

但是如果你在调用的时候这么写:

if(a>0)
    DOSOMETHING(value);

因为宏在预处理的时候会直接被展开,你实际上写的代码是这个样子的:

if(a>0)
    foo1(value);
foo2(value);

这就出现了问题,因为无论a是否大于0,foo2(value)都会被执行,导致程序出错。
那么仅仅使用{}将foo1(x)和foo2(x)包起来行么?比如:

#define DOSOMETHING(x) { foo1(x); foo2(x); }

我们在写代码的时候都习惯在语句右面加上分号,如果在宏中使用{},代码编译展开后宏就相当于这样写了:“{...};”,展开后就是这个样子:

if(a>0)
{
    foo1();foo2();
};

很明显,这是一个语法错误(大括号后多了一个分号)。
现在的编译器会自动检测自动忽略分号,不会报错,但是我们还是希望能跑在老的编译器上。

在没有do/while(0)的情况下,在所有可能情况下,期望我们写的多语句宏总能有正确的表现几乎是不可能的。
如果我们使用do{...}while(0)来定义宏,即:

#define DOSOMETHING(x) do{foo1(x); foo2(x);} while(0)  //注意这里没有分号

对于上面的if语句,将会被扩展为:

if(a > 0)
    do{foo1(x); foo2(x);} while(0);

这样,宏被展开后,上面的调用语句才会保留初始的语义。do能确保大括号里的逻辑能被执行,而while(0)能确保该逻辑只被执行一次,就像没有循环语句一样。

总结:在Linux和其它代码库里的,很多宏实现都使用do/while(0)来包裹他们的逻辑,这样不管在调用代码中怎么使用分号和大括号,而该宏总能确保其行为是一致的。
cocos2d-x中大量使用了这种宏定义:

#define CC_SAFE_DELETE(p) do { if(p) { delete (p); (p) = 0; } } while(0)

2. 避免使用goto控制程序流(和宏定义无关,属于代码优化)

在一些函数中,我们可能需要在return语句之前做一些清理工作,比如释放在函数开始处由malloc申请的内存空间,使用goto总是一种简单的方法:

int foo()
{
    somestruct *ptr = malloc(...);

    dosomething...;
    if(error)
        goto END;
    dosomething...;
    if(error)
        goto END;
    dosomething...;
END:
    free(ptr);
    return 0;
}

但由于goto不符合软件工程的结构化,而且有可能使得代码难懂,所以很多人都不倡导使用,这个时候我们可以使用do{...}while(0)来做同样的事情:

int foo()
{
    somestruct *ptr = malloc(...);
    do
    {
        dosomething...;
        if(error)
            break;
        dosomething...;
        if(error)
            break;
        dosomething...;
    }
    while(0);      //注意这里有;分号

    free(ptr);
    return 0;
}

这里将函数主体部分使用do{...}while(0)包含起来,使用break来代替goto,后续的清理工作在while之后,现在既能达到同样的效果,而且代码的可读性、可维护性都要比上面的goto代码好的多了。

我经常使用这个种技能在Lua里,Lua不支持do{...}while(0)语法,但是Lua有一种类似的语法repeat...until,伪代码如下:

repeat
  dosomething...
  if error then
    break;
  end
  dosomething...;
  if error then
    break;
  end      
  dosomething...;
until (1);
print("break repeat");

这样和do{...}while(0)一样,也保证了只执行一次,可以用break调出循环。

3. 避免由宏引起的警告

内核中由于不同架构的限制,很多时候会用到空宏,。在编译的时候,这些空宏会给出warning,为了避免这样的warning,我们可以使用do{...}while(0)来定义空宏:

#define EMPTYMICRO do{}while(0)

这种情况不太常见,因为有很多编译器,已经支持空宏。

4. 定义单一的函数块来完成复杂的操作

如果你有一个复杂的函数,变量很多,而且你不想要增加新的函数,可以使用do{...}while(0),将你的代码写在里面,里面可以定义变量而不用考虑变量名会同函数之前或者之后的重复。
但是我不建议这样做,尽量声明不同的变量名,以便于后续开发人员阅读。

int key;
string value;
int func()
{
    int key = GetKey();
    string value = GetValue();
    dosomething for key,value;
    do{
        int key;string value;
        dosomething for this key,value;
    }while(0);    
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值