【深度解刨C语言】预处理(全)

前言

先来两张图复习一下预处理的基本内容
在这里插入图片描述
在这里插入图片描述

结合这两张图我们可以得到

1.每个源文件需单独经过编译器编译,生成目标文件(obj)
2.链接器则是将目标文件和链接库进行链接得到了可执行程序(后缀为.exe)
3.可执行程序实际上是可以运行一个文本文件。

而我们需要引出的新问题是

1.头文件到底包含了什么? 链接的方式有哪两种?
2.注释的替换和宏的替换谁先谁后?如何证明?

补充:程序环境与预处理——想复习之前的预处理可以看这篇文章。

一.预处理

头文件

我们首先要打开头文件看一下里面到底是一些什么?
首先说明:我是在VS2019的版本下,打开的是我们最常用的stdio.h头文件

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

首先说明:我也看不懂这些头文件里面具体包含了什么(菜鸡勿喷),我们只能大致地看一下里面的结构是什么。

结论:头文件大致包含的是头文件,函数的声明,宏定义,预处理指令,类型重定义等

我们通常可能认为库函数都在都头文件中,那我们又要问了,我们包含的如果只是函数的声明,那我们使用的库函数又在哪里呢?答案就在链接当中。那我们可能又要问了,怎么链接呢?

链接器链接的是目标文件与链接库,其它环节都没有包含外部的文件,那答案自然就出来了,函数的定义就在链接库中。除此之外我们知道的链接根据链接库的使用其实分为两种:

静态链接:就是将静态库的内容(我们可以理解成库函数的定义)复制粘贴到可执行文件当中,这里的静态库的删除并不会影响程序的运行。这保证了可执行程序的完整独立性。

动态链接:链接的时候找到要链接的内容,然后与可执行文件产生链接,通过某种方式找到链接的内容并使用,所以删除动态库,可执行程序不能运行。这样多份代码可使用同一个库节省了一定的空间消耗。
总结:静态链接:复制粘贴,完整独立(特点)。动态链接:间接使用,一码多用。

宏与注释

我们先看这个代码:

#include<stdio.h>
#define A //
int main()
{
	A printf("hello world\n");
	return 0;
}

假设法论证:
如果宏先替换,则printf函数内的内容不会执行,反之如果注释先删除,则printf函数的内容会执行。

结果:
注释先删除,printf内容会执行。

图:
在这里插入图片描述

速记小技巧:预处理,预编译,汇编的Linux指令:-E,-S,-C。

二.详解宏

注释

#define A /*
#define B */

解析:
/*先与 * /进行匹配,所以/ *#define B */是注释部分。

字符串里面能用宏吗?

#define MAX 100
int main()
{

	printf("%d\n", MAX);
	printf("MAX\n");

	return 0;
}

图解:
在这里插入图片描述
结论:
字符串里面的宏,实际上是字符串。

空格

# define MAX(a)++a
int main()
{

	int x = 0;
	printf("%d\n", MAX (x));

	return 0;
}

解析:
在这里插入图片描述

正确用宏定义语句

错误的例子

#define Init(a,b) a=100,b=200;
int main()
{

	int x = 0;
	int y = 1;
	if (1)
		Init(x, y);
	else
		x = 1;
	return 0;
}

解释:
在预编译后的式子:


int main()
{

	int x = 0;
	int y = 1;
	if (1)
		x=100,x=200;
		;
	else
		x = 1;
	return 0;
}

说明
if:不加大括号后面只能跟一条语句。
else与最近的一条语句的if所匹配。也就是不能间隔两条语句。

解释:if后面跟了两条语句,其中一个是空语句,else只跟最近的一条语句的if匹配,然而之间还隔了一个空语句,所以匹配不上。

正确的例子:

#define Init(a,b) do{a=100,b=20;}while(0)
int main()
{
	int x = 0;
	int y = 1;

	if (1)
		Init(x, y);
	else
		x=1;
	
	return 0;
}

预编译之后的代码:

int main()
{
	int x = 0;
	int y = 1;

	if (1)
		 do{x=100,y=20;}while(0);
	else
		x=1;
	return 0;
}
解释:do{}while(0);里面可以包含多条语句,而这个循环只会执行一次。这完美解决了宏定义多条语句的问题。

宏的作用域

我们要破除一个常识:
宏只能在函数外定义吗?

习惯上,我们会认为是的,但宏在函数体内也可以被定义。

看这段代码:

int main()
{
#define MAX 100
	printf("%d\n", MAX);
#undef MAX
	return 0;
}

说明:#undef 是取消宏定义

那我们即可确定宏的作用域:
在从定义宏的位置开始,到取消宏结束,这段区间内是宏的作用域。
当没有取消宏时,则作用域从定义开始到文件末。
宏在没有被包含在头文件中时,是不能跨文件使用的。

看下面一段代码:

int add(int x, int y)
{
#define MAX 100
	return x + y;
}
int main()
{

	printf("%d\n", MAX);
#undef MAX
	int x = 0;
	int y = 0;
	int ret = add(1, 2);
	printf("%d\n", MAX);
	return 0;
}

我们应该思考的是处理宏的时间段,与宏的处理方式:
预处理,替换。

试着想一想预处理后的代码:

int add(int x, int y)
{
	return x + y;
}
int main()
{
	printf("%d\n", 100);
	int x = 0;
	int y = 0;
	int ret = add(1, 2);
	printf("%d\n", MAX);
	return 0;
}

补充:最后一个MAX并没有在宏的作用域内,所以编译器并不会识别此MAX。
误区:并不是先调用add函数再识别宏。

结论:
此代码报错。

#和##

1.#

功能:将宏里面的参数进行替换转换成字符串

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

这说明:字符串具有连接属性

而#的属性可以把上面的代码改成这样:

#define X(a) #a
int main()
{
	printf("hello "X(world\n));
	return 0;
}

代码打印结果相同!
注意:
宏的参数是被替换进去的,也就是说不管所传的参数有没有意义,都会被转换为字符串!

功能:将符号进行粘连,形成一个新的符号(必须是合法的

#define STICK(a,b) a##b
int main()
{

	int beauty = 10;
	printf("%d", STICK(bea, uty));
	return 0;
}

结果:10
我们甚至可以用它来定义变量名

#define X(a) x##a
int main()
{
	int X(1)=10;//实际上是:int x1 = 10;
	printf("%d\n", X(1));
	return 0;
}

定义宏的建议

1.尽量使用大括号保证宏的独立性
反例:

#define MAX(a,b) a+b
int main()
{
	printf("%d\n", 2 * MAX(1, 2));
	//             2 *1 + 2= 3+2 = 5
	return 0;
}

不带括号,会导致宏的结果会被外面的参数所影响。

2.避免使用++或–这类带副作用的运算符
在这里插入图片描述

宏的本质是:替换

3.宏定义使用全大写字母

这是我们区分宏和函数的命名约定

三.条件编译

1.常量表达式

#if //常量表达式
//...
#endif
//常量表达式由预处理器求值。
//如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif

2.多分支

#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif

3.判断是否被定义

#if defined(symbol)
//……
#endif
#ifdef symbol
//……
#endif
#if !defined(symbol)
//……
#endif
#ifndef symbol
//……
#endif

4.嵌套指令

#define A 100
#define B 200
#define C 300
#define D 400
#if defined(A)
	#ifdef B
		//……
	#endif
	#ifdef C
		//……
	#endif
#elif defined(D)
	#ifdef B
		//……
	#endif
#endif

注意:
条件编译处理的阶段:预处理阶段

条件编译语句都以:#endif结尾

条件编译语句:可以出现在函数体外

#if后加 一个宏表示的是此宏的值

例:

#define A 0
int main()
{
#if A//这里实际上是0,所以此代码不执行
	printf("you can see me!\n");
#endif
	return 0;
}

说明:条件编译可以让一码多用,也就是一段代码在合适的场景满足条件就执行,不满足就不执行,这样不同版本只需要维护一份代码即可。

头文件避免重复包含的方法:(用条件编译)

#ifndef __TEST__
	#define __TEST
	//……代码块
#else
	//啥也不写
#endif

解释:代码在第一次包含时需满足 __ TEST__未被定义, 这样我们在第一次包含的时候会把 __ TEST__定义,同时包含目标代码,下一次包含,由于__TEST__已经定义,则啥也不包含。

补充:头文件是能重复包含的,但重复包含会使代码冗余
头文件避免重复包含另一种方法是:#pragma once

四.include

#include是预处理指令,在预编译时,就已经被编译器处理好了。

举一个包含头文件的例子:

在这里插入图片描述

总结:推荐 ""包含自己项目下的头文件,<>包含库里的头文件

五.error与pragma

//#error 你好啊!
#pragma message(你好啊!)
int main()
{
	return 0;
}

这两条指令的区别在于:error在编译期间就报错了,message这句指令只是提醒
相同之处:都是让编译器给你信息。

技巧:
在这里插入图片描述
我们在VS使用scanf函数时,编译器会报错,因为此函数不安全,一个解决方法是定义宏

#define _CRT_SECURE_NO_WARNINGS 1

另一种:

#pragma warning(disable:4996)

预定义符号

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

  • 7
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值