【C语言】预处理

2024-06-06 20:53:05 浏览数 (2)

一、预处理符号

代码语言:javascript复制
__FILE__ //进⾏编译的源⽂件
__LINE__ //⽂件当前的⾏号
__DATE__ //⽂件被编译的⽇期
__TIME__ //⽂件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义

以上是C语言设置的一些预定义符号,是可以直接使用的,预定义符号在预处理阶段处理

二、#define定义常量

基本语法:

代码语言:javascript复制
#define name stuff

例子:

代码语言:javascript复制
#define MAX 1000  //将MAX赋值为1000
#define reg register //为 register这个关键字,创建⼀个简短的名字reg
#define do_forever for(;;) //⽤更形象的符号来替换⼀种实现
//for(;;)是无条件的for循环,是一个死循环
#define CASE break;case //在写case语句的时候⾃动把 break写上。

当我们在使用#define的时候,变量有两项,一是name,二是stuff,而stuff中的值将会代替代码中所有的name,相当于是办公软件word里边的替换,所以我们遇到以下的问题,就可以一下解决出来:

代码语言:javascript复制
#define MAX 1000;
#define MAX 1000

我们说,这两种被定义的方式是不同的,上边的第一个定义可以用来完成以下代码:

代码语言:javascript复制
#define MAX 1000;
#include <stdio.h>
int main()
{
    int a = MAX
    return 0;
}

第二个可以完成以下代码:

代码语言:javascript复制
#define MAX 1000
#include <stdio.h>
int main()
{
    int a = MAX;
    return 0;
}

都是可以的,但是我们要注意,当我们想要直接用printf输出MAX的值时,用第一个是不可以的 这是使用第二个定义,正确的做法:

代码语言:javascript复制
#define MAX 1000
#include <stdio.h>
int main()
{
    printf("%d",MAX);
    return 0;
}

使用第一个定义:

代码语言:javascript复制
#define MAX 1000;
#include <stdio.h>
int main()
{
    printf("%d",MAX);
    return 0;
}

我们在这里发现,MAX被替换成了1000; 1000;是不能通过printf打印的 所以我们得出一个结论:在使用#define定义数字时,尽量不要加入符号

三、#define定义宏

#define机制包括了一个机制,就是允许把参数替换到文本中,这种实现通常称为宏或者定义宏 宏的声明方式:

代码语言:javascript复制
#define name( parament-list ) stuff

parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中 name与旁边的参数列表的左圆括号必须紧邻,如果二者之间有任何空白存在,参数列表就会被认为是stuff的一部分

举一个求平方的例子:

代码语言:javascript复制
#define SQUARE( x ) x * x

当我们使用SQUARE( 9 )时,编译器就会将它替换成9*9

注意: 在使用宏定义的时候,我们为了不让我们所定义的量出现错误,最好给每个变量都加上括号,不然就会出现错误

例子:

代码语言:javascript复制
#define SQUARE( x ) x * x
#include <stdio.h>
int main()
{
    int a = 5;
    printf("%dn" ,SQUARE(a   1) );
    return 0;
}

这会发现一个错误,替换之后变成5 1*5 1,最终的答案是11而不是36 所以最好给每个量都套一个括号

修改后:

代码语言:javascript复制
#define SQUARE( x ) ((x) * (x))
#include <stdio.h>
int main()
{
    int a = 5;
    printf("%dn" ,SQUARE(a   1) );
    return 0;
}

四、带有副作用的宏参数

代码语言:javascript复制
#define x  ;

上面这个宏就是一个副作用宏,因为替换之后会持续造成作用,这样就可能会导致危险,简单来说,副作用就是表达式求值的时候会出现的永久性效果 我们来举一个例子

代码语言:javascript复制
#define MIN(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 6;
y = 9;
z = MIN(x  , y  );//z = ((x  ) < (y  ) ? (x  ) : (y  ))
printf("x=%d y=%d z=%dn", x, y, z);

结果是x=8 y=10 z = 7 我们先来计算<左右两边的值,x 是先赋值再 ,y 也是先赋值再 ,x<y,所以执行问号后边的语句,即x 先赋值再 ,此时x=7,y=10,所以z=7,然后 ,x=8,最终得到答案就是上述答案

五、宏替换的规则

1、调用宏的时候,首先对参数进行检查,看是否包含任何由#define定义的符号,如果是,他们首先被替换,也就是首先替换参数 2、然后替换文本随后被插入到程序中原本文本的位置,对于宏,参数名被它们的值所替换,也就是把宏定义的值替换被替换的值 3、最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号,如果是,就重复上述处理过程,也就是再次扫描然后重复上述过程 4、宏参数和#define定义中可以出现其他#define定义的符号,但是宏是不能够递归的 5、在字符串中的#define定义的符号不能被替换

六、宏与函数的对比

(一)、宏的优势

当我们要进行一些简单的计算时,使用宏替换比函数更有优势一些 1、因为不管是简单的还是复杂的计算,使用函数都会在栈中开辟一块空间(在我们之前的博文函数栈帧的创建和销毁一文中有详细的内容,大家有兴趣可以看一下),然后还有销毁空间,在开辟空间之前会有开辟空间之前的指令,这会增长运行时间,而反观用宏替换的方式,直接将代码替换,省去了开辟空间的时间,速度更快 2、使用函数要声明数据类型,所以一个函数只能由特定数据类型的数据使用,但是宏可以使用任意的数据,宏的参数与类型无关,只是一个简单的替换 3、宏的参数可以是类型,函数不行 例子: ( 这个符号是连字符,如果代码内容较长,可以用连字符来连接,程序生成的过程中会自动将 去掉并连接上下)

代码语言:javascript复制
#define MALLOC(num, type)
 (type*)malloc(num*sizeof(type))
int main()
{
    MALLOC(5,int);//(int*)malloc(5*sizeof(int));
    return 0
}

(二)、宏的劣势

再进行复杂计算时,使用函数会更有优势一些 1、每次使用宏的时候,宏定义的代码会插入到程序中,在宏较长的情况下可能会导致大幅度增加程序的长度 2、宏无法调试 3、宏与类型无关,这虽然是它的一个优点,也是一个缺点,因为这导致它不够严谨 4、宏可能会带来运算优先级问题,如上面第三条所说,容易导致程序出错

(三)、宏和函数的对比

属性

#define定义宏

函数

代码长度

每次使用时,宏代码都会被插入到程序当中,除了非常小的宏之外,程序的长度会大幅度增长

每次使用函数时,都调用同一个地方的同一份代码

执行速度

更快

存在函数栈帧的创建和销毁,相对于宏会慢一些

操作符优先级

宏在书写的时候要多加括号,否则会因为临近操作符优先级不同,使目的与代码不匹配的问题

表达式的求值容易被预测,不会出现宏一样的前后操作符优先级问题

带有副作用的参数

参数可能会被替换到程序中的多个位置,如果宏的参数被多次计算,带有副作用的参数求值可能会造成风险

函数参数只在传参的时候求值一次,结果容易被控制

参数类型

宏的参数与类型无关,只要对参数的操作合法,就可以使用任何数据类型

函数的参数与类型有关,必须严格按照参数类型来进行使用,不同参数类型不同,所需要的函数就不同,尽管它们的任务相同

调试

不能调试

可逐句调试

递归

不能递归

可以递归

七、#和##

1、#运算符

#运算符可以将宏的一个参数转换为字符串字面量,它仅允许出现在带参数的宏的替换列表中 简单来说它的功能就是字符串化 例子: 当我们想打印出来一个数字的大小:the value of number is 6 我们可以这样做:

代码语言:javascript复制
#define PRINT(n) printf("the value of "#n " is %d", n);
int main()
{
    int number = 6;
    PRINT(number);
    return 0;
}

它在使用时,#n就是#number,#number就是转换成"number",这时字符串内的代码就会被预处理为:

代码语言:javascript复制
printf("the value of ""number" " is %d", number);

然后正常执行代码,就得到了the value of number is 6

2、##运算符

##被称为记号粘合,它可以把左右两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符,当然这样的连接是要产生一个合法的标识符的,否则其结果就是未定义的,我们可以用这个运算符来写一个类似于宏的函数,这样的函数是可以定义自由定义数据类型的,使用起来是非常方便的

代码语言:javascript复制
#define GENERIC_MIN(type) 
type type##_min(type x, type y)
{ 
 return (x<y?x:y); 
}
代码语言:javascript复制
GENERIC_MIN(int) //替换到宏体内后int##_min ⽣成了新的符号 int_min做函数名
GENERIC_MIN(float) //替换到宏体内后float##_min ⽣成了新的符号 float_min做函数名
int main()
{
 //调⽤函数
 int m = int_min(6, 3);
 printf("%dn", m);
 float b = float_min(1.5f, 4.5f);
 printf("%fn", b);
 return 0;
}

八、命名约定

函数与宏的使用比较相似,我们这里约定俗成的规则就是将宏全部大写,函数部分大写,然后其他代码使用小写,这样可以很好的区分宏、函数以及其他代码

九、#undef

#undef 可以移除一个宏定义,如果现存的一个名字需要被重新定义,那么就使用它进行移除

代码语言:javascript复制
#undef NAME

十、命令行定义

许多C的编译器提供了在命令行中定义符号的能力,用于启动编译过程 在这里我们可以调节数组的大小,或者循环次数的大小等

十一、条件编译

我们平常写代码的时候,我们不清楚所写的代码是否能够实现目标时,我们往往会对某一个某块进行调试,但有一些代码是专门用来调试时加上的,删了有些可惜,保留又碍事,这时我们就可以选择性的编译,使用条件编译指令

例子:

代码语言:javascript复制
#include <stdio.h>
#define __DEBUG__
int main()
{
	int i = 0;
	int arr[10] = { 0 };
	for (i = 0; i < 10; i  )
	{
		arr[i] = i;
#ifdef __DEBUG__
		printf("%dn", arr[i]);
#endif
	}
	return 0;
}

这里的printf函数用来检查赋值是否成功,#ifdef用来检查后边的指令是否被定义,如果被定义了,那么就进行编译,如果未被定义则编译,调试结束之后将#define语句注释掉就行了

常见的条件编译指令

代码语言:javascript复制
//1.
#if 常量表达式
 //...
#endif
//常量表达式由预处理器求值。
//如:
#define __DEBUG__ 1
#if __DEBUG__
 //..
#endif
//2.多个分⽀的条件编译
#if 常量表达式
 //...
#elif 常量表达式
 //...
#else
 //...
#endif
//3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
//4.嵌套指令
#if defined(OS_UNIX)
 #ifdef OPTION1
 unix_version_option1();
 #endif
 #ifdef OPTION2
 unix_version_option2();
 #endif
#elif defined(OS_MSDOS)
 #ifdef OPTION2
 msdos_version_option2();
 #endif
#endif

十二、头文件的包含

1、头文件被包含的方式

①本地文件包含
代码语言:javascript复制
#include "filename"

查找策略:先在源文件所在目录下查找,如果未找到,就在标准位置查找,即库函数所在的位置,如果找不到就提示编译错误

②嵌套文件包含

我们知道,每一条代码就可能使用一块空间,如果我们在一个大的程序里边写代码时,我们可能多次包含同一个头文件,那么包含了几次,这条代码就编译几次,极大的影响效率,我们可以通过使用条件编译避免头文件的重复引入

代码语言:javascript复制
#ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容
#endif

代码语言:javascript复制
#pragma once

十三、其他预处理指令

c语言给我们很多预处理指令,我们工作的过程中可能会用到,大家自行查找学习

今日分享就到这里了~

0 人点赞