【C语言基础】:预处理详解(一)

简介: 【C语言基础】:预处理详解(一)

一、预定义符号

在C语言中设置了许多的预定义符号,这些预定义符号是可以直接使用的,预定义符号也是在预处理阶段进行处理的。


常见的预定义符号:

__FILE__ //进⾏编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义

【示例】:

#include<stdio.h>

int main()
{
  printf("%s\n", __FILE__);
  printf("%s\n", __DATE__);
  printf("%s\n", __TIME__);
  printf("%d\n", __LINE__);
  return 0;
}

我们在VS上使用 _ _ STDC _ _ 会发现显示未定义,这也就说明VS的编译器是不完全遵循ANSI C的,为了展示效果,我没可以在gcc的环境下查看一下。

在gcc环境下运行可以看到它输出的是1,这表明gcc环境下的编译器是遵循ANSI C的。

预处理之后我们会发现,前面我们就学过,程序在预处理之后会把预定义指令给替换掉,这里结果也确实如此。

二、#define定义常量

#define一般有两种应用场景:

  1. #define定义常量
  2. #define定义宏

#define定义常量基本语法:

#define name stuff

【示例】:

#include<stdio.h>
#define MAX 100
#define STR "hello world"
int main()
{
  int a = MAX;
  printf("%d\n", MAX);
  printf("%s\n", STR);
  return 0;
}

#define定义标识符加不加 ; 的区别:

#include<stdio.h>
#define MAX 100
#define MAX1 100;
int main()
{
  int a = MAX;
  int b = MAX1;
  printf("%d\n", MAX);
  printf("%d\n", MAX1);
  return 0;
}

#define定义标识符加不加 ; 的区别:

#include<stdio.h>
#define MAX 100
#define MAX1 100;
int main()
{
  int a = MAX;
  int b = MAX1;
  printf("%d\n", MAX);
  printf("%d\n", MAX1);
  return 0;
}

可以看到,MAX1加了分号之后, 之后后面使用的MAX1全都加上了分号,这也就导致了在打印MAX1时报错,在预处理之后可以清楚的看到原因(#define把;也替换过来了)。所以一般使用 #define 定义常量时,不要加分号。

三、#define定义宏

#define 机制包括了⼀个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。

宏的申明方式:

#define name( parament-list ) stuff

parament-list 是⼀个由逗号隔开的符号表,它们可能出现在stuff中。

注意:参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的⼀部分。

【示例】:利用宏求一个数的平方

#include<stdio.h>
#define SQURE(x) x * x
int main()
{
  int a = 12;
  int b = SQURE(a);
  printf("%d\n", b);
  return 0;
}

因为参数是允许替换到文本中的,把a传给SQURE(a)就相当于把程序替换成了a * a, 这里预处理后也能看到效果。

但是这个宏也存在着一些问题:

int a = 5;
printf("%d\n", SQURE(a + 1));

按照惯性,我们会觉得这个代码的运行结果会是6 * 6 = 36,但结果真的会是这样吗?

我们来运行试一下:

运行之后可以发现结果等于11,这里就要注意了,宏的参数是不会参与计算的,会直接进行替换,我们进行预处理生成目标文件后可以发现SQURE(a + 1)替换成了a + 1 * a + 1,而 * 的优先级比 + 高,导致会先算 * 再算 + ,a等于5,乘以一还是5,再加上6就等于11。

那怎么让他得到36呢,其实这里加个括号就可以了。

#include<stdio.h>
#define SQURE(x) (x) * (x)
int main()
{
  int a = 5;
  int b = SQURE(a + 1);
  printf("%d\n", b);
  return 0;
}

当然,下面这种方法也是一样的。

我们只要确保替换之后运算顺序不发生改变就可以达到目的了。

下面是一个宏定义:

#define DOUBLE(x) (x) + (x)

定义中我们使用了括号,虽然这样可以避免之前的问题,但是这个宏定义可能会出现新的问题:

#include<stdio.h>
#define DOUBLE(x) (x) + (x)
int main()
{
    int a = 5;
    printf("%d\n", 5 * DOUBLE(a));
    return 0;
}

按照惯性思维,我们可能会认为打印50,但结果是否会是50呢?

结果发现打印的是30,预处理之后生成目标文件之后可以发现5会先和a相乘,然后再加a,导致结果与我们的出现误差。

这个问题,的解决办法是在宏定义表达式两边加上⼀对括号就可以了。

#define DOUBLE(x) ((x) + (x))

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


四、带有副作用的宏参数

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

【示例】:

x+1;//不带副作用
x++;//带有副作用

【示例】:MAX宏可以证明具有副作用的参数所引起的问题。

#include<stdio.h>
#define MAX(x, y) ((x) > (y) ? (x) : (y))
int main()
{
    int a = 3;
    int b = 5;
    int m = MAX(a++, b++);
    printf("m = %d\n", m);
    printf("a = %d\n", a);
    printf("b = %d\n", b);
    return 0;
}

替换之后是一个三目运算符,首先a = 3,b = 5,由于是后置加加,判断之后才会加一,所以判断之后a就等于4,b就等于6,因为表达式为假,后面那个a++不会执行,a还是等于4,后面的b++会执行,但由于也是后置加加,先使用后再加一,即m就等于6,b就等于7。

结论:如果一个带有副作用的参数在宏定义中出现两份,就有可能出现不同的结果,即带有副作用的参数是非常危险的,要尽量避免使用。

五、宏替换的规则

宏替换是C语言预处理器的一个重要功能,它在编译之前进行文本替换。宏替换遵循一定的规则,这些规则确保了宏能够在正确的上下文中被替换为定义的文本,需要涉及几个步骤:

  1. 文本替换:
    宏定义中的所有文本都将被直接替换到宏名出现的任何位置。这意味着宏名在代码中出现的每个地方,都会用宏定义中的文本替换。
#include<stdio.h>
#define MAX(x, y) ((x) > (y) ? (x) : (y))
#define M 10
int main()
{
    int a = 3;
    int b = 5;
    int m = MAX(a, M);
    printf("m = %d\n", m);
    printf("M = %d\n", M);
    printf("b = %d\n", b);
    return 0;
}

每个宏名的位置都会用宏定义中的文本替换。


2.宏参数的保留:

当宏名被替换时,宏参数将保持其原始的括号结构。这是为了避免改变操作符的优先级和结合性,确保代码的逻辑不变。


3.宏参数的展开:

宏参数在替换时会展开,这意味着如果宏参数本身是一个宏,它也会被展开(替换)。这个过程称为宏的展开或宏的宏展开。

5ade4d55a42bf3da80e6d059aabe2745_ad883f79f3b845d4ac70da6cf20b263b.png


4.宏展开的顺序:

当宏参数中包含其他宏时,预处理器会按照它们在宏定义中出现的顺序进行替换。如果宏A中使用了宏B,而宏B又使用了宏C,那么预处理器首先会替换宏C,然后是宏B,最后是宏A。


5.宏展开的深度:

宏展开的深度是有限的。如果一个宏展开后仍然是一个宏(即宏的宏),这个过程会继续,但是有一个深度限制,以避免无限循环。


6.宏定义的顺序:

宏定义的顺序可能会影响宏替换的结果。如果两个宏相互依赖,可能会导致预处理错误。为了解决这个问题,可以使用宏的函数样宏形式,或者确保依赖关系正确。


7.宏定义的优先级:

如果两个宏定义具有相同的名称,预处理器将使用最后一个定义。这意味着宏定义可以被后续的宏定义覆盖。


8.条件编译中的宏替换:

在使用#ifdef、#ifndef、#if、#else、#elif和#endif等条件编译指令时,只有当条件为真时,相关的宏才会被替换。


9.字符串化和标记粘贴:

预处理器提供了特殊的宏操作符,如字符串化运算符#和标记粘贴运算符##。字符串化运算符可以将宏参数转换为字符串字面量,而标记粘贴运算符可以将两个宏参数连接成一个单一的标识符。


10.宏展开的最佳实践:

为了避免宏展开引起的问题,建议使用括号包围宏参数,避免宏定义过于复杂,以及避免宏名与关键字或现有标识符冲突。


注意:

  1. 宏参数和 #define 定义中可以出现其他 #define 定义的符号。但是对于宏,不能出现递归。
#include<stdio.h>
#define MAX(x, y) ((x) > (y) ? (x) : (y))
#define M 10
int main()
{
    int a = 3;
    int b = 5;
    int m = MAX(a, MAX(2, 3));
    printf("m = %d\n", m);
    printf("M = %d\n", M);
    printf("b = %d\n", b);
    return 0;
}

  1. 当预处理器搜索 #define 定义的符号的时候,字符串常量的内容并不被搜索。
相关文章
|
1月前
|
编译器 C语言
C语言--预处理详解(1)
【10月更文挑战第3天】
|
1月前
|
编译器 Linux C语言
C语言--预处理详解(3)
【10月更文挑战第3天】
|
1月前
|
自然语言处理 编译器 Linux
【C语言篇】编译和链接以及预处理介绍(上篇)1
【C语言篇】编译和链接以及预处理介绍(上篇)
40 1
|
17天前
|
C语言
【c语言】你绝对没见过的预处理技巧
本文介绍了C语言中预处理(预编译)的相关知识和指令,包括预定义符号、`#define`定义常量和宏、宏与函数的对比、`#`和`##`操作符、`#undef`撤销宏定义、条件编译以及头文件的包含方式。通过具体示例详细解释了各指令的使用方法和注意事项,帮助读者更好地理解和应用预处理技术。
20 2
|
1月前
|
编译器 Linux C语言
【C语言篇】编译和链接以及预处理介绍(下篇)
【C语言篇】编译和链接以及预处理介绍(下篇)
32 1
【C语言篇】编译和链接以及预处理介绍(下篇)
|
1月前
|
C语言
C语言--预处理详解(2)
【10月更文挑战第3天】
|
1月前
|
编译器 C语言
C语言预处理详解
C语言预处理详解
|
1月前
|
存储 C语言
【C语言篇】编译和链接以及预处理介绍(上篇)2
【C语言篇】编译和链接以及预处理介绍(上篇)
36 0
|
3月前
|
存储 自然语言处理 程序员
【C语言】文件的编译链接和预处理
【C语言】文件的编译链接和预处理
|
3月前
|
程序员 编译器 C语言
C语言中的预处理指令及其实际应用
C语言中的预处理指令及其实际应用
86 0