【C语言】什么是宏定义?(#define详解)

简介: 【C语言】什么是宏定义?(#define详解)

一.什么是宏定义

在我们看球赛时,常常会留意到许多球星,比如:梅西,姆巴佩,乔丹,科比等等...,但我们也知道,"梅西","乔丹"等这类称呼并不是他们的本名,而是国内的人们为了方便称呼他们而起的昵称.

如梅西的名字实际上是:Lionel Andrés Messi Cuccitini(利昂内尔·安德烈斯·梅西·库奇蒂尼),但在国内,你只需要和对方说:"梅西",对方便知道你说的是那个Lionel Andrés Messi Cuccitini的"梅西".

这是因为,用"梅西"来代替"Lionel Andrés Messi Cuccitini"已经是国内人们约定俗成的观念了,而这样类似的用"替换"的方式使用一个简短的名称来代称一个繁杂的名称,在C语言中,我们称之为------宏定义(#define).

宏定义在C语言源程序中允许用一个标识符来表示一个字符串,称为“宏” ,被定义为“宏”的标识符称为“宏名”.

如:

#define 梅西 Lionel Andrés Messi Cuccitini

以上就是一个宏定义,该定义是用"梅西"来表示"Lionel Andrés Messi Cuccitini"

其中,"梅西"这个标识符被称为宏名.

而Lionel Andrés Messi Cuccitini则是被表示的"字符串",这个"字符串"可以是常数,表达式,格式串等等.

在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换,这称为“宏代换”或“宏展开”.

宏定义是由源程序中的宏定义命令完成的,宏代换是由预处理程序自动完成的.

编译器会在编译期间对所有的常量表达式(只包含常量的表达式)求值,预处理器不做计算,不对表达式求值,它只进行替换.

C程序运行过程图示


二.宏定义的组成

每行#define(逻辑行)都由3部分组成:

📌第1部分

#define指令本身.

(在C语言中凡是以“#”开头的均为预处理命令)


📌第2部分

选定的缩写,也称为宏.

有些宏代表值,这些宏被称为类对象宏(object-like macro),如下例:

类对象宏中不接收参数,只是根据宏定义做简单的字符串替换操作.

C语言还有类函数宏(function-like macro),如下例:

类函数宏不仅进行简单的字符串替换,而且还要包含参数的替换.

tips:宏的名称中不允许有空格,而且必须遵守C变量的命名规则:只能使用字符,数字和下划线( _ )字符,而且首字符不能是数字.


📌第3部分

(指令行的其余部分)称为替换列表或替换体.

一旦预处理器在程序中找到宏的示实例后,就会用替换体代替该宏.

从宏变成最终替换文本的过程称为宏展开.

注意,可以在#define行使用标准C注释.每条注释在预处理后都会被一个空格代替.

当然,宏定义还可以包含其他宏(有一些编译器不支持这种嵌套功能),比如:

#define X 3
#define Y 5
 
#define MAX(X,Y) X>Y?X:Y
 
int main()
{
  printf("%d", MAX(X, Y));
 
  return 0;
}

如上程序,宏定义MAX中包含了宏定义X和Y,vs2022中运行结果如下:

可见,宏定义是允许嵌套调用的.

一般而言,预处理器发现程序中的宏后,会用宏等价的替换文本进行替换,如果替换的字符串中还包含宏,则继续替换这些宏.

唯一例外的是双引号中的宏,如:

这时因为第二个宏X被双引号引起来了,导致其不被编译器识别为宏,而识别为一个没有特殊含义的字符串了.


三.宏定义的应用

🎏类对象宏

宏定义中的类对象宏的应用场景大致分为以下几种:

首先,对于绝大部分数字常量,我们应该使用宏定义来表示它们.

如果在算式中用宏定义代替数字,常量名能更清楚的表达该数字的含义,如:

#define PI 3.14  /*表示圆周率常量*/
 
int main()
{
  int r = 2;
  double area = 0;
  area = r * r * PI;   /*计算圆的面积area*/
 
  return 0;
}

其次,如果是表示数组大小的数字,用符号常量后更容易改变数组的大小和循环次数,如:

#define ROW 5
#define COL 5
 
int main()
{
    int arr[ROW][COL];//使用宏定义创建一个二维数组
 
    return 0;
}

最后,如果数字是系统代码(如,EOF),用宏定义表示的代码更容易移植(只需要改变EOF的定义).

#define EOF -1
#define True 1
#define False 0

宏定义有价值的特性包括:助记,易更改,可移植.


🎏类函数宏

📌求两个数中的较大值

在C语言初学阶段,我们学习过怎样编写一个函数求两个数中的较大值,如:

int Move_Max(int x, int y)
{
  return x>y?x:y;
}
 
int main()
{
  int x = 3;
  int y = 5;
  int max = 0;
 
  max=Move_Max(x, y);
    printf("%d",max);
 
  return 0;
}

运行程序,得到结果:

在我们学习了宏定义后,我们可以借助宏定义和三目运算符来完成这一功能,如:

#define MAX(X,Y) X>Y?X:Y
 
int main()
{
  int x = 3;
  int y = 5;
  int max = 0;
 
  max = MAX(x, y);
  printf("%d", max);
 
  return 0;
}

该程序运行时,第9行代码会被替换成:

max = x>y?x:y ;

运行程序,得到结果:


📌求一个数的平方值

同样的,我们学习过怎样编写一个函数求一个数的平方值,如:

int Move_Square(int x)
{
  return x * x;
}
 
int main()
{
  int x = 3;
  int square = 0;
 
  square = Move_Square(x);
  printf("%d", square);
 
  return 0;
}

运行程序,得到结果:

再试试使用宏定义来实现这一功能:

#define Square(X) X*X
 
int main()
{
  int x = 3;
  int square = 0;
 
  square = Square(x);
  printf("%d", square);
 
  return 0;
}


该程序运行时,第8行代码会被替换成:

square = x*x ;

运行程序,得到结果:


📌求结构体成员偏移量

C语言中有这样一个库宏offsetof:

offsetof是一个宏,在C语言中用于获取结构体成员相对于结构体起始地址的偏移量(以字节为单位)。

包含在<stddef.h>头文件中

通过指定结构体类型成员名称作为参数,offsetof宏会返回该成员在结构体中的偏移量

(不懂如何计算结构体成员偏移量的可以移步我的这篇博客:【C语言】结构体的大小是如何计算的?(结构体对齐))

我们在vs2022中测试一下该宏:

我们接下来使用宏定义模仿实现一下这个库宏:

#include<stdio.h>
#define MY_OFFSETOF(type,member)  (size_t)&(((type*)0)->member)
 
struct stu {
    char ch;
    int sz;
    short age;
};
 
int main()
{
 
    printf("%d\n", MY_OFFSETOF(struct stu, ch));
    printf("%d\n", MY_OFFSETOF(struct stu, sz));
    printf("%d\n", MY_OFFSETOF(struct stu, age));
 
    return 0;
}

测试运行,得到结果:

有关更多库宏offsetof的详解可以移步我的另一篇博客: 【C语言】库宏offsetof详解


四.宏定义陷阱

即便使用宏定义看似简便,高效,但宏定义中同样存在一些陷阱,接下来我们将会以三目运算符求两个数中的较小值为例,向大家展示宏定义中可能一不小心就被大家忽略的陷阱:

📌小白写法

#define MIN(A,B) A < B ? A : B

然后我们使用这个宏定义:

int a = MIN(1, 2);

该代码在预处理结束后会被替换为:

int a = 1 < 2 ? 1 : 2;
int a = 1;

该定义的问题:

当我们需要这样使用这个宏定义时:

int a = 2 * MIN(3, 4);

我们以为得到的结果会是:

int a = 2 * 3;
int a=6;

但实际上我们得到的结果是:

int a = 2 * 3 < 4 ? 3 : 4 ;
int a = 6 < 4 ? 3 : 4 ;
int a = 4 ;

📌码农写法

上段代码的问题在于没有保证宏体被替换后整体的优先级最高,因此我们修改一下上面的宏定义,给后面的表达式整体带上括号,使宏体在被替换后仍能保证优先级最高:

#define MIN(A,B) (A < B ? A : B)

这下我们再像刚才那样使用这个宏定义:

int a = 2 * MIN(3, 4);

该代码在预处理后会被替换为:

int a = 2 * (3 < 4 ? 3 : 4);
int a = 2 * 3;
int a = 6;

该定义的问题:

当我们需要这样使用这个宏定义时:

int a = MIN(3, 4 < 5 ? 4 : 5);

我们以为得到的结果会是:

int a = MIN(3,4);
int a = 3 < 4 ? 3 : 4;
int a = 3;

但实际上我们得到的结果是:

int a = ( 3 < 4 < 5 ? 4 : 5 ? 3 : 4 < 5 ? 4 : 5 );
//按照操作符的优先级给上面的式子加上括号便于理解
int a = ((3 < (4 < 5 ? 4 : 5) ? 3 : 4) < 5 ? 4 : 5); 
int a = ((3 < 4 ? 3 : 4) < 5 ? 4 : 5)
int a = (3 < 5 ? 4 : 5)
int a = 4

📌工程师写法

上段代码的问题在于没有考虑到宏参数是表达式的情况,导致宏展开后参数运算的优先级不是最高的,因此我们修改一下上面的宏定义,给参数带上括号,使宏展开后参数的运算优先级是最高的:

#define MIN(A,B) ((A) < (B) ? (A) : (B))

这下我们再像刚才那样使用这个宏定义:

int a = MIN(3, 4 < 5 ? 4 : 5);

该代码在预处理后会被替换为:

int a = ( (3) < (4 < 5 ? 4 : 5) ? (3) : ( 4 < 5 ? 4 : 5) );
int a = ( 3 < 4 ? 3 : 4 );
int a = 3;

该定义的问题:

当我们需要这样使用这个宏定义时:

float a = 1.0f;
float b = MIN(a++, 1.5f);

我们以为得到的结果会是:

float b = MIN(1.0f,1.5f);
float b = 1.0f;

但实际上我们得到的结果是:

float b = ((a++) < (1.5f) ? (a++) : (1.5f));
float b = ( 1.0f < 1.5f ? 2.0f : 1.5f );
float b = 2.0f;

📌大牛写法

上面代码的问题在于没有考虑到自增/自减类参数在宏展开后会有副作用,我们再修改该宏使之达到完美:

#define MIN(A,B)    ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; })

五.类函数宏与函数的对比

类函数宏的调用看上去和函数调用相同,那么这两者有何区别呢?

下表列出了一些关于#define定义宏和函数的区别:

属 性 #define定义宏 函数
代 码 长 度 每次使用时,宏代码都会被插入到程序中。除了非常
小的宏之外,程序的长度会大幅度增长
函数代码只出现于一个地方;每
次使用这个函数时,都调用那个
地方的同一份代码
执 行 速 度 更快 存在函数的调用和返回的额外开
销,所以相对慢一些
操 作 符 优 先 级 宏参数的求值是在所有周围表达式的上下文环境里,
除非加上括号,否则邻近操作符的优先级可能会产生
不可预料的后果,所以建议宏在书写的时候多些括
号。
函数参数只在函数调用的时候求
值一次,它的结果值传递给函
数。表达式的求值结果更容易预
测。
带 有 副 作 用 的 参 数 参数可能被替换到宏体中的多个位置,所以带有副作
用的参数求值可能会产生不可预料的结果。
函数参数只在传参的时候求值一
次,结果更容易控制。
参 数 类 型 宏的参数与类型无关,只要对参数的操作是合法的,
它就可以使用于任何参数类型。
函数的参数是与类型有关的,如
果参数的类型不同,就需要不同
的函数,即使他们执行的任务是
不同的。
调 试 宏是不方便调试的 函数是可以逐语句调试的
递 归 宏是不能递归的 函数是可以递归的

显示详细信息


结语

在本文中我们介绍了宏定义的概念,组成及其应用,还拓展了宏定义的易错陷阱,以及类函数宏与函数的优劣对比,希望能对大家有所帮助,一起学习,一起进步!




相关文章
|
1天前
|
编译器 C语言
【C语言】宏定义在 a.c 中定义,如何在 b.c 中使用?
通过将宏定义放在头文件 `macros.h` 中,并在多个源文件中包含该头文件,我们能够在多个文件中共享宏定义。这种方法不仅提高了代码的重用性和一致性,还简化了维护和管理工作。本文通过具体示例展示了如何定义和使用宏定义,帮助读者更好地理解和应用宏定义的机制。
13 2
|
3天前
|
程序员 编译器 C语言
C语言中的预处理器指令,涵盖其基本概念、常见指令(如`#define`、`#include`、条件编译指令等)、使用技巧及注意事项
本文深入解析C语言中的预处理器指令,涵盖其基本概念、常见指令(如`#define`、`#include`、条件编译指令等)、使用技巧及注意事项,并通过实际案例分析,展示预处理器指令在代码编写与处理中的重要性和灵活性。
21 2
|
7月前
|
C语言
C语言使用宏定义实现等级调试输出PRINT_LEVEL
C语言使用宏定义实现等级调试输出PRINT_LEVEL
131 0
|
2月前
|
编译器 C语言
C语言:typedef 和 define 有什么区别
在C语言中,`typedef`和`#define`都是用来创建标识符以简化复杂数据类型或常量的使用,但它们之间存在本质的区别。`typedef`用于定义新的数据类型别名,它保留了数据类型的特性但不分配内存。而`#define`是预处理器指令,用于定义宏替换,既可用于定义常量,也可用于简单的文本替换,但在编译前进行,过度使用可能导致代码可读性下降。正确选择使用`typedef`或`#define`可以提高代码质量和可维护性。
|
7月前
|
编译器 C语言
C语言宏定义(#define定义常量​、#define定义宏​、 带有副作用的宏参数、 宏替换的规则、 宏函数的对比)
C语言宏定义(#define定义常量​、#define定义宏​、 带有副作用的宏参数、 宏替换的规则、 宏函数的对比)
|
6月前
|
程序员 C语言
C语言中的宏定义:从常量到高级技巧
C语言中的宏定义:从常量到高级技巧
285 1
|
7月前
|
自然语言处理 编译器 Linux
C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)(下)
C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)
66 0
|
7月前
|
程序员 编译器 C语言
C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)(中)
C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)
41 0
|
7月前
|
存储 程序员 编译器
C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)(上)
C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)
53 0
|
7月前
|
C语言
【C语言】#define的认识
【C语言】#define的认识