当前位置: 首页 > news >正文

C语言之预处理

作者:从未止步…

博客主页:从未止步的博客…

专栏:和我一起学C

语录:Every day is a second chance

行动是理想最高贵的表达 ,给大家介绍一款超牛的斩获大厂offer利器——牛客网

快来和我一起刷题吧

在这里插入图片描述

程序的翻译环境和执行环境:

在ANSI的任何一种实现中,存在两个不同的环境。
在这里插入图片描述

第一种是翻译环境,在这个环境中源代码被转换为可执行的机器指令,第二种是执行环境,它用于实际执行代码。

而程序的编译过程如下:

在这里插入图片描述组成一个程序的每个源文件通过编译过程分别转换成目标代码。

每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序。

链接器同时会引入标准C函数库中任何该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

在这里插入图片描述

翻译环境:

感兴趣的同学可以去看这篇文章,说的挺详细的哈

http://t.csdn.cn/E0I8x

运行环境:

程序执行的过程:

1:程序必须载入内存中,在有操作系统的环境中:一般这个由操作系统完成,在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。

2:程序的执行就已经开始了,接着是调用main函数。

3:开始执行程序代码,这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址,程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序执行过程一直保留他们的值。

4:终止程序,正常终止main函数,也有可能是意外终止。

预处理:

预定义符号:

FILE //进行编译的源文件

#include<stdio.h>
int main()
{
	printf("%s\n", __FILE__);
	return 0;
}

输出如下:
在这里插入图片描述

LINE //文件当前的行号

在这里插入图片描述
输出如下:

5

DATE//文件被编译的日期

#include<stdio.h>
int main()
{
	printf("%s\n",__DATE__);
	return 0;
}

输出如下:

在这里插入图片描述

TIME//文件被编译的时间

#include<stdio.h>
int main()
{
	printf("%s\n",__TIME__);
	return 0;
}

输出如下:

在这里插入图片描述

STDC//如果编译遵循ANSI C,其值为1,否则未定义

vs编译器下是不支持C语言标准的,因此__STDC__会直接显示未定义。

但在gcc编译器下是 支持的。
在这里插入图片描述

那么这些预定义符号到底有什么作用呢?

事实上,我们可以通过这些符号,将文件的信息写入日志记录下来。

举例:

#include<stdio.h>
int main()
{
	int i = 0;
	int arr[10] = { 0 };
	FILE* pf = fopen("log.txt", "w");
	for (i = 0; i < 10; i++)
	{
		arr[i] = i;
		fprintf(pf, "file:%s line:%d date:%s time:%s i=%d\n",__FILE__,__LINE__,__DATE__,__TIME__,i);//写入文件信息

	}
	fclose(pf);
	pf = NULL;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

当我们打开log.txt文件后,会发现关于程序的一些信息都被写入了log.txt文件,方便以后查阅。

在这里插入图片描述

0 1 2 3 4 5 6 7 8 9

预处理指令:

#define

#include

#pragma pack(4)

#pragma

#if

#endif

#ifdef

#line

举例:

#define:定义标识符

语法:#define name stuff

举例:

#include<stdio.h>
#define str1 "你好"//定义字符串
#define number 9//定义数字
int main()
{
	printf("%s ",str1);
	printf("%d\n", number);
	return 0;
}

输出如下:

你好 9

#define不仅可以用来定义数字和字符串,还可以用来定义关键字的别名等等。

那么#define后面是否要加分号吗?

举例:

在这里插入图片描述

此时编译不会通过,会出现语法错误,但在预处理阶段,我们很难发现代码的问题,只有在进行编译时,将定义的标识符的值进行替换后,编译器才会报错,因此,在以后写代码的过程中,#define后面不要加分号。

#define:定义宏

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

下面是宏的申明方式:

#define name(parament—list)stuff 其中的parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中。

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

举例:

#include<stdio.h>
#define number(x) x+x ;//x用10替换,实现10+10
int main()
{
	int ret = number(10);
	printf("%d\n", ret);
	return 0;
}

输出如下:

20

举例:

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

按照上面的方式将x替换为5+1,那么xx的值为36,但事实输出结果并不是36,而是11,原因是宏不是进行传参的,而是进行替换的,因此实际计算为5+15+1,输出结果为11

那么应该怎么杜绝这种情况呢?

宏修改如下:

#define number(x) (x)*(x) ;

传递过去,计算为(5+1)*(5+1),此时输出结果为36.

因此对于数值表达式进行求值的宏定义都应该使用注意不要出现由于忘记加括号而出现问题这种情况。

#define替换的规则:

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

1:在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号,如果是,他们首先被替换。

2:替换文本随后被插入到程序中原来文本的位置,对于宏,参数名被他们的值替换。

3:再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号,如果是,就重复上述处理过程。

注意:

1:宏参数和#define定义中可以出现其他#define定义的变量,但是对于宏,不能出现递归。

2:当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

举例:

#define number 10
#define numbers(x) (x)*(x) ;
int main()
{
	int ret=10*numbers(number)
	printf("number=%d\n", number);//字符串常量内容和定义的符号相同。
	printf("%d\n", ret);
	return 0;
}

字符串有自动连接的特点。

int main()
{
	printf("he" "llo" " world\n");
}

输出

hello world

如何把参数插入字符串中?

只有当字符串作为宏参数的时候才可以把字符串放在字符串中。

举例:

#include<stdio.h>
#define PRINT(format,value) printf("the value is "format"\n", value);
int main()
{
	PRINT("%d", 10);
}

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

#include<stdio.h>
#define PRINT(format,value) printf("the value of "#value" is "format"\n",value);
int main()
{
	int i = 10;
	PRINT("%d", i+3);
}

输出:

the value of i+3 is 13

##的作用:

举例:

#include<stdio.h>
#define STR(X,Y) X##Y
int main()
{
	int str1 = 90;
	printf("%d\n", STR(str, 1));
	//##可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。
	//printf("%d\n",STR(str##1);
	//printf("%d\n",str1);
	return 0;
}

输出

90

注:这样的连接必须产生一个合法的标识符,否则其结果就是未定义的。

带副作用的宏参数:

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么在使用这个宏的时候就可能出现危险,副作用即为表达式求值的时候出现的永久性效果。

举例:

#include<stdio.h>
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
int main()
{
	int a = 10;
	int b = 11;
	int max = MAX(a++, b++);
	//将a++和b++直接传递到宏,而不是将其计算结果替换进去:MAX((a++)>(b++)?(a++);(b++)) 
	//MAX((10)>(11)?(a++);(b++))注:后面的b++第一次加一为前面11的b++,而第二次b++是为后面产生比较结果的b++
	printf("%d\n", max);//12
	printf("%d\n", a);//11
	printf("%d\n", b);//13
	return 0;
}

输出:

产生这种结果的原因即为x++带有副作用,虽然没有直接的体现在参数上,但这种副作用往往会体现在表达式求值中。

12
11
13

宏和函数的对比:

宏通常被应用于执行简单的运算,比如在两个数中找出较大的一个。

#define MAX(a,b)((a)>(b)?(a):(b))

那么为什么不使用函数来执行这个任务?

原因有两个:

1:用于调用函数和从函数返回的代码可能比实际执行这个小型计算机工作所需要的时间更多,所以宏比函数在程序的规模和速度方面更胜一筹。

2:更为重要的是函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用,反之,这个宏既可以适用于整形,长整型,浮点型等可以用于>来比较的类型,宏是无关类型的。

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

1:每次使用宏的时候,一份宏定义的代码将插入到程序中,除非 宏比较短,否则可能大幅度增加程序的程度。

2:宏是无法进行调试的。

3:宏由于类型无关,也就不够严谨。

4:宏可能会带来运算符优先级的问题,导致容易出现错误。

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

命名约定:

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

把宏名全部大写,函数名不要全部大写

在这里插入图片描述在这里插入图片描述在这里插入图片描述#undef:用于移除一个宏定义。

如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。

命令行定义:

许多C的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程,例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假设某个程序中声明了某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写)。

编译指令:

gcc -D ARRAY_SIZE programe.c

条件编译:

在编译一个程序的时候,我们如果要将一条语句(一组语句)编译或者放弃是很方便的,可以通过条件编译指令。

比如:调试性的代码,删除可惜,保留又碍事,那么我们可以对其进行选择性的编译。

举例:

#include<stdio.h>
int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9};
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		arr[i] = 0;
#ifdef DEBUG//如果DEBUG在此之前被定义过,那么ifdef和endif之间的语句会被执行,否则不会执行。
		printf("%d ", arr[i]);
#endif
	}
	return 0;
}

上述代码将无任何输出。

那么如果想输出printf的内容,我们可以定义DEBUG。

代码修改如下所示:

#define DEBUG
#include<stdio.h>
int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9};
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		arr[i] = 0;
#ifdef DEBUG//如果DEBUG在此之前被定义过,那么ifdef和endif之间的语句会被执行,否则不会执行。
		printf("%d ", arr[i]);
#endif
	}
	return 0;
}

输出:

0 0 0 0 0 0 0 0 0

常见的条件编译指令:

1:
#if 常量表达式
//…
#endif
//常量表达式由预处理器求值

举例:

#include<stdio.h>
int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9};
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		arr[i] = 0;
#if 1//if 后面的常量表达式为真,下面的printf将会被执行,否则不执行
		printf("%d ", arr[i]);
#endif
	}
	return 0;
}

输出:

0 0 0 0 0 0 0 0 0

2:多个分支的条件编译

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

举例:

#include<stdio.h>
int main()
{
#if 1==1//当if后面的条件为真,则if后面的语句将会被执行
	printf("haha\n");
	//当if后面的语句为假时,则对elif后面的语句进行判断
#elif 7==1
	printf("heihei\n");
#else
	print("wiwi\n");
#endif
	return 0;
}

输出:

haha

3:判断是否被定义

#if defined(symbol)
#ifdef symbol
上述逻辑反操作如下:
#if !defined(symbol)
#ifndef symbol

举例:

#include<stdio.h>
int main()
{
#if defined(DEBUG)
//当defined()括号中的标识符被定义过,那么执行if后面的语句,反之不执行
	printf("hehe\n");
#endif
	return 0;
}

此时没有输出。

要想输出printf语句,只需要在程序开头定义DEBUG即可。

#define DEBUG 0

注:即使这里的DEBUG被定义为0,但是printf语句依然会输出hehe,因为这里只关心是否定义,而不关心定义的内容。

4:嵌套指令:
在这里插入图片描述

和if/elif/else的条件编译用法基本相同,这里就是增加了嵌套用法而已。

文件包含:

通过前面的学习,我们已经知道,#include指令可以使另一个文件被编译,就像它实际出现于#include指令的地方一样,这种替换方式很简单:预处理器先删除这条指令,并用包含文件的内容替换,这样一个源文件被包含10次,那就被编译10次。

头文件被包含的方式:

本地文件包含:

#include“filename”

add.h:

int ADD(int x, int y)
{
	return x + y;
}

test.h:

#include<stdio.h>
#include"add.h"//注意引add函数的文件方式不是<>,而是双引号
int main()
{
	int ret = ADD(2, 3);
	printf("ret=%d\n",ret);
	return 0;
}

输出:

ret=5

那么什么时候使用<>呢?什么时候使用“”呢?

如果是本地文件,也就是自己编写的头文件,就使用“”包含。

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

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

/usr/include

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

C:\program Files(x86)\Microsoft STudio 9.0\VC\include

注意按照自己的安装路径去找。

库文件包含:

#include<filename.h>

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

那么,我们可以得出一个结论,无论是我们自己编写的头文件还是库文件,都可以使用<>和双引号的形式,只要注意路径即可。

但如果库文件也使用双引号包含的形式,这样一来在查找文件的时候效率就会低很多,并且本地文件和库文件也不容易被区分了。

嵌套文件包含:

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

对于这种现象,我们可以通过条件编译来解决。

每个头文件的开头写:

#ifndef _TEST_H
#define _TEST_H
//头文件的内容
#endif //_TEST_H

或者#pragma once

这样就可以避免头文件的重复使用。

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

相关文章:

  • Flutter 系列---入门篇
  • 全球与中国汽车多楔带行业发展趋向分析及投资前景预测报告2022-2028年
  • Java IO流详解
  • 迷宫_Sarsa算法_边做边学深度强化学习:PyTorch程序设计实践(2)
  • iOS视频捕获入门篇
  • 数据可视化系列教程|六大组件基础知识
  • 《MySQL技术内幕:InnoDB存储引擎》学习笔记-第二章
  • 无线能量传输(WPT)中收发线圈的等效电路模型
  • 回归预测 | MATLAB实现BP神经网络多输入单输出回归预测
  • 新手入门:APP软件测试需要考虑哪些方面
  • .NET序列化 serializable,反序列化
  • 【源码+项目部署】Java课程设计项目_Java人力资源管理系统
  • 从云原生到数字原生,数字化转型背后的需求逻辑发生哪些变化?
  • 2022年最新宁夏水利水电施工安全员模拟试题题库及答案
  • 视频批注软件哪个好用 分秒帧在线批注怎么用
  • C++11: atomic 头文件
  • iOS帅气加载动画、通知视图、红包助手、引导页、导航栏、朋友圈、小游戏等效果源码...
  • java多线程
  • js ES6 求数组的交集,并集,还有差集
  • LeetCode刷题——29. Divide Two Integers(Part 1靠自己)
  • Mithril.js 入门介绍
  • nginx 负载服务器优化
  • React-Native - 收藏集 - 掘金
  • SpingCloudBus整合RabbitMQ
  • SpringCloud集成分布式事务LCN (一)
  • Spring思维导图,让Spring不再难懂(mvc篇)
  • Vue.js-Day01
  • 力扣(LeetCode)21
  • 力扣(LeetCode)965
  • 如何将自己的网站分享到QQ空间,微信,微博等等
  • 想使用 MongoDB ,你应该了解这8个方面!
  • scrapy中间件源码分析及常用中间件大全
  • ​如何在iOS手机上查看应用日志
  • $.proxy和$.extend
  • (4)事件处理——(2)在页面加载的时候执行任务(Performing tasks on page load)...
  • (Redis使用系列) SpirngBoot中关于Redis的值的各种方式的存储与取出 三
  • (翻译)Quartz官方教程——第一课:Quartz入门
  • (免费领源码)python#django#mysql校园校园宿舍管理系统84831-计算机毕业设计项目选题推荐
  • (四)模仿学习-完成后台管理页面查询
  • (转)PlayerPrefs在Windows下存到哪里去了?
  • (转)视频码率,帧率和分辨率的联系与区别
  • **CI中自动类加载的用法总结
  • .“空心村”成因分析及解决对策122344
  • .equals()到底是什么意思?
  • .NET CF命令行调试器MDbg入门(三) 进程控制
  • .Net 垃圾回收机制原理(二)
  • .NET 应用架构指导 V2 学习笔记(一) 软件架构的关键原则
  • .NET使用存储过程实现对数据库的增删改查
  • @AliasFor注解
  • @Transaction注解失效的几种场景(附有示例代码)
  • [ C++ ] STL priority_queue(优先级队列)使用及其底层模拟实现,容器适配器,deque(双端队列)原理了解
  • [ C++ ] STL---string类的使用指南
  • [ vulhub漏洞复现篇 ] Django SQL注入漏洞复现 CVE-2021-35042
  • [ACTF2020 新生赛]Include
  • [bzoj1901]: Zju2112 Dynamic Rankings