首页 > 编程笔记 > C语言笔记 阅读:24

C语言#define预处理指令的用法(非常详细,图文并茂)

在 C语言中,预处理指令是在编译过程中进行的一些预处理操作。它们并非 C语言的语句,而是由预处理器处理的指令。

源代码中以 # 开头的内容都是预处理指令,预处理指令通常出现在 C语言源文件的开头。

在 C语言代码编译前,预处理器会先处理预处理指令,根据指令的含义修改 C语言代码。修改后的代码会被另存为中间文件或直接输入编译器中,而不会保存到源文件中。因此,预处理器不会改动源文件。

本节讲解 C 语言中的 #define 预处理指令。#define 的用法如下:
#define 宏 替换体
例如,预处理指令 #define 定义了一个符号常量,其值为 3:
#include <stdio.h>
#define PRICE 3      // 商品的价格为 3 元
int main()
{
    int num;
    int total = 0;
    // 买一件
    num = 1;
    total = num * PRICE;
    printf("num:%d total:%d\n", num, total);
    // 买两件
    num = 2;
    total = num * PRICE;
    printf("num:%d total:%d\n", num, total);
    // 买三件
    num = 3;
    total = num * PRICE;
    printf("num:%d total:%d\n", num, total);
    return 0;
}
查看下图,预处理器会根据预处理指令的含义,将符号 PRICE 替换为 3,同时删除代码中的预处理指令。


图 1 符号常量的预处理

经过预处理后,代码将变为下面程序中的代码,接着编译器会对预处理后的代码进行编译:
#include <stdio.h>
int main()
{
    int num;
    int total = 0;

    num = 1;
    total = num * 3;
    printf("num:%d total:%d\n", num, total);

    num = 2;
    total = num * 3;
    printf("num:%d total:%d\n", num, total);

    num = 3;
    total = num * 3;
    printf("num:%d total:%d\n", num, total);
    return 0;
}
注意,宏的命名规则遵循 C语言标识符的命名规则:只能使用字母、数字、下画线,且首字符不能是数字。替换体不仅限于值,它的形式非常丰富,唯一要求是替换后的代码仍能正常通过编译。

例如,下面展示了一个正确的替换示例:
#include <stdio.h>
# define INTGER int
# define FMT "n1 = %d, n2 = %d, n3 = %d"
# define VAR n3
int main()
{
    INTGER n1, n2, n3;
    n1 = 1;
    n2 = 2;
    VAR = 3;
    printf(FMT, n1, n2, VAR);
    return 0;
}
预处理器会进行如下图所示的替换操作:


图 2 替换宏

运行结果为:

n1 = 1, n2 = 2, n3 = 3

代码能够成功运行。

从上面的示例中可以看出,宏的替换是无差别的,它仅把代码当作文本来处理,遇到宏就替换为宏对应的替换体:
然而,下面展示了一个错误的替换示例,也是初学者经常会犯的错误。
#include <stdio.h>
#define PI 3.1415926;
int main() {
    float r = 2.0;
    float area = PI * r * r;
    printf("圆的面积为 %f\n", area);
    return 0;
}
上面的示例在预处理指令 #define 的末尾加上了一个分号。当在程序中使用该宏时,就会导致语法错误,例如下面的语句:
float area = PI * r * r;
会被替换如下:
float area = 3.1415926; * r * r;
很明显,多了一个分号导致了编译错误。为了避免这类错误,建议在定义宏时不要在末尾加分号。正确的定义应该如下:
#define PI 3.1415926
这样,当在程序中使用宏时,替换后的代码就可以被正确地编译和执行。

C语言带参数的#define

在 #define 中使用参数可以创建类似函数的宏函数。宏函数的使用格式如下:
#define 宏(参数1,  参数2, ..., 参数n) 替换体
例如,用于求 a 和 b 两个数的平均值的宏函数可以写成以下形式:
#define MEAN(a, b) (a + b)/2
在程序中,可以按照以下方式使用它:
int result;
result = MEAN(2, 4);
经过预处理后,宏被替换为以下形式:
int result;
result = (2 + 4)/2;
在参数 a 的位置填写了 2,因此在替换内容中,所有的 a 都将被替换为 2;在参数 b 的位置填写了 4,因此在替换内容中,所有的 b 都将被替换为 4。

尽管带参数的 #define 定义的宏函数在使用方式上类似于函数,但其本质仍然是将宏替换为对应的替换内容。因此,如果将其简单地视为函数使用,则可能会出现问题。

现在写一个宏函数,它的作用是求一个数的平方:
#define SQUARE(x) x*x
接下来,使用这个宏函数:
int n = 2;
SQUARE(n);
SQUARE(n + 2);
100 / SQUARE(n);
SQUARE(++n);
如果 SQUARE 是一个函数,那么我们的预期结果如下:
运行结果如下图所示:
4
8
100
16
其中除了第一条符合预期,其他的均不符合预期。我们将逐条探究其原因。

1) SQUARE(n)

查看下图,SQUARE(n) 展开为 n*n。


图 3 SQUARE(n)

将 2 代入运算,2*2 结果为 4。预期结果也是 4,因此运算结果符合预期。

2) SQUARE(n + 2)

查看图 2,SQUARE(n + 2) 展开为 n + 2 * n + 2:


图 4 SQUARE(n + 2)

将 2 代入运算,2 + 2 * 2 + 2,乘法的优先级高于加法,先算 2 * 2,再从左到右算加法,结果为 8。预期结果为 16,因此运算结果不符合预期。

我们的本意是希望 n + 2 作为一个整体优先进行运算。想要实现这个预期,需要使用括号将 n + 2 括起来。因此,我们将预处理命令修改为 #define SQUARE(x) (x)*(x),以 n + 2 作为它的参数,它展开后为 (n + 2) * (n + 2)。这样可以保证结果正确。

3) 100 / SQUARE(n)

查看下图,100 / SQUARE(n) 展开为 100 / n * n:


图 5 100 / SQUARE(n)

将 2 代入运算,100 / 2 * 2,除法优先级与乘法一致,因此从左向右计算,结果为 100。预期结果为 25,因此运算结果不符合预期。

我们的本意是希望 n * n 作为一个整体优先进行运算。但实际上,因为优先级问题可能被左右两边的运算符影响。因此,应该在 n * n 表达式两边添加括号,以确保它优先完成计算。因此,我们将预处理指令修改为 #define SQUARE(x) (x * x),使其展开后的表达式为 100 /(2 * 2),从而保证结果正确。

4) SQUARE(++n)

查看下图,SQUARE(++n) 展开为 ++n * ++n:


图 6 SQUARE(++n)

在一个表达式中多次对同一个变量 n 使用 ++ 运算符,结果是不确定的。

5) 保证宏函数按照预期运行

宏函数的参数应当作为一个整体,优先运算。在本例中为 #define SQUARE(x) (x)*(x)

宏函数展开后的表达式应当作为一个整体,以避免被左右运算符优先级影响。在本例中为 #define SQUARE(x) (x * x)

结合以上两点,本例中的宏函数应当修改为 #define SQUARE(x) ((x)*(x))

若宏函数的替换体多次使用参数,那么不要在宏函数的参数内填自增、自减表达式。

由于宏函数仅仅是完成替换操作,将参数替换并拼接到替换体的表达式中,而不是先让参数运算得到结果后,再进行运算,因此为了保证参数不被其他运算符优先级影响,需要在参数两边加上括号。

此外,宏函数展开后的表达式如果作为一个更大表达式的子表达式,则可能会受到左右两边运算符优先级的影响。为了保证宏函数展开后的表达式能够优先计算,需要在替换体两边加上括号。

最后,为了避免在一个表达式中对同一个变量多次进行自增、自减操作,如果宏函数的替换体在一个表达式中多次使用同一个参数,则不要在宏函数的参数内填写自增、自减表达式。

C语言宏函数的运算符

在 C语言中,# 和 ## 是两个常用的宏函数运算符:

1) #(井号)

# 运算符可以将宏参数转换为字符串,其语法格式为 # 参数。例如:
#define STR(x) #x
printf("%s\n", STR(Hello world));  // 输出 "Hello world"
在这个例子中,宏函数 STR 将其参数 x 转换为字符串,因此在 printf() 函数中输出的是字符串 "Hello world"。

下面是一个更加复杂的示例:
#include <stdio.h>
#define FMT(varname) "The value of " #varname " is %d\n"
int main()
{
   int number = 123;
   printf(FMT(number), number);
   return 0;
}
FMT(number) 的展开为 "The value of " "number" " is %d\n"。在 C语言中,相邻的字符串会被自动连接成一个完整的字符串。因此,"The value of " "number" " is %d\n" 会被自动连接成 "The value of number is %d\n"。因此,程序可以正常编译并输出结果,运行结果为:
The value of " #varname 123
如果在宏函数中没有使用 #,则 FMT(number) 展开为 "The value of " number " is %d\n"。

由于两个字符串之间出现了一个变量 number,不符合 printf() 第一个参数需要字符串的写法,因此无法编译通过。

2) ##(双井号)

## 运算符可以将两个宏参数连接起来以形成一个新的标识符,其语法格式为:
参数1##参数2
例如,我们想要使用宏函数来表示如下的两组变量名。这两组变量名是有一定规律的,前缀为 group1 或 group2,后缀为 Apple 或 Orange。
//  第一组变量,group1
int group1Apple = 1, group1Orange = 2;
//  第二组变量,group2
int group2Apple = 100, group2Orange = 200;
因此,我们可以使用宏函数来组合前缀与后缀,让它们成为一个完整的变量名。
#define VARNAME(group, name) group ## name
在这个例子中,宏函数 VARNAME 将其两个参数连接起来形成一个新的标识符。例如:
如果不使用 ##,则会发生什么?
#define VARNAME(group, name) group name

不使用 ##,展开后的两个参数之间留有空格,无法正常使用。那如果删除替换体中的空格呢?
#define VARNAME(group, name) groupname
现在,宏函数出现了问题,它具有两个参数:group 和 name。但是,替换体中没有与这两个参数对应的记号。因此,## 的存在是有意义的。

3) 用法示例

下面程序是一个完整的使用宏函数运算符的示例:
#include <stdio.h>
#define FMT(group, name) "The value of " #group #name " is %d\n"
#define VARNAME(group, name) group ## name
int main()
{
    // 第一组变量,group1
    int group1Apple = 1, group1Orange = 2;
    // 第二组变量,group2
    int group2Apple = 100, group2Orange = 200;

    // 使用第一组
    printf(FMT(group1, Apple), VARNAME(group1, Apple));
    printf(FMT(group1, Orange), VARNAME(group1, Orange));

    // 使用第二组
    printf(FMT(group2, Apple), VARNAME(group2, Apple));
    printf(FMT(group2, Orange), VARNAME(group2, Orange));
    return 0;
}
运行结果为:

The value of group1Apple is 1
The value of group1Orange is 2
The value of group2Apple is 100
The value of group2Orange is 200


其余的类似上面的处理。代码预处理之后,将会变为如下所示:
#include <stdio.h>
int main()
{
    // 第一组变量,group1
    int group1Apple = 1, group1Orange = 2;
    // 第二组变量,group2
    int group2Apple = 100, group2Orange = 200;

    // 使用第一组
    printf("The value of \"group1\" \"Apple\" \" is %d\n", group1Apple);
    printf("The value of \"group1\" \"Orange\" \" is %d\n", group1Orange);

    // 使用第二组
    printf("The value of \"group2\" \"Apple\" \" is %d\n", group2Apple);
    printf("The value of \"group2\" \"Orange\" \" is %d\n", group2Orange);
    return 0;
}

C语言取消宏定义

当我们定义了一个宏后,我们如果需要更改宏的定义,那么可以重新定义它吗?例如下面实例,在将宏定义为 100 之后,又尝试将其重新定义为 101:
#include <stdio.h>
#define NUM 100
#define NUM 101
int main()
{
    printf("%d\n", NUM);
    return 0;
}
在 Visual Studio 中,重复定义宏并不会导致编译报错,但是它会抛出一个警告,如下图所示:


图 7 宏重定义

因此,更合适的做法是使用预处理指令 #undef 取消这个宏的定义,然后重新定义它。具体代码如下:
#include <stdio.h>
#define NUM 100
//  取消宏定义NUM
#undef NUM
//  重新定义宏NUM为101
#define NUM 101
int main()
{
   printf("%d\n", NUM);
   return 0;
}

相关文章