c语言-预处理详解【求个关注!】


一 预处理阶段

1 知识背景:

一个c语言项目可能由多个.c文件与.h文件组成,那么它是如何转换成可执行程序的呢?
每一个.c文件单独通过编译器转换成目标文件
在windows系统中,目标文件的后缀为.obj,在linux系统中,目标文件的后缀为.o
通过链接器,将多个目标文件与链接库链接起来,生成可执行程序
链接库是指运行时的库(支持程序运行的基本函数的集合)或者第三方库(比如c语言标准库)
用高级编程语言所写的代码,要执行,需要在转换成操作系统可执行的二进制
代码
大体的转换流程是:编译-链接 转换成可执行程序
如果再细分,则是预处理-编译-汇编-链接 转换成可执行程序

如图:

在这里插入图片描述
本篇所写仅涉及与预处理阶段相关的内容【处理的代码方面】

2 预定义符号

c语言中定义了一些预定义符号,这些预定义符号可以直接使用,当然这些预定义符号也在预处理阶段处理!

//_ _FLIE_ _  //用来编译的源文件  
//__LINE__  //在当前文件中被编译的代码行号
//__DATE__  //文件被编译的日期
//__TIME__  //文件被编译的时间
//__STDC__  如果编译器完全实现 ANSI C 则值为1,否则未定义 
// 至于这个是关于什么的信息,我不清楚
#include<stdio.h>
int main() {
	printf("%s\n", __FILE__);
	printf("%d\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
//	printf("%d\n", __STDC__);//在VS编译器中并没有STDC的规定!
	return 0;
}

在这里插入图片描述

在预处理之后,生成的文件test.i 为:
在这里插入图片描述

3 #define 定义常量

在程序中#define 定义的符号,在经过预处理之后,直接被其值替换
#include<stdio.h>
#define MAX 100           // 定义符号 值为数值常量
#define Str  "hello World"// 定义符号 值为字符串常量
#define MA  'a'           // 定义符号 值为字符
int main() {
// #define 可以定义符号,其值为常量
	printf("%d\n", MAX);
	printf("%s\n", Str);
	printf("%c\n", MA);
	return 0;
}

在这里插入图片描述

当定义的标识符的值过长时:

#define Print printf("file:%s\tline:%d\tDATE:%s\tTIME:%s\t",__FILE__,__LINE__,__DATE__,__TIME__);

#include<stdio.h>
//在默认情况下#define定义标识符只能在一行中定义,但如果想换行的话,则需在每一行末尾加上 \,意为这一行的扩展
#define Print printf("file:%s\t \
line:%d\t                   \
DATE:%s\t                    \
TIME:%s\t",                   \
__FILE__,__LINE__,__DATE__,__TIME__);
int main() {
   
	return 0;
}

注意,如果#define定义的标识符,其值的末尾有; 则说明; 是该标识符值的一部分

在这里插入图片描述

4 #define 定义宏

宏是带有参数的标识符,参数与标识符对应的值有关

宏的声明方式:

#define name(parament-Iist ) stuff 
其中stuff是宏体,parament-list代表用,隔开的参数集
注意:name与()的左括号,中间不能有间隔,否则编译器会认为(parament-Iist)是宏体的一部分

当传入的参数是一个符号时:

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

在这里插入图片描述

当传入的参数是一个表达式时:

#include<stdio.h>
#define SQUARE(x) x*x
int main() {

	int c = SQUARE(5+1);
	printf("%d\n", c);
	//预期值是36 ,但结果是;
	return 0;
}

在这里插入图片描述
这是因为,在经历预处理时,先将宏的形参变为实参变为:

// 5+1*5+1

再将转换后的文本替换到原来程序所在的文本的位置即:

#include<stdio.h>
#define SQUARE(x) x*x
int main() {

	int c = 5+1*5+1;
	//此时的结果为1*5 + 6 ==11
	printf("%d\n", c);
	
	return 0;
}

如何解决这个问题呢?只需要在宏的定义中,将每一个参数用()括起来,再将整个宏的体括起来
这是为了防止外在的程序直接与宏的部分体进行计算,例如:

#include<stdio.h>
#define ADD(x) x+x
int main() {
	int a = 5;
	//目标打印出50,但是结果为:
	printf("%d\n", ADD(5) * a);
	return 0;
}

在这里插入图片描述
解决方法:

#include<stdio.h>
#define ADD(x) ((x)+(x)) // 将参数括起来,再将整个宏体括起来
int main() {
	int a = 5;
	printf("%d\n", ADD(5) * a);
	return 0;
}

在这里插入图片描述

带有副作用的宏参数

当带副作用的参数(所谓副作用即在计算结束后,自身的值发生变化)在宏体中,超过一次,
就会可能导致错误的结果

例:

#include<stdio.h>
#define MAX(x,y) ((x)>(y)?x:y)

int main() {
	int a = 3;
	int b = 5;
	//如果是按照函数的思维,c 返回值应该是5,但是结果是:
	int c = MAX(a++,b++);
	printf("%d\n", a);
    printf("%d\n", b);
	printf("%d\n", c);
	return 0;
}

在这里插入图片描述

所以尽量不要用这种带有副作用的形参
x++//有副作用
x+1//挺好

宏替换的规则

有以下几个步骤:
1   首先扫描宏的参数,如果参数中有#define定义的符号或宏,则替换成对应的值
2   随后将替换文本插入到程序中原文本处
3   最后重新扫描结果文本,将#define定义的宏或符号转换成宏体或值

举例:

#include<stdio.h>
#define MAX(x,y) ((x)>(y)?(x):(y))
#define M 10 
int main() {
	int a = 3;
	int b = 5;
	
	int c = MAX(M, a);
	
	// 预处理时,1 首先扫描宏的参数,发现#define定义的M,将其转换成其值 10:
	         // 2   int c = MAX(10,a);这条语句代替int c = MAX(M,a);插入到程序中
	        // 3  最后再扫描结果文本 int c = MAX(10,a);发现MAX也是#define定义,
	        //转换为其对应的宏体
            //    ((10)>(a)?(10):(a))
// 最后变为:
  //int c = ((10)>(a)?(10):(a));

	return 0;
}
注意:1  宏参数或#define定义的符号中可以出现其他#define定义的符号或宏,但是对于宏不能出现递归
           2   当预处理器搜索#define定义的符号与宏时,字符串常量的内容并不被搜索!

5 宏与函数的对比

(1)在执行小型运算时,宏更有优势:

举例:

#include<stdio.h>
//#define MAX(x,y) ((x)>(y)?(x):(y))
int Max(int x, int y) {
	return x > y ? x : y;
}
int main() {
	int a = 3;
	int b = 5;
	//int c = MAX(a, b);
	int c = Max(a, b);
	return 0;
}

用函数所需的指令:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上面是调用函数执行的指令,共用了11条

这是执行函数中的语句所用的指令:
在这里插入图片描述
这是返回函数值所执行的指令:
在这里插入图片描述
在这里插入图片描述
用宏需要的指令:
在这里插入图片描述

结果一目了然,说明当功能运算语句占比较小时,宏的执行更能够节省时间

(2)宏对于函数的劣势

1        除非宏比较小,否则宏替换到程序中的代码量极大,预处理之后会产生极大的冗余代码
        而函数则只有固定的一份代码
2       宏是无法调试的,因为我们看到的代码与预处理替换后的代码不同,我们不能查看到问题出现在哪里
3       宏的参数没有类型,不够严谨(当然这也是宏对于函数的优势,只能说是一把双刃剑)

(3)总的对比

在这里插入图片描述

6 #与##操作符

6.1 #操作符

此操作符的作用将宏的参数转换成**字符串字面量**(就是转换成参数的标识符的字符串形式) 
它仅允许出现在带参数的宏体中(或者说是宏的替换列表中)

举例:

// 在举例之前,先补充一个背景知识点
#include<stdio.h>
int main() {
	printf("helloworld\n");
	// 如果用两对双引号打印呢?
	printf("hello"   "world\n"); 
	return 0;
}

在这里插入图片描述

#include<stdio.h>
//用#操作符将参数n变为字符串字面量,即参数的标识符的字符串状态
#define DIGIT(n) printf("the value " #n " is %d\n",n); 
int main() {
	int a = 3;
	//在传入实参后,替换的文本实际为:
	//printf("the value " "a" "is %d\n", a);
	DIGIT(a);

	return 0;
}

在这里插入图片描述

6.2 ## 操作符

##操作符可以把它两边的符号合成一个符号,它允许宏定义从分离的文本片段
中创建标识符,这样的连接必须产生一个合法的标识符

在这里插入图片描述

7 #undef

用于移除一个宏定义
#undef NAME  // 如果移除一个宏定义,移除其名字即可
#include<stdio.h>
#define SQUARE(x) x*x
int main() {

//#undef SQUARE	
	printf("%d\n", SQUARE(3));

	return 0;
}

在这里插入图片描述

#include<stdio.h>
#define SQUARE(x) x*x
int main() {

#undef SQUARE	
	printf("%d\n", SQUARE(3));
	return 0;
}


在这里插入图片描述

8 条件编译

我们可以通过一些语句来设定一些代码是否进行编译,这样即避免删掉可能有用的代码,又防止暂时不需要的代码消耗资源

举例:

#include<stdio.h>
#define DEBUG
int main() {
	int arr[10] = { 0 };
	for (int i = 0; i < 10; i++) {
		arr[i] = i;

#ifdef DEBUG  // 如果DEBUG被定义则下面这条语句可执行,反之不可
		printf("%d\n", arr[i]);

#endif 
	}
	return 0;
}

常见的条件编译指令

1    格式: #if  常量表达式
        #endif
  #if作为开始的标志,#endif作为结束的标志,如果常量表达式的返回值>0
  则指令中间的代码块便可执行

举例:

#include<stdio.h>
#define M 10
int main() {
#if M>0
	printf("hehe\n");
#endif // M>0

	return 0;
}

在这里插入图片描述

   2   多分支的条件编译指令

举例:

#include<stdio.h>
#define M 3
int main() {
#if M==1
	printf("hehe");
#elif M==2
	printf("haha");
#else
	printf("wawa");
#endif // M=1
	return 0;
}

在这里插入图片描述

3       判断是否被定义的条件编译
#include<stdio.h>
#define DEBUG
int main() {
	int arr[10] = { 0 };
	for (int i = 0; i < 10; i++) {
		arr[i] = i;
//#if defined(symbol) #endif 组合 与#ifdef symbol #endif 组合的功能相同,
// 如果symbol被定义,则指令中的代码块可执行
		
#if defined(DEBUG)
//#ifdef DEBUG 
		printf("%d\n", arr[i]);

//#endif 
#endif 

	}
	return 0;
}

3.2    如果是不定义情况下可执行代码的条件编译:
#include<stdio.h>
#define M 10
int main() {
#ifndef M
	printf("haha");
#endif // !M

	return 0;
}

在这里插入图片描述

 4  嵌套指令,前面的几个指令可以嵌套,就像是if else语句一样,
 大家可以尝试一下

9 头文件的包含

头文件是存放函数声明的文件,让其他函数调用使用

9.1 头文件包含的形式:

9.1.1 本地文件的包含:
 当我们引用本地文件时,即自己写的文件,格式为:
 #include " FileName"

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

9.1.2 库文件的包含

当我们调用开发环境为我们提供的标准库函数时,就需要引用相应的头文件

格式为:#include <FileName>

比如调用下面的printf函数,就需要包含stdio.h文件
在这里插入图片描述

两种包含方式查找策略的不同:
1   对于用" " 包含头文件的形式,在查找相应的头文件时,先在本文件的
    目录中查找,如果找到了则包含进来,如果没找到则去存放库函数的
    位置所在的路径去查找。
2   而用<>包含头文件的形式,在查找相应的头文件时,直接去存放库函数
的位置所在的路径去查找
 所以库文件也可以用" " 来包含,但是执行起来比<>慢

举例1:在这里插入图片描述
找到了.h文件
在电脑中找到存放stdio.h头文件的路径
在这里插入图片描述

9.2 嵌套文件包含

当我们在调用头文件时,可能不止调用一个 ,如果出现这种情况:

在这里插入图片描述
如果文件的调用结构是这样的,那对于test.c来说,是调用了两次con.h
如果结构更复杂呢?会导致代码量的极大冗余
这个问题怎么解决呢?用条件编译!
举例:

#ifndef DEBUG
#define DEBUG
int Add(int x, int y);
#endif
// 这些代码的执行规则是:如果.h文件第一次被调用,那此时DEBUG还没有被定义
// 就定义DEBUG,然后将文件包含过去,即将声明赋值过去,当.h文件又被调用时,则
// 因为DEBUG已经被定义,所以指令中间的代码不被编译,不会被包含过去,而总项目
// 只需要声明这个.h文件一次即可,这样就避免了重复声明!

在这里插入图片描述
在这里插入图片描述
或者用

#pragma once 
来避免头文件的重复引入

在这里插入图片描述

  • 32
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值