【C语言】一篇带你玩转 预处理指令

很多人好奇预处理是什么,C程序中插入传给编译程序的各种指令(宏),这些指令被称为预处理器指令,它们扩充了程序设计的环境,也称预处理符号。
这一节我们就讲解预处理,

文章目录

  • 预定义符号
  • #define
    • #define 定义标识符
    • #define 定义宏
    • #define 替换规则
    • #和##
    • 带副作用的宏参数
    • 宏和函数对比
      • 宏和函数的一个对比
  • #undef
  • 命令行定义
  • 条件编译
  • 文件包含
    • 头文件被包含的方式:
    • 嵌套文件包含

预定义符号

  • FILE 进行编译的源文件
  • LINE 文件当前的行号
  • DATE 文件被编译的日期
  • TIME 文件被编译的时间
  • STDC 如果编译器遵循ANSI C,其值为1,否则未定义
  • FUNCTION 获取函数名

这些预定义符号都是语言内置的。
那该如何使用呢?

  • 代码实例
#include
#include

int main()
{
	int a[] = { 1,2,3,4,5, };
	int sz = sizeof(a) / sizeof(a[0]);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
	 printf("%d , %s , %s , %s , lene:%d\n", a[i], __FILE__, __TIME__, __DATE__, __LINE__);
	}
	system("pause");
	return 0;
}

最终于输出结果:
1 , D:\C.Code\2023test.c\test_4_2\test_4_2\test.c , 00:26:17 , Apr 4 2023 , lene:90
2 , D:\C.Code\2023test.c\test_4_2\test_4_2\test.c , 00:26:17 , Apr 4 2023 , lene:90
3 , D:\C.Code\2023test.c\test_4_2\test_4_2\test.c , 00:26:17 , Apr 4 2023 , lene:90
4 , D:\C.Code\2023test.c\test_4_2\test_4_2\test.c , 00:26:17 , Apr 4 2023 , lene:90
5 , D:\C.Code\2023test.c\test_4_2\test_4_2\test.c , 00:26:17 , Apr 4 2023 , lene:90

知识点扩展:

补充: 其实编译器在代码编译的时候,会对函数和变量名重命名的//在C语言中重命名的规则基本就是: 加_


#define

#define 定义标识符

#define MAX 100
int main()
{
	printf("%d", MAX);
	int a[MAX] = { 0 };
	return 0;
}

我们用VS code gcc编译打开预处理看看
输入gcc test.c -E -o test.i 打开预处理指令
#define MAX 定义的标识符都给替换成100了
【C语言】一篇带你玩转 预处理指令_第1张图片

当然用#define重命名也是可以的

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

此时打开预处理指令显示【C语言】一篇带你玩转 预处理指令_第2张图片

如果定义的 某个值过长,可以分成几行写,除了最后一行外,每行的后面都加一个 \ 反斜杠(续行符)

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

如果一直按回车跳到下一行,语法会出现问题,所以要使用 \ 反斜杠(续行符)
注意:续行符后面不能添加其他东西

注意事项:

在define定义标识符的时候,不要加上 ; 分号

  • 那为什么呢?我们用gcc编译器打开预处理看看【C语言】一篇带你玩转 预处理指令_第3张图片

如果#define加了 ; 在其他语句也会默认替换成; 所以一定要牢记 #define不要加分号


#define 定义宏

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

下面是宏的申明方式:

#define ADD(x) x + x

注意:

参数列表的左括号必须与ADD紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为x+x的一部分。

在置于程序中,预处理器会将传进来的参数原封不动的替换宏定义的表达式、
-比如【C语言】一篇带你玩转 预处理指令_第4张图片

警告:
宏存在一定的问题,我们要尽量避开:
观察下面的代码段:

#define SQUABE(x) x * x

int main()
{
	printf("%d", SUL(1 + 7));
	return 0;
}

下面输出结果是15,为什么呢?就是因为我们刚刚所说的,传进来的参数原封不动的替换宏定义的表达式.替换文本时,参数x被替换成1 + 7,所以这条语句实际上变成了:
printf (“%d\n”,1+7 * 1+7 );
这样就比较清晰了,由替换产生的表达式并没有按照预想的次序进行求值。【C语言】一篇带你玩转 预处理指令_第5张图片

那该怎么处理?
在宏定义上加上两个括号,这个问题便轻松的解决了:>

#define SQUABE(x) (x) * (x)

这样预处理之后就产生了预期的效果:

printf ("%d\n",(1+7) * (1+7) );

warning:
这里还有一个宏定义:
比如

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

int main()
{
	printf("%d", SUL(1 + 7));
	return 0;
}
  • 定义中我们使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误。

看上去,好像打印100,但事实上打印的是55.
我们发现打开预处理替换之后:

printf ("%d\n",10 * (5) + (5));

乘法运算先于宏定义的加法,所以出现了
55 .

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

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

提醒:

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


#define 替换规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

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

注意:

  1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

#和##

#.
我们直接看代码:

#include
 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 f is %f\n", f);

     return 0;
 }

这里写是不是太麻烦了,每次都要打印。那分装一个函数能不能解决,答案是不行的,因为不能每次都不能把the value of a,b,f这些字符串跟着改变,那该怎么办?

可以用宏来实现!
当然我们这里先要做下知识铺垫

int main()
 {
     printf("hello world\n");
     printf("hello " "world\n");
     return 0;
 }

最终输出结果:
hello world
hello world

这样写的格式也会自然合成一个字符串

  • 宏的方式
 #define print_format(num, format) \
             printf("the value of num is "format, num)

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

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

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

     system("pause");
     return 0;
 }

最终输出结果:
the value of num is 10
the value of num is 20
the value of num is 3.140000

我们发现这样写还是不能实现该功能,num那里应该是a,b,f才对阿,那该怎么办?
这样我们就要用到我们的 #和C语言自带的天然字符串格式,我们只需要把 the value of和is变成字符串就可以了,任何num用一个#来修饰。

  • 下面来看代码吧

#num 会让这个参数不是替换进来,而是对应他的名字所对应的字符串。

 #define print_format(num, format) \
             printf("the value of "#num" is "format, num)

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

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

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

     system("pause");
     return 0;
 }

最终输出结果:
the value of a is 10
the value of b is 20
the value of f is 3.140000

使用 # ,把一个宏参数变成对应的字符串。


##.

##的作用

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

  • 代码实例
    ##把Class和110合并成Class110
 int Class110 = 2023;

 #define CAT(x,y) x##y
 //Class110
 int main()
 {
     printf("%d\n", CAT(Class, 110));

     system("pause");
     return 0;
 }

输出结果:
2023

注:

这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。比如上面没有int Class110 = 2023; 产生的结果是未定义的

##的使用场景很少


带副作用的宏参数

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

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

求两个数较大值

  • 宏代码实例
    MAX宏可以证明具有副作用的参数所引起的问题。
#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++));
    //         3      5           6
    printf("%d\n", c);//6
    printf("%d\n", a);//4
    printf("%d\n", b);//7
    
    return 0;
}

最终于输出结果
6
4
7

  • 函数代码实例
    换成函数实现这个功能,后果不至于那么严重。因为函数的参数是计算之后传进去的,不是替换进去的
int Max(int x, int y)
{
    return x>y?x:y;
}


int main()
{
    int a = 3;
    int b = 5;
    int c = Max(a++, b++); 
    printf("%d\n", c);
    printf("%d\n", a);
    printf("%d\n", b);
    
    return 0;
}

最终输出结果
5
4
6
【C语言】一篇带你玩转 预处理指令_第6张图片
注: 后置++ 是先使用后++,Max使用完才++


宏和函数对比

命名约定:一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。那我们平时的一个习惯是:

  1. 把宏名全部大写
  2. 函数名不要全部大写

这只是我们的一个约束,但有的编译器甚至把宏全部小写。

宏通常被应用于执行简单的运算。
比如在两个数中找出较大的一个。
函数同样也可以完成同样的问题,那两者哪一个更好呢?

#include
#include
//函数的实现 - 1
int Max(int x, int y)
{
	return x > y ? x : y;
}

//宏的实现 - 2
#define MAX(x,y)  ((x)>(y)?(x):(y))

int main()
{
	int a = 0;
	int b = 0;
	//输入
	scanf("%d %d", &a, &b);
	//较大值
	int m1 = Max(a, b);
	printf("%d\n", m1);

	int m2 = MAX(a, b);
	//int m2 = ((a)>(b)?(a):(b));
	printf("%d\n", m2);
	system("pause");
	return 0;
}

实现该功能宏更加好,原因有2:

  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。
    函数调用的时间花费:
  1. 函数调用前准备 (传参、L函数栈帧空间的维护)
  2. 函数内部(主要运算)
  3. 函数返回,返回值的处理,函数栈帧的销毁

所以宏比函数在程序的规模和速度方面更胜一筹。

  1. 更为重要的是函数的参数必须声明为特定的类型。(比如一定要定义int char类型的)

所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用来比较的类型。
宏是与类型无关的。

当然和函数相比宏也有劣势的地方:

  1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
  2. 宏是没法调试的。
  3. 宏由于类型无关,也就不够严谨。
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

以后功能比较简单的时候,可以采用宏来实现,如果功能比较复杂,建议使用函数来实现

宏有时候可以做函数做不到的事情。比如:宏的参数可以只出现类型,但是函数做不到。

  • 代码实例:
#include
#include
#define MALLOC(num,Type) (Type*)malloc(sizeof(Type)*num)
int main()
{
	int* p = MALLOC(10, int);
	if (p == NULL)
		perror("malloc:");
	
	//业务处理

	//内存释放
	free(p);
	p = NULL;
	system("pause");
	return 0;
}

宏和函数的一个对比

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

#undef

这条指令用于移除一个宏定义。

  • 代码实例
    【C语言】一篇带你玩转 预处理指令_第7张图片
    此时后面的MAX已经出现未定义了,因为给我们#undef取消了.

命令行定义

许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。

执行程序时,可以从命令行传值给 C 程序。这些值被称为命令行参数,它们对程序很重要,特别是当您想从外部控制程序,而不是在代码内对这些值进行硬编码时,就显得尤为重要了。

例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些。)

那应该怎么操作呢?

  • 接下来我们使用Vscode gcc编译器演示
    此时SZ还没有定于,我们用命令行定义
//演示命令行定义

int main()
{
    int a[SZ];
    int i = 0;
    for (i = 0; i < SZ; i++)
    {
        a[i] = i;
    }
    for (i = 0; i < SZ; i++)
    {
        printf("%d ", a[i]);
    }
    return 0;
}

我们输入: gcc .\test-vscode.c -D SZ=10 (把SZ定义成10)
在这里插入图片描述
命令行定义是真实存在的。这个只能在gcc编译器才能演示,Vs是无法查看的.


条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
比如说:

调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。

我们继续在VScode里给大家演示:

【C语言】一篇带你玩转 预处理指令_第8张图片

从预处理可以看见
#ifdef DEBUG
#endif //DEBUG
确实把printf那一句屏蔽掉了,我们只要把__DEBUG__定义了就可以解除屏蔽了

当然这里还有更多的条件编译指令,大家可以自行去了解。这里不一 一陈述。


文件包含

#include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样.

这样一个源文件被包含10次,那就实际被编译10次。


头文件被包含的方式:

  • 本地文件包含
    #include “filename”

查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误.

  • 库文件包含
    #include

查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。

这样是不是可以说,对于库文件也可以使用 “ ” 的形式包含?
答案是肯定的,可以。
他先去本地文件查找,在去库文件查找,这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

自己定义的原文件不能用<>,因为我们自己定义的是放在本地文件的。


嵌套文件包含

如果出现这样的场景:【C语言】一篇带你玩转 预处理指令_第9张图片

comm.h和comm.c是公共模块。
test1.h和test1.c使用了公共模块。
test2.h和test2.c使用了公共模块。
test.h和test.c使用了test1模块和test2模块。
这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复。

  • 如何解决这个问题?
  1. 条件编译。
    每个头文件的开头写:
#ifndef __TEST_H__
#define __TEST_H__
 //头文件的内容
#endif   //__TEST_H__
  1. 也是最常用的一种
#pragma once

可以避免头文件的重复引入

你可能感兴趣的:(C语言,c语言,c++,开发语言)