【C语言】预处理详解:#define的各种使用方法

目录


1.#define定义标识符

#define定义标识符的用法非常简单

语法:
#define name stuff

name可以由自己来命名,尽量取一些有意义的名字

stuff是名字所对应的内容

举几个例子:

1.1赋值

1.#define MAX 1000

第一个意思就是给MAX赋值为1000

可以用代码使用宏定义,同样也可以定义数组的大小

#define MAX 1000
#include<stdio.h>
int main()
{
	printf("%d\n", MAX);
    int arr[MAX];
	return 0;
}

 我们在编译器中可以看到左边的MAX都被替换成了1000,也就是说在以后的代码中可以用到1000的地方,都可以使用MAX来表示。

1.2   定义关键字

#define还可以用来定义关键字

#define reg register      //为 register这个关键字,创建一个简短的名字

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

上面这个reg的意思就是register

可以看到 reg被替换成了register。

1.3用更形象的符号来替换一种实现

#define do_forever for(;;)

我们都知道如果for循环中什么条件都没有那就是死循环

所以如果我们想要写出死循环的话也可以使用这种方法

 可以看到do_forever就代替了for循环变成了死循环,这也是一种用法

1.4   加续行符换行

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

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

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

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

 希望大家可以多都尝试。

1.5#define定义宏

我们前面所讲到的#define所定义的都是一些常量和符号,而没有参数, 而接下来要说的是#define定义宏,可以像函数一样解决问题,#define 机制包括了一个规定,允许把参数替换到文本中,

下面是宏的申明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

下面用代简单举例

#define SQUARE(x) x*x
int main()
{
    printf("%d\n",SQUARE(8));
    return 0;
}

 可以看到我用#define 定义了一个乘方公式,其中参数是x,他的内容是x*x,我们不妨将这段代码预处理看一看他的本质

 可以看到定义的SQUARE内容变成了8*8

当我们让这个代码运行起来的时候我们也能看到它输出了正确的结果。

但是我们还需要注意的是,使用时还需要在宏定义的内容中带上括号,这才能保证有些地方不会出错。下面继续用代码举例;

#define SQUARE(x) x*x
int main()
{
    printf("%d\n",SQUARE(1+7));
    return 0;
}

如果我们将上面的参数8换成了1+7,结果会是怎么样呢?

 我们会发现这样会把参数的内容原原本本的、不加计算的替换到宏的体内去,原本的内容被替换成了1+7*1+7,运算顺序就出了问题,所以运算结果也出了问题

 果然不出所料结果是15

所以我们在使用宏定义时不要吝啬我们的括号,我们只需在宏定义时给内容加上括号,就可以避免这样的问题

 我们给(x)*(x)的结果也带上了括号变成了((x)*(x)),是因为如果我们还需要对这个宏进行其他运算的话,也不会影响这个宏的结果。

同样的我们也可以使用宏来求两个数较大值 

       

同样的,我们不要吝啬自己的括号,要慷慨且大方,这样才能保证自己的代码不会出错 

所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中
的操作符或邻近操作符之间不可预料的相互作用。

1.6  #define替换的规则

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

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

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

注意事项

我们在使用#define定义后不能添加分号,很多人敲代码时都会习惯性的在后面加上分号,当然这种习惯不能用在宏定义中,使用时请注意细节问题。

2.#和##

这个#并不是#define中的#,而是另一种意义的操作符,把一个宏参数变成对应的字符串。

关于它的作用我们直接上代码演示

int main()
{
    int a=10;
    printf("the value of a is %d\n",a);

    int b=20;
    printf("the value of b is %d\n",b);

    float f=3.14f;
    printf("the value of a is %f\n",f);
}

再这样的代码中我们会发现出现了许多有规律的,繁荣的相同的代码,那怎么样才能使这些代码能够简洁的使用呢?很多人会想到函数,但是我们需要注意的是如果使用函数的话,代码中的参数是不断变化的,有int类型也有float类型,我们无法确定,所以不能用函数来解决这样的问题。

再这之前我们还需要了解一个关于字符串的特点

可以看到第一种方式是我们平时使用的输出方式,将输出内容放在打印函数的双引号内即可被输出;

那第二种方式也可以用来输出内容,可以将使用双引号引用我们想打印的内容,结果和第一种是相同的,这也是字符串的一个特点。

我们现在知道了这个特征,我们就可以对上面那段冗余的代码进行操作了,直接上代码

#define print_format(num,format) \
            printf("the value of "#num" is "format,num)
int main()
{
    int a=10;
    print_format(a,"%d\n");

    int b=20;
    print_format(b,"%d\n");

    float f=3.14f;
    print_format(f,"%f\n"); 
    return 0;
}

 首先我们先用#define定义一个宏,这个宏的名字叫做print_format;

其次我们想改变打印函数中的两个变量,我们不妨给我们定义的宏也加入两个参数,一个参数叫num,一个参数叫format;

因为要多次打印形如the value of num is format的函数,我们不妨给这个宏的内容写成如上代码:

我们根据上面字符串的特点都知道了其实字符串可以分开打印,所以我们将”the value of num is format”中要改成a,b,f的地方用num来替换,将修改数据的地方,比如%d,%f这些的地方用format这个参数来替换;

最后我们只需要将”the value of num is format”中,给num前加上#即可,还需要再这个宏定义的内容的最后来用参数名来声明。

 这样我们所得到的结果就和上面的一样了,根本不要这么冗长的打印函数也能完成规范的操作,这样就是关于#的简单的用法。

接下来是##

##可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。

那具体什么意思呢?上代码给大家演示

int henghengaaa=114514;
#define CAT(x,y) x##y

int main()
{
    printf("%d\n",CAT(hengheng,aaa));
    return 0;
}

我们定义一个宏,将宏内容使用##符号,意思就是将x和y的内容连接在一起

ok我们输出结果 

可以看到我们通过#define 的宏定义将字符串的内容连接在了一起,并且成功的输出了我们预期中的结果。

当然我们合成的前提是我们合成的符号必须是可以用的,如果合成出来的字符串没有出现过,那也不能正常使用。

3.带有副作用的宏参数

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

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

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去比大小,我们输出结果

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

那我们看到这样的结果我们不妨写一个函数,来与宏定义来形成的对比

 可以看到结果和宏定义的结果就不同了,因为函数传参时是计算好结果再传参的。

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

4.函数和宏的对比

当我们求两个数的较大值时既能写成宏的形式也能写成函数的形式

 

 我们可以对比一下以上两种方式求较大值

其实对于当前两个数比大小的代码还是宏定义是更优解,原因有二:
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。
所以宏比函数在程序的规模和速度方面更胜一筹。

当我们用vs调试代码时,我们观察反汇编代码时发现在进入了函数时还有许许多多的指令,在经过这些指令时才能达到下方比大小的结果,在下方比大小时还需要很多的指令,执行这些指令都是需要花费时间的,所以函数的方式比较浪费时间。

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

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

②主要的运算

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

但是我们反观宏在处理这样的代码时

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


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

宏是类型无关的,宏是更加灵活的,这也是重要的原因。

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

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

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

③宏由于类型无关,也就不够严谨。
④宏可能会带来运算符优先级的问题,导致程容易出现错。上面我们说过,如果在一个有宏的表达式中,表达式对宏的运算结果还有运算时,可能就会出现优先级的问题从而导致宏的运算结果错误。

 

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

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

用代码举例:

 

我们在动态内存管理中学过,需要开辟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 宏定义的区别,使用时可以根据不同的环境使用。

以上就是本篇文章的所有内容,如果对你有所帮助,还请三联支持,感谢您的阅读。

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
乘风的头像乘风管理团队
上一篇 2023年12月19日
下一篇 2023年12月19日

相关推荐