预处理详解

预处理详解

1. 预定义符号

C语⾔设置了⼀些预定义符号,可以直接使⽤,预定义符号也是在预处理期间处理的。

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

运行结果如下:

注意点:VS没有完全遵从ANSI C

2. #define 定义常量

基本语法:

C 复制代码
#define name stuff//#define后面是内容

++一些奇葩的写法++

有一些计算机语言中有switch,但是case后面不使用break

C 复制代码
#define CASE break;case

int main()
{
	int n = 0;
	scanf("%d", &n);
	switch (n)
	{
	case 1:
		//
	CASE 2:
		//
	CASE 3:
		//..
	}
	return 0;
}

做点补充:

C 复制代码
// 如果定义的 stuff过⻓,可以分成⼏⾏写,除了最后⼀⾏外,每⾏的后⾯都加⼀个反斜杠(续⾏
符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )

++思考++

在define定义标识符的时候,要不要在最后加上 ; ?

⽐如:

C 复制代码
#define MAX 1000;
#define MAX 1000

建议不要加上++;++ ,这样容易导致问题。

如下图:在Linux的gcc环境下

很明显能够发现问题

⽐如下⾯的场景:

c 复制代码
if(condition)
max = MAX;
else
max = 0;

如果是加了分号的情况,等替换后,if和else之间就是2条语句,⽽没有⼤括号的时候,if后边只能有⼀ 条语句。这⾥会出现语法错误

3#define定义宏

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

宏其实也是有参数的

下⾯是宏的申明⽅式:

C 复制代码
#define name( parament-list ) stuff//parament-list是参数列表

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

注意

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

来举个小例子:

++用宏来求平方++

C 复制代码
#define square(n) n*n//n是参数,n*n是表达式
#include <stdio.h>
int main()
{
    int x=0;
    scanf("%d\n",&x);
   int ret= square(x);
    printf("%d\n",ret);
    return 0;
}

宏适用于计算那种简单一点的计算

宏是不加计算直接替换进去的

如果我的n是5+1,你可能会想结果是不是36,但是出乎意料的是结果是11,因为它代入到表达式中为5+1*5+1,它没有事先对宏进行计算,而是直接放进去

如果想要计算结果为36该怎么做呢,对n加上括号,如(n)*(n)

写宏的表达式不要吝啬括号

再来个示例

++用宏来求一个数的2倍++

C 复制代码
#define DOUBLE(x)  ((x)+(x))
#include <stdio.h>
int main()
{
    int n=0;
    scanf("%d\n",&n);
    int ret=DOUBLE(n);
    return 0;
}

4. 带有副作用的宏参数

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

例如:

c 复制代码
x+1;//不带副作⽤
x++;//带有副作用

再来展开说就是:

C 复制代码
int main()
{
    int a=10;
    int b=a+1;//b=11;a=10;
    int b=++a;//b=11;a=11; 这里a发生了改变,所以说有副作用
}

来举个例子:

++写一个宏,求两个数的较大值++

c 复制代码
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
    int a=10;
    int b=20;
    int ret=MAX(a,b);
    printf("%d",ret);
    return 0;
}

5 .宏替换的规则

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

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

注意事项:

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

再对上述的例子做一点点变形:

C 复制代码
int main()
{
	int a = 10;
	int b = 20;
	int m = MAX(a++, b++);
	int m = ((a++) > (b++) ? (a++) : (b++));
	       //10    > 20    ?  x    :  21  
	       // a=11 b=22
	printf("%d\n", m);//?
	printf("a=%d b=%d\n", a, b);

	return 0;
}

这个体现出了带有副作用的宏参数

6 .宏函数的对比

宏通常被应⽤于执⾏简单的运算。

⽐如在两个数中找出较⼤的⼀个时,写成下⾯的宏,更有优势⼀些。

C 复制代码
#define MAX(a, b) ((a)>(b)?(a):(b))

那为什么不⽤函数来完成这个任务?

原因有⼆:

  1. ⽤于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多。所以宏⽐ 函数在程序的规模和速度⽅⾯更胜⼀筹。

  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使⽤。反之 这个宏怎可以适⽤于整形、⻓整型、浮点型等可以⽤于 > 来⽐较的类型。宏的参数是类型⽆关 的。

从上图我们可以看到执行函数所需要的步骤是非常多的,比宏的计算多了调用和返回的过程,而宏的计算只有执行函数的核心运算的部分,而且上面这个函数还要检查函数的类型,而宏的参数类型是无关的

和函数相⽐宏的劣势:

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

++宏为什么没法调试呢++

我们都知道一个程序能跑起来需要经过四个过程:++预编译-编译-链接-运行++

调试是在运行阶段,但是预编译到编译的阶段,宏定义的变量会发生替换,也就不是原本的模样,所以不能进行调试

再来看使用宏的例子:

我们之前学过malloc函数分配空间

C 复制代码
int *p=(int*)malloc(10*sizeof(int));

如果说我们用宏的方式来做呢?

C 复制代码
#define MALLOC(n,type) (type*)mallo*sizeof(type));
int*p=MALLOC(10,int);

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

7 .#和##

7.1 #运算符

#运算符将宏的⼀个参数转换为字符串字⾯量。它仅允许出现在带参数的宏的替换列表中。

#运算符所执⾏的操作可以理解为"字符串化"。

做个知识铺垫:

C 复制代码
printf("hello world\n");
printf("hello""world\n");

这两个打印的结果是相同的,因为编译器会自动将其字符串化

C 复制代码
#define PRINT(format, n)    printf("the value of " #n " is " format"\n", n)

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

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

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


	return 0;
}

如果我们不在n前面加上#呢

当我们有⼀个变量 int a = 10; 的时候,我们想打印出: the value of a is 10 .

就可以写:

C 复制代码
#define PRINT(n) printf("the value of "#n " is %d", n);

当我们按照下⾯的⽅式调⽤的时候:

PRINT(a);//当我们把a替换到宏的体内时,就出现了#a,⽽#a就是转换为"a",时⼀个字符串

代码就会被预处理为:

C 复制代码
printf("the value of ""a" " is %d", a);

运⾏代码就能在屏幕上打印:

C 复制代码
the value of a is 10

7 .2##运算符

可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的⽂本⽚段创建标识符。 ## 被称 为记号粘合

这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的。

这⾥我们想想,写⼀个函数求2个数的较⼤值的时候,不同的数据类型就得写不同的函数。

⽐如:

C 复制代码
int int_max(int x, int y)
{
return x>y?x:y;
}
float float_max(float x, float y)
{
return x>yx:y;
}

但是这样写起来太繁琐了,现在我们这样写代码试试:

C 复制代码
//宏定义
#define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{ \
return (x>y?x:y); \
}

使⽤宏,定义不同函数

C 复制代码
GENERIC_MAX(int) //替换到宏体内后int##_max ⽣成了新的符号 int_max做函数名
GENERIC_MAX(float) //替换到宏体内后float##_max ⽣成了新的符号 float_max做函数名
int main()
{
//调⽤函数
int m = int_max(2, 3);
printf("%d\n", m);
float fm = float_max(3.5f, 4.5f);
printf("%f\n", fm);
return 0;
}

输出:

C 复制代码
3
4.500000

使用##就像是套个模板去做月饼,一个个往里面套用

在实际开发过程中##使⽤的很少,很难取出⾮常贴切的例⼦。

8 .命名约定

⼀般来讲函数的宏的使⽤语法很相似。所以语⾔本⾝没法帮我们区分⼆者。

那我们平时的⼀个习惯是:

  • 把宏名全部⼤写

  • 函数名不要全部⼤写

9 .#undef

这条指令⽤于移除⼀个宏定义。

C 复制代码
#undef NAME
//如果现存的⼀个名字需要被重新定义,那么它的旧名字⾸先要被移除。

10 .命令行定义

许多C 的编译器提供了⼀种能⼒,允许在命令⾏中定义符号。⽤于启动编译过程。

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

C 复制代码
#include <stdio.h>
int main()
{
int array [ARRAY_SIZE];
int i = 0;
for(i = 0; i< ARRAY_SIZE; i ++)
{
array[i] = i;
}
for(i = 0; i< ARRAY_SIZE; i ++)
{
printf("%d " ,array[i]);
}
printf("\n" );
return 0;
}

编译指令:

C 复制代码
//linux 环境演⽰
gcc -D ARRAY_SIZE=10 programe.c

小小拓展:

11 .条件编译

简单来说,就是满足提交,参与编译,不满足条件,就不参与编译

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

⽐如说:

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

C 复制代码
#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("%d\n", arr[i]);//为了观察数组是否赋值成功。
#endif //__DEBUG__
}
return 0;
}

常⻅的条件编译指令:

1 .

C 复制代码
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。

如:

C 复制代码
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
C 复制代码
#define M 2
int main()
{
    #if M==2
   printf("hehe\n");
    return 0;
}

此时在Linux的环境,去除了宏定义的内容,只保留了printf("hehe\n")

再来看,如果改成了#define M 3, 那么就因为不满足条件,不好执行

再来看一个:

C 复制代码
int main()
{
    int a=2;
    #if a==2
    printf("hehe\n");
    return 0;
}

这也是不可以的,预处理的过程和变量的创建不在同一时期,变量的创建是在运行阶段的时候,程序能否跑起来包括:预处理-编译-链接-运行

2.多个分⽀的条件编译

C 复制代码
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif

C 复制代码
#define M 3
int main()
{
#if M==1
	printf("hehe\n");
#elif M==3
	printf("haha\n");
#elif M == 4
	printf("heihei\n");
#else
	printf("呵呵\n");
#endif
	return 0;
}

3.判断是否被定义

C 复制代码
#define ZHANGSAN 100

int main()
{
#if defined(ZHANGSAN)
	printf("zhangsan\n");
#endif
	return 0;
}

如果定义了,就可以进行编译

C 复制代码
#if !defined(ZHANGSAN)
#ifndef ZHANGSAN

上面这两个表示不被定义会怎么样

4.嵌套指令

C 复制代码
#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

逻辑关系如下:

++条件编译适用于代码的跨平台性问题++

条件编译也在头文件的命名中有所体现

12.头⽂件的包含

首先来做个小小引入:

头文件的两种形式:

C 复制代码
#include <stdio.h>//库文件包含-一般指标准库头文件的包含
#include "xxxxx.h"//本地文件的包含,一般指自己创建的头文件的包含

12.1 头⽂件被包含的⽅式:

12.1.1 本地⽂件包含
C 复制代码
#include "filename"

查找策略:先在源⽂件所在⽬录下查找,(步骤一)

如果该头⽂件未找到,编译器就像查找库函数头⽂件⼀样在 标准位置查找头⽂件。(步骤二)

如果找不到就提⽰编译错误。(步骤三)

Linux环境的标准头⽂件的路径:

C 复制代码
 /usr/include

VS环境的标准头⽂件的路径:

C 复制代码
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
//这是VS2013的默认路径

注意按照⾃⼰的安装路径去找。

12.1.2 库⽂件包含
C 复制代码
#include <filename.h>

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

这样是不是可以说,对于库⽂件也可以使⽤ "" 的形式包含?

答案是肯定的,可以,但是这样做查找的效率就低些,当然这样也不容易区分是库⽂件还是本地⽂件 了。

Linux环境下的库函数

12.2 嵌套⽂件包含

我们已经知道, #include 指令可以使另外⼀个⽂件被编译。就像它实际出现于 #include 指令的 地⽅⼀样。

这种替换的⽅式很简单:预处理器先删除这条指令,并⽤包含⽂件的内容替换。 ⼀个头⽂件被包含10次,那就实际被编译10次,如果重复包含,对编译的压⼒就⽐较⼤。

如果直接这样写,test.c⽂件中将test.h包含5次,那么test.h⽂件的内容将会被拷⻉5份在test.c中。 如果test.h ⽂件⽐较⼤,这样预处理后代码量会剧增。如果⼯程⽐较⼤,有公共使⽤的头⽂件,被⼤家 都能使⽤,⼜不做任何的处理,那么后果真的不堪设想。

如何解决头⽂件被重复引⼊的问题?答案:条件编译。

每个头⽂件的开头写:

C 复制代码
#ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容
#endif //__TEST_H__

或者

C 复制代码
#pragma once

就可以避免头⽂件的重复引⼊。

注意

在一些笔试题中的涉及:

  1. 头⽂件中的 ifndef/define/endif是⼲什么⽤的?

    条件编译

  2. #include 和 #include "filename.h" 有什么区别?

一个是库文件另一个是本地文件

13 其他预处理指令

思维导图

相关推荐
孤客网络科技工作室1 分钟前
VMware 虚拟机使用教程及 Kali Linux 安装指南
linux·虚拟机·kali linux
百事老饼干8 分钟前
Java[面试题]-真实面试
java·开发语言·面试
customer0816 分钟前
【开源免费】基于SpringBoot+Vue.JS医院管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·开源·intellij-idea
2402_8575893626 分钟前
SpringBoot框架:作业管理技术新解
java·spring boot·后端
HBryce2429 分钟前
缓存-基础概念
java·缓存
一只爱打拳的程序猿44 分钟前
【Spring】更加简单的将对象存入Spring中并使用
java·后端·spring
幸运超级加倍~1 小时前
软件设计师-上午题-16 算法(4-5分)
笔记·算法
杨荧1 小时前
【JAVA毕业设计】基于Vue和SpringBoot的服装商城系统学科竞赛管理系统
java·开发语言·vue.js·spring boot·spring cloud·java-ee·kafka
颇有几分姿色1 小时前
深入理解 Linux 内存管理:free 命令详解
linux·运维·服务器