C 语言 —— 明示常量指令:#define

预处理指令可以出现在源文件的任何地方。
变量有作用域,#define 定义的明示常量同样有其作用区域,其定义从指令出现的地方到该文件末尾或者 #undef 命令之间有效。
使用 #define 指令最常见的用法就是用来定义明示常量(也叫做符号常量),但是该指令还有许多其他用途。
用法#define 标识符 常量。注意后面没有分号!!

#define 又称宏定义,标识符是所定义的宏名,简称宏。标识符的命名规则遵守 C 语言的标识符命名规则,不过通常采用大写字母来命名宏,以此提醒程序员注意。#define 的功能是将标识符定义为其后的常量。

明示常量(符号常量)

C 语言中,可以用 #define 定义一个标识符来表示一个常量。其特点是:定义的标识符不占内存,只是一个临时的符号,预编译后这个符号就不存在了。因此这种常量也叫做符号常量。

// 例1.
#include<stdio.h>
#define A1 300
#define S1 "Hello, World"
int main()
{
    const int a2 = 100;
    cosnt char* s2 = "hello, world";
    printf("%d\n%s\n", A1, S1);
    printf("%d\n%s\n", a2, s2);
    return 0;
}

1. 常考:预处理阶段只进行文本替换,不进行运算

对于最后一条,预处理指令定义的标识符都不会占据内存。预处理指令也称预编译指令,即在编译之前进行一些处理,而预处理做的其实就是文本替换,这个过程也叫做宏展开,即用宏定义后面的替换体将程序中出现宏的地方替换掉。
例如,#define 定义的明示变量就是在预处理阶段用标识符后面的值替换标识符,因此不会占据内存。例如,上面程序中将在预处理阶段将 printf("%d\n%s\n", A1, S1); 替换成 printf("%d\n%s\n", 300, "Hello, World");
在这个文本替换过程中需要注意的一点是,#define 只执行文本替换,不执行计算!这个考点在各种笔试中都经常碰到。例如:

// 例2.
#include<stdio.h>
#define SUM 3+4 
int main()
{
    int a = SUM * SUM;
    printf("%d\n", a);
    return 0;
}

例2. 运行结果是 19,而不是 49。因为预处理阶段预处理器将 SUM 替换成 3+4,经过预处理之后的程序为 int a = 3 + 4 * 3 + 4;,在运行阶段程序才进行计算,此时根据运算符优先级,先算 * 的部分,因此 a 的值为 3+12+4,即 19。

2. 易错点

// 例3.
#include<stdio.h>
#define PI 3.14;
int main()
{
    double r = 4.3;
    double s = PI * r * r;
    printf("%lf\n", s);
    return 0;
}

例3. 会在 PI * r 处报错,错误描述为:"*" 的操作数必须是指针,但它具有类型 "double"。这时候是不是很纳闷为什么作为乘法运算符会被识别为指针的解引用?原因在于 PI 宏定义中多加了分号!在预处理阶段例3. 中的 double s = PI * r * r; 会被替换成 double s = 3.14; * r * r; 这下就能明白为什么会被识别为解引用运算符了吧。

4. 什么时候用明示常量?

对于绝大部分数字常量,应该使用字符常量。如果在算式中用字符常量代替数字,常量名能更清楚地表达该数字的含义。如果是表示数组大小的数字,用符号常量后更容易改变数组的大小和循环次数。如果数字是系统代码(如,EOF),用符号常量表示的代码更容易移植(只需改变EOF的定义)。助记、易更改、可移植,这些都是符号常量很有价值的特性。
C 语言现在也支持 const 关键字,提供了更灵活的方法。用 const 可以创建在程序运行过程中不能改变的变量,可具有文件作用域或块作用域。另一方面,宏常量可用于指定标准数组的大小和 const 变量的初始值。

5. 宏定义和 const 常量区别

  1. 定义的区别:
    1. 明示变量用 #define 声明,而 const 常量用 const + 数据类型 声明。
    2. 明示变量最后没用分号,const 常量声明需要用分号表示语句结束。
    3. 明示变量不需要用等号赋值,cosnt 常量需要用等号赋值。
    4. 明示变量不占内存,cosnt 常量需要占据内存。
  2. 处理阶段阶段的不同:
    1. 宏定义在预处理阶段进行文本替换。
    2. cost 常量在程序运行时使用。
  3. 存储方式不同:
    1. 宏定义是直接替换,不会分配内存,存储于程序的代码段中。
    2. const 常量需要进行内存分配。
  4. 是否进行类型检查:
    1. 宏定义是字符替换,不进行类型检查。
    2. const 常量定义时需要声明数据类型,使用时会进行类型检测。
  5. 宏定义可以声明函数。

宏函数

宏函数介绍

前面介绍了明示常量(宏定义),其实宏定义除了定义常量之外还可以定义宏函数。
在 #define 中使用参数可以创建外形和作用与函数类似的类函数宏。带有参数的宏看上去很像函数,因为这样的宏也使用圆括号。类函数宏定义的圆括号中可以有一个或多个参数,随后这些参数出现在替换体中。
image.png

// 例4. 
// 宏函数定义
#define SQUARE(X) X*X 
// 在程序中使用:
z = SQUARE(2); // 结果:z = 4;
例4. 看上去像函数调用,但是它的行为和函数调用完全不同。
5.
#define SQUARE(X) X*X
int t = 4;
x = SQUARE(t);  // result: x = 16
y = SQUARE(t+2);// result: y = 14

例5. 演示了宏函数和函数调用的不同,y = SQUARE(t+2) 在预处理阶段被替换为 y = t + 2 * t + 2,因此 y 的值为 14。如果想要避免这样情况,需要在宏定义时多加几个括号来确保运算和结合的正确顺序。

#define SQUARE(X) ((X)*(X))

替换体最外面的括号是为了避免诸如 100/SQUARE(4) 这样的情况下运算顺序。如果不加最外面的括号,在预处理阶段替换之后就变成了 100/(4)*(4),会先算 100/4,再将结果×4。

尽管如此,这样做还是无法避免程序中最后一种情况的问题 —— SQUARE(++x) 变成了 ++x*++x,递增了两次x,一次在乘法运算之前,一次在乘法运算之后:++x*++x = 6*7 = 42。由于标准并未对这类运算规定顺序,所以有些编译器得 7*6。而有些编译器可能在乘法运算之前已经递增了x,所以7*7得49。
解决这个问题最简单的方法是,避免用 ++x 作为宏参数。一般而言,不要在宏中使用递增或递减运算符

字符串中的宏参数:#

// 例6. 
#define PSQR(X) printf("The square of X is %d.\n", ((X)*(X))); 
PSQR(8); // 输出为: The square of X is 64. 

注意双引号字符串中的 X 被视为普通文本,而不是一个可被替换的记号。C 允许在字符串中包含宏参数。在类函数宏的替换体中,# 号作为一个预处理运算符,可以把记号转换成字符串。例如,如果 x 是一个宏形参,那
么 #x 就是转换为字符串"x"的形参名,这个过程称为字符串化。

#define PSQR(X) printf("The square of " #X " is %d.\n", ((X)*(X)));
int y = 8;
PSQR(y); // 输出为: The square of y is 64. 

调用宏函数的时候,用 “y” 替换 #x,printf("The square of " "y" " is %d.\n", ((y)*(y)));,然后利用 ANSI C 字符串的串联特性将这些字符串组合起来,printf("The square of y is %d.\n", ((y)*(y)));

预处理器粘合剂:##

与 # 运算符类似,## 运算符可用于类函数宏的替换部分。而且,## 还可用于对象宏的替换部分。## 运算符把两个记号组合成一个记号。例如,可以这样做:

#define XNAME(n) x ## n 

然后,宏 XNAME(4) 将展开为 x4。

#include <stdio.h> 
#define XNAME(n) x ## n 
#define PRINT_XN(n) printf("x" #n " = %d\n", x ## n); 
int main(void) 
{
    int XNAME(1) = 14; // 变成 int x1 = 14; 
    int XNAME(2) = 20; // 变成 int x2 = 20; 
    int x3 = 30; 
    PRINT_XN(1); // 变成 printf("x1 = %d\n", x1); 
    PRINT_XN(2); // 变成 printf("x2 = %d\n", x2); 
    PRINT_XN(3); // 变成 printf("x3 = %d\n", x3); 
    return 0; 
}
// 该程序的输出如下: x1 = 14 x2 = 20 x3 = 30

PRINT_XN() 宏用 # 运算符组合字符串,## 运算符把记号组合为一个新的标识符。

变参宏:… 和 VA_ARGS

C99/C11 对宏提供了变参的工具。虽然标准中未使用“可变”这个词,但是它已成为描述这种工具的通用词(虽然,C 标准的索引添加了字符串化词条,但是,标准并未把固定参数的函数或宏称为固定函数和不变宏)。

变参宏通过把宏参数列表中最后的参数写成省略号(即,3个点…)来实现这一功能。而 __VA_ARGS____可用在替换部分中,表明省略号代表什么。

#include <stdio.h> 
#include <math.h> 

#define PR(X, ...) printf("Message " #X ": " __VA_ARGS__) 
int main(void) 
{
	double x = 48
    double y; 
    y = sqrt(x); 
    PR(1, "x = %g\n", x); 
    PR(2, "x = %.2f, y = %.4f\n", x, y); 
    return 0; 
}
// 输出结果:
// Message 1: x = 48
// Message 2: x = 48.00, y = 6.9282

记住,省略号只能代替最后的宏参数

#define WRONG(X, ..., Y) #X #_ _VA_ARGS_ _ #y //不能这样做

宏和函数的选择

有些编程任务既可以用带参数的宏完成,也可以用函数完成。应该使用宏还是函数
使用宏比使用普通函数复杂一些,稍有不慎会产生奇怪的副作用。

宏和函数的选择实际上是时间和空间的权衡。
宏生成内联代码,即在程序中生成语句。如果调用20次宏,即在程序中插入20行代码。如果调用函数20次,程序中只有一份函数语句的副本,所以节省了空间。然而另一方面,程序的控制必须跳转至函数内,随后再返回主调程序,这显然比内联代码花费更多的时间。 因此,对于简单的函数,程序员通常用宏来处理。

C99 提供了第3种可替换的方法——内联函数

在使用宏函数时,依旧推荐使用大写字母表示宏,以此来提醒程序员宏带来的副作用。

如果打算使用宏来加快程序的运行速度,那么首先要确定使用宏和使用函数是否会导致较大差异。在程序中只使用一次的宏无法明显减少程序的运行时间。在嵌套循环中使用宏更有助于提高效率。许多系统提供程序分析器以帮助程序员压缩程序中最耗时的部分。

  • 6
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值