c/c++中define宏定义用法

发布时间 2023-08-14 14:35:12作者: 邹木木

#define定义标识符

无参宏定义

无参宏的宏名后不带参数。
其定义的一般形式为:

#define  标识符  字符串
  • 其中的“#”表示这是一条预处理命令。凡是以“#”开头的均为预处理命令。“define”为宏定义命令。“标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。

例如:

#define MAXNUM 99999

这样MAXNUM就被简单的定义为99999。

  • #define还可以用来定义关键字
#define reg register      //为 register这个关键字,创建一个简短的名字

我们如果觉得register太长了,使用不方便,我们还可以创一个简单的名字reg

上面这个reg的意思就是register

有参宏定义

C++语言允许宏带有参数。在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数。
对带参数的宏,在调用中,不仅要宏展开,而且要用实参去代换形参。
带参宏定义的一般形式为:

 #define  宏名(形参表)  字符串

在字符串中含有各个形参。在使用时调用带参宏调用的一般形式为:宏名(实参表);
例如:

#define add(x, y) (x + y)
int main()
{
    cout << "1 plus 1 is " << add(1, 1.5) << ".\n";
    //输出“1 plus 1 is 2.5.”
    system("pause");
    return(0);
}

这个“函数”定义了加法,但是该“函数”没有类型检查,有点类似模板,但没有模板安全,可以看做一个简单的模板。

注意:该“函数”定义为(a + b),在这里加括号的原因是,宏定义只是在预处理阶段做了简单的替换,如果单纯的替换为a + b时,当你使用5 * add(2, 3)时,被替换为5 * 2 + 3,值为13,而非5 * (2 + 3),值为25。

加续行符换行

在#define的宏定义的内容过长时,我们的编译器中一行放不下,我们还可以加入续行符,也就是''来进行换行

#define DEBUG_PRINT printf("file:%s\tline:%d\t \
             date:%s\ttime:%s\n" ,\
             __FILE__,__LINE__ ,    \
             __DATE__,__TIME__ ) 

那么一定要使用换行符吗?

是的,如果代码过长而需要换行时,光使用回车键的话会报错(红色的下划线)

宏定义中的条件编译

在大规模的开发过程中,特别是跨平台和系统的软件里,define最重要的功能是条件编译。

#ifdef WINDOWS
......
(#else)
......
#endif
#ifdef LINUX
......
(#else)
......
#endif

可以在编译的时候通过#define设置编译环境。

如何取消宏

//定义宏
#define [MacroName] [MacroValue]
//取消宏
#undef [MacroName]

防止重复包含头文件

由于头文件包含可以嵌套,那么C文件就有可能包含多次同一个头文件,就可能出现重复定义的问题的。
通过条件编译开关来避免重复包含(重复定义)
例如:

#ifndef __headerfileXXX__
#define __headerfileXXX__
…
文件内容
…
#endif

define替换的规则

最后我们不妨再了解一下#define替换的规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

也就是说#define宏定义会不断地检查我们所使用的地方并且不断地替换宏定义的内容,直到所有内容都被替换完

注意事项

小结及说明

  1. 宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单的代换,字符串中可以含任何字符,可以是常数,也可以是表达式,预处理程序对它不作任何检查。如有错误,只能在编译已被宏展开后的源程序时发现。

  2. 宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起置换。

  3. 宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用#undef命令。只要函数定义在#undefine之后,则函数无法使用#define的内容。

  4. 宏名在源程序中若用引号括起来,则预处理程序不对其作宏代换。

  5. 宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名。在宏展开时由预处理程序层层代换。
    例如:

     #define PI 3.1415926
     #define S PI*y*y          /* PI是已定义的宏名*/
    
  6. 习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母。

  7. 可用宏定义表示数据类型,使书写方便。
    应注意用宏定义表示数据类型和用typedef定义数据说明符的区别。

    • 宏定义只是简单的字符串代换,是在预处理完成的,
    • 而typedef是在编译时处理的,它不是作简单的代换,而是对类型说明符重新命名。被命名的标识符具有类型定义说明的功能。
      请看下面的例子:
    #define PIN1 int *
    typedef (int *) PIN2;
    

    从形式上看这两者相似, 但在实际使用中却不相同。
    下面用PIN1,PIN2说明变量时就可以看出它们的区别:
    PIN1 a,b;在宏代换后变成:
    int *a,b;
    表示a是指向整型的指针变量,而b是整型变量。
    然而:
    PIN2 a,b;
    表示a,b都是指向整型的指针变量。因为PIN2是一个类型说明符。由这个例子可见,宏定义虽然也可表示数据类型, 但毕竟是作字符代换。在使用时要分外小心,以避出错。

  8. 对“输出格式”作宏定义,可以减少书写麻烦。
    例如:

    #define P printf
    #define D "%d\n"
    #define F "%f\n"
    main(){
      int a=5, c=8, e=11;
      float b=3.8, d=9.7, f=21.08;
      P(D F,a,b);
      P(D F,c,d);
      P(D F,e,f);
    }
    
  9. 带参宏定义中,宏名和形参表之间不能有空格出现。

  10. 在带参宏定义中,形式参数不分配内存单元,因此不必作类型定义。而宏调用中的实参有具体的值。要用它们去代换形参,因此必须作类型说明。这是与函数中的情况不同的。在函数中,形参和实参是两个不同的量,各有自己的作用域,调用时要把实参值赋予形参,进行“值传递”。而在带参宏中,只是符号代换,不存在值传递的问题。

  11. 在宏定义中的形参是标识符,而宏调用中的实参可以是表达式。

#和##

(1)#

这个#并不是#define中的#,而是另一种意义的操作符,把一个宏参数变成对应的字符串。
它可以把语言符号转化程字符串。例如,如果x是一个宏参量,那么#x可以把参数名转化成相应的字符串。该过程称为字符串化。

例如:

#incldue <stdio.h>
#define PSQR(x) printf("the square of" #x "is %d.\n",(x)*(x))
int main(void)
{
    int y =4;
    PSQR(y);
    //输出:the square of y is 16.
    PSQR(2+4);
    //输出:the square of 2+4 is 36.
    return 0;
}

(2)##

#include <stdio.h>
#define XNAME(n) x##n
#define PXN(n) printf("x"#n" = %d\n",x##n)
int main(void)
{
    int XNAME(1)=12;//int x1=12;
    PXN(1);//printf("x1 = %d\n", x1);
    //输出:x1=12
    return 0;
}

带有副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。

具体是什么意思,我们先从“副作用”这个词来解释

int main()
{
	//代码1
	int a = 10;
	int b = a + 1;//a=10,b=11

	//代码2
	int a = 10;
	int b = ++a;//a=11,b=11
	//代码2就是有副作用的
	return 0;
}

我们可以看到上述代码中代码1中的a值没有变,而代码2中的a值变了,我们就说代码2是有副作用的。

那如果宏里的副作用会影响到我们什么呢?

    #define MAX(X,Y)  ((X)>(Y)?(X):(Y))
    int main()
    {
        int a=3;
        int b=5;
        int c=MAX(a++,b++);
        //int c=((a++)>(b++)?(a++):(b++));
        printf("%d\n",a);
        printf("%d\n",b);
        printf("%d\n",c);
    }

我们还是用宏定义比大小来举个例子;

看到上面的代码,c就是比大小中的较大值,那c的值到底是多少呢?a和b的值又是多少呢?

我们上面讲过,预处理时我们会将参数原封不动的传入到宏里面去,所以c就成了这个样子

c=((a++)>(b++)?(a++):(b++));

那我们依次对其进行运算,第一次a++结果是4,第一次b++结果是6,第二次a++结果是5,第二次b++结果是7

但是按照我们直观的想法,笼统的,不加深究的眼光去看这串代码,我们依旧会认为是3和5去比大小,我们输出结果

image
我们就会看到与我们预期的结果有些出入。

那我们看到这样的结果我们不妨写一个函数,来与宏定义来形成的对比
image
可以看到结果和宏定义的结果就不同了,因为函数传参时是计算好结果再传参的。

那么这又引出一个问题,函数和宏到底哪个好用呢?

函数和宏的对比

当我们求两个数的较大值时既能写成宏的形式也能写成函数的形式
image
我们可以对比一下以上两种方式求较大值

其实对于当前两个数比大小的代码还是宏定义是更优解,原因有二:

  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。
    所以宏比函数在程序的规模和速度方面更胜一筹。
    image
    当我们用vs调试代码时,我们观察反汇编代码时发现在进入了函数时还有许许多多的指令,在经过这些指令时才能达到下方比大小的结果,在下方比大小时还需要很多的指令,执行这些指令都是需要花费时间的,所以函数的方式比较浪费时间。

因为在调用函数时有三个步骤会影响时间

  • 函数调用前的准备(传参、函数栈帧空间的维护)

  • 主要的运算

  • 函数返回和返回值的处理,和最后函数栈帧的销毁

但是我们反观宏在处理这样的代码时
image
我们就会看到在传入参数时直接就执行的是打印函数,因为上面讲过,宏可以直接在我们所需要时将代码替换掉,没有函数那么多的开销。

2.更为重要的是函数的参数必须声明为特定的类型。
所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以
用于>来比较的类型。
宏是类型无关的,宏是更加灵活的,这也是重要的原因。

当然宏也有自己劣势的地方

  • 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。也就是说如果我们在宏定义的内容中定义的内容有十几二十几行的代码时,我们每用一次宏就会增加我们代码的长度。

  • 宏是没有办法调试的。在预处理阶段时,宏就会将内容全部替换到代码中所运用宏的地方,当我们在调试时可能会和我们所看到的代码有所不同。

  • 宏由于类型无关,也就不够严谨。

  • 宏可能会带来运算符优先级的问题,导致程容易出现错。上面我们说过,如果在一个有宏的表达式中,表达式对宏的运算结果还有运算时,可能就会出现优先级的问题从而导致宏的运算结果错误。

宏对比函数的还有一个优点就是可以将类型当成参数。

我们在使用函数时从来都没见过用函数的参数类型给函数传参。

用代码举例:
image

我们在动态内存管理中学过,需要开辟10个整形类型的空间需要如上代码进行处理,并且对其进行判断。

但是我想让他的写法简单一点行不行呢?

就比如写成以下形式

MALLOC(10,int)

这样的代码要开辟十个类型的整形的空间要简单得多。 那怎么使用呢?

    #define MALLOC(num,type) (type*)malloc(num *sizeof(type))
    int main()
    {
        int* p2 = MALLOC(10, int);
        if (p2 == NULL)
        {
            //...
        }
        return 0;
    }

这里用宏定义来解决这样的问题

我们定义一个名为MALLOC的宏,并且再定义两个参数,其中一个既然要以类型名,我们不妨就令这个参数为type;在后面参数的内容中就用malloc函数开辟个数一个类型的大小,最后再用(type)强制将其类型转化为type类型,这样就完成了MALLOC的宏定义,使用时简单方便,一劳永逸。

相信通过这些可以看出函数和#define 宏定义的区别,使用时可以根据不同的环境使用。