【C语言】程序环境和预处理

👀℉f:个人主页

 🎥个人专栏:《C语言》《数据结构》《蓝桥杯试题》

🌝每一个不曾起舞的日子,都是对生命的辜负。


目录

前言:

一、程序的翻译环境和执行环境

二、详解编译+链接

(一)翻译环境

(二)编译本身也分为几个阶段:

(三)运行环境

三、预处理详解

(一)预定义符号

(二)#define

(1)#define 定义标识符

(2)#define 定义宏

(3)#define 替换规则

(4)#和##

(5)带副作用的宏参数

(6)宏和函数对比

(7)命名约定

(三)#undef

(四)命令行定义

(五)条件编译

(六)文件包含

(1)头文件被包含的方式:

(2)嵌套文件包含


前言:

本篇文章主要讲解程序的翻译执行环境、宏的概念、预处理指令以及条件编译的相关知识。

希望大家多多点赞支持,关注博主不迷路🍎

=========================================================================

GITEE相关代码:🌟fanfei_c的仓库🌟

=========================================================================

一、程序的翻译环境和执行环境

计算机能够执行二进制指令,但是我们写出的C语言代码是文本信息,计算机不能直接理解,这时候就需要有两种环境来辅助计算机理解文本信息。

在ANSI C的任何一种实现中,存在两个不同的环境。   

  • 翻译环境,在这个环境中源代码被转换为可执行的机器指令,即将C语言代码翻译为二进制指令(可执行程序)。
  • 执行环境,它用于实际执行代码,即执行二进制的代码。

二、详解编译+链接

(一)翻译环境

  • 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
  • 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
  • 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

(二)编译本身也分为几个阶段:

本图片可以很好辅助大家理解,博主这么用心大家点个赞吧😁

一些gcc编译器指令:

  1. 预处理选项 gcc -E test.c -o test.i 预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。
  2. 编译选项 gcc -S test.c 编译完成之后就停下来,结果保存在test.s中。
  3. 汇编 gcc -c test.c 汇编完成之后就停下来,结果保存在test.o中。

(三)运行环境

程序执行的过程:

  1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
  2. 程序的执行便开始。接着便调用main函数。
  3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(函数栈帧),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
  4. 终止程序。正常终止main函数,也有可能是意外终止。

三、预处理详解

(一)预定义符号

__FILE__      //进行编译的源文件
__LINE__     //文件当前的行号
__DATE__    //文件被编译的日期
__TIME__    //文件被编译的时间
__STDC__    //如果编译器遵循ANSI C,其值为1,否则未定义

这些预定义符号都是语言内置的。 比如:

printf("file:%s line:%d\n", __FILE__, __LINE__);

(二)#define

(1)#define 定义标识符

语法:
#define name stuff

比如:

#define MAX 1000
#define reg register          //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;)     //用更形象的符号来替换一种实现
#define CASE break;case        //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
                          date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ ,       \
__DATE__,__TIME__ )

 其实续行符'\'的实际作用是将换行符转义。

(2)#define 定义宏

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

下面是宏的申明方式:#define name( parament-list ) stuff

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

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

#define的原理就是直接替换,并不会计算,如:

#define SQUARE( x ) x * x

这个宏接收一个参数 x .。如果在上述声明之后,你把

SQUARE( 5 );

置于程序中,预处理器就会用下面这个表达式替换上面的表达式:

5 * 5

警告: 这个宏存在一个问题,观察下面的代码段: 

int a = 5;
printf("%d\n", SQUARE(a + 1));

乍一看,你可能觉得这段代码将打印36这个值。 事实上,它将打印11。

为什么? 替换文本时,参数x被替换成a + 1,所以这条语句实际上变成了:

printf ("%d\n",a + 1 * a + 1 );

这样就比较清晰了,由替换产生的表达式并没有按照预想的次序进行求值。 在宏定义上加上两个括号,这个问题便轻松的解决了: 

#define SQUARE(x) (x) * (x)

这样预处理之后就产生了预期的效果:

printf ("%d\n",(a + 1) * (a + 1) );

 这里还有一个宏定义:

#define DOUBLE(x) (x) + (x)

定义中我们使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误。 

int a = 5;
printf("%d\n", 10 * DOUBLE(a));

这将打印什么值呢? 看上去,好像打印100,但事实上打印的是55。我们发现替换之后:

printf("%d\n", 10 * (5) + (5));

乘法运算先于宏定义的加法。

这个问题的解决办法是在宏定义表达式两边加上一对括号就可以了。

#define DOUBLE( x)   ( ( x ) + ( x ) )

提示:所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符和邻近操作符之间不可预料的相互作用。 

(3)#define 替换规则

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

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换,比如#define M 100和宏#define SQUARE(x) ((x)*(x)),主函数中int r=SQUARE(M+2)会先替换宏参数中的M。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

注意:

  • 宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
  • 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。比如printf("M=%d",M); 字符串中的M不会被替换。

(4)#和##

#的讲解:

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

首先我们知道字符串是可以连续打印的,比如:

int main()
{
	char* p = "hello ""world\n";
	printf("hello"" world\n");
	printf("%s", p);
	return 0;
}//输出结果都是hello world

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

所以有:

#define PRINT(FORMAT, VALUE)\
 printf("the value is "FORMAT"\n", VALUE);

int main()
{
	PRINT("%d", 10);
	return 0;
}

我们来分析一下:该宏的参数为"%d"和10,传参到宏定义中,替换后为

printf("the value is ""%d""\n", 10);

此时%d两侧的""会分别成一对,让%d脱离出来,从而可以打印。

 那么#的作用就是可以把一个宏参数变成对应的字符串,比如:

int i = 10;
#define PRINT(FORMAT, VALUE)\
 printf("the value of " #VALUE "is "FORMAT "\n", VALUE);
...
PRINT("%d", i + 3);

代码中的 #VALUE 会预处理器处理为: "VALUE" 。 

最终的输出的结果应该是:the value of i+3 is 13 


##的讲解:

  • ##可以把位于它两边的符号合成一个符号。
  • 它允许宏定义从分离的文本片段创建标识符。

说白了就是a##b的结果就是ab。 

比如:

#define CAT(x,y) x##y

int main()
{
	int number = 2023;
	printf("%d\n", CAT(num, ber));//和下面的效果是相同的
	printf("%d\n", number);
	return 0;
}

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


(5)带副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。

副作用就是表达式求值的时候出现的永久性效果。 例如:

x+1;//不带副作用

x++;//带有副作用(会改变x的值)

MAX宏可以证明具有副作用的参数所引起的问题:

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?

 这里我们得知道预处理器处理之后的结果是什么:

z = ( (x++) > (y++) ? (x++) : (y++));

所以输出的结果是:x=6 y=10 z=9 

(6)宏和函数对比

宏通常被应用于执行简单的运算。

比如在两个数中找出较大的一个:

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

那为什么不用函数来完成这个任务?原因有二:

  • 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多(函数调用有很多准备工作,函数栈帧)。 所以宏比函数在程序的规模和速度方面更胜一筹。
  • 更为重要的是函数的参数必须声明为特定的类型(函数有类型检查)。 所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以 用于>来比较的类型。 宏是类型无关的

宏的缺点:

  • 每次使用宏的时候,一份宏定义的代码将插入到程序中(预编译阶段的替换工作)。除非宏比较短,否则可能大幅度增加程序的长度(增大编译器编译压力)。
  • 宏是没法调试的。
  • 宏由于类型无关,也就不够严谨。
  • 宏可能会带来运算符优先级的问题,导致程容易出现错。 

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

#define MALLOC(num, type)\
 (type *)malloc(num * sizeof(type))
...
//使用
MALLOC(10, int);//类型作为参数
//预处理器替换之后:
(int*)malloc(10 * sizeof(int));

宏和函数的对比: 

(7)命名约定

 一般来讲函数的宏的使用语法很相似,所以语言本身没法帮我们区分二者。

那我们平时的一个习惯是: 把宏名全部大写,函数名不要全部大写

(三)#undef

这条指令用于移除一个宏定义。

#undef NAME

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

(四)命令行定义

许多C的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。

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

需要利用gcc编译器演示:

int main()
{
	int arr[SZ] = { 0 };
	int i = 0;
	for (i = 0; i < SZ; i++)
	{
		arr[i] = i;
	}
	for (i = 0; i < SZ; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

这里的SZ需要利用编译指令来声明:

linux 环境演示 gcc test.c -D SZ=10 -o test

此时就将SZ赋值为10了,这样每次编译我们都可以指定SZ的大小。 

(五)条件编译

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

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

#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;
}

在项目中,我们可以将调试性的代码两侧放置条件编译, 在代码头部放置#define __DEBUG__,这样当你想要进行调试时,就保留,调试完成就注释掉头部语句。

常见的条件编译指令: 

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

(六)文件包含

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

这种替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。 这样一个源文件被包含10次,那就实际被编译10次。

(1)头文件被包含的方式:

  • 本地文件包含:

#include "filename"

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

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

/usr/include

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

C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include 

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

  • 库文件包含:

#include <filename.h> 

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

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

答案是肯定的。

但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

(2)嵌套文件包含

看以下场景:

 如何解决这个问题呢?

答案是:条件编译。

每个头文件的开头这样写:

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

 或者:

#pragma once

就可以避免头文件的重复引入。 


到这里,C语言的内容就全部结束了,我的GITEE仓库中实现了三个版本的通讯录:

🌍我的仓库:fanfei_c的仓库

其中涉及到结构体、动态内存以及文件操作的相关知识,可以帮助大家巩固知识,希望大家多多点赞支持,关注博主不迷路🔥🔥🔥

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/43014.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ORB-SLAM2学习笔记4之KITTI开源数据集运行ORB-SLAM2生成轨迹并用evo工具评估轨迹

文章目录 0 引言1 KITTI数据集1.1 下载数据1.2 真值轨迹格式转换 2 单目ORB-SLAM22.1 运行ORB-SLAM22.2 evo评估轨迹(tum格式)2.2.1 载入和对比轨迹2.2.2 计算绝对轨迹误差 3 双目ORB-SLAM23.1 运行ORB-SLAM23.2 evo评估轨迹(kitti格式)3.2.1 载入和对比轨迹3.2.2 计算绝对轨迹…

Microsoft Edge 浏览器的Bing Chat

微软公司持续发力&#xff0c;推出的产品 Bing Chat 与 ChatGPT 之间的竞争愈发激烈。如今&#xff0c;微软不仅不断更新 Edge 浏览器&#xff0c;还将 Bing Chat 内置在边栏中&#xff0c;方便用户快速访问。这一举措不禁让人想起&#xff0c;Edge 浏览器如今已经是一款名副其…

探索AI图像安全,助力可信AI发展

探索AI图像安全&#xff0c;助力可信AI发展 0. 前言1. 人工智能发展与安全挑战1.1 人工智能及其发展1.2 人工智能安全挑战 2. WAIC 2023 多模态基础大模型的可信 AI2.1 WAIC 2023 专题论坛2.2 走进合合信息 3. AI 图像安全3.1 图像篡改检测3.2 生成式图像鉴别3.3 OCR 对抗攻击技…

动态规划入门第1课

1、从计数到选择 ---- 递推与DP&#xff08;动态规划&#xff09; 2、从递归到记忆 ---- 子问题与去重复运算 3、动态规划的要点 第1题 网格路1(grid1) 小x住在左下角(0,0)处&#xff0c;小y在右上角(n,n)处。小x需要通过一段网格路才能到小y家。每次&#xff0c;小x可以选…

【学会动态规划】最小路径和(9)

目录 动态规划怎么学&#xff1f; 1. 题目解析 2. 算法原理 1. 状态表示 2. 状态转移方程 3. 初始化 4. 填表顺序 5. 返回值 3. 代码编写 写在最后&#xff1a; 动态规划怎么学&#xff1f; 学习一个算法没有捷径&#xff0c;更何况是学习动态规划&#xff0c; 跟我…

视频增强技术-去噪

本文介绍了关于视频增强技术的相关方法包括传统方法和基于深度学习的方法&#xff0c;并给出了他们的对比实验结果&#xff0c;最后对它们简单的做了总结&#xff0c;文中有一些图片和总结来自于网上其他博主的文章&#xff0c;已在文中标记并给出了相关的原文链接&#xff0c;…

JAVA SE -- 第十天

&#xff08;全部来自“韩顺平教育”&#xff09; 一、枚举&#xff08;enumeration&#xff0c;简写enum&#xff09; 枚举是一组常量的集合 1、实现方式 a.自定义类实现枚举 b.使用enum关键字实现枚举 二、自定义类实现枚举 1、注意事项 ①不需要提供setXxx方法&#xff…

开源QianWei搭建音乐网站,并实现公网连接

开源QianWei搭建音乐网站&#xff0c;并实现公网连接 1、前言2、本地网页搭建2.1环境使用2.2 支持组建选择2.3 网页安装 3、本地网页发布3.1 Cpolar云端设置3.2 Cpolar本地设置 4、公网访问测试5、结语 1、前言 音乐是我们生活和工作中不可或缺的调剂&#xff0c;它能让我们心…

155、基于STM32单片机老人防跌倒摔倒GSM短信报警系统ADXL345加速度设计(程序+原理图+PCB源文件+参考论文+硬件设计资料+元器件清单等)

毕设帮助、开题指导、技术解答(有偿)见文未 目录 一、硬件方案 二、设计功能 三、实物图 四、原理图 五、PCB图 六、程序源码 资料包括&#xff1a; 需要完整的资料可以点击下面的名片加下我&#xff0c;找我要资源压缩包的百度网盘下载地址及提取码。 单片机主芯片选…

【PostgreSQL内核学习(六)—— 工具使用学习】

工具使用学习 工具使用学习安装中出现的问题 声明&#xff1a;本文的工具学习内容来自于《小宇带你学pg内核分析》 工具的代码仓库链接为&#xff1a; https://github.com/shenyuflying/pgNodeGraph 此外&#xff0c;我还参考了以下文章&#xff1a; https://rng-songbaobao.bl…

Mac配置Latex环境教程2023

第一步&#xff1a;安装MacTex 官网&#xff1a;https://www.tug.org/mactex/ 第二步&#xff1a;安装编译器&#xff1a;Texpad xclient官网下载Texpad&#xff1a;https://xclient.info/s/texpad.html 第三步&#xff1a;开始使用 LeTex \documentclass{article}\begin{do…

rabbitmq模块启动报java.net.SocketException: socket closed的解决方法

问题 最近在接手一个项目时&#xff0c;使用的是spring-cloud微服务构架&#xff0c;mq消息消费模块是单独一个模块&#xff0c;但启动这个模块一直报如下错误&#xff1a; java.net.SocketException: socket closed 这个错误是这个模块注册不到nacos报的错&#xff0c;刚开…

在Debian 12 上安装 PHP 5.6, 7.4

环境&#xff1a;Debian 12 Debian 12 默认的PHP版本为 8.2 如果直接安装php7.4就出现下面的报错&#xff1a; sudo apt-get install libapache2-mod-php7.4 php7.4 php7.4-gd php7.4-opcache php7.4-mbstring php7.4-xml php7.4-json php7.4-zip php7.4-curl php7.4-imap p…

Spring使用注解存储Bean对象

文章目录 一. 配置扫描路径二. 使用注解储存Bean对象1. 使用五大类注解储存Bean2. 为什么要有五大类注解&#xff1f;3.4有关获取Bean参数的命名规则 三. 使用方法注解储存Bean对象1. 方法注解储存对象的用法2. Bean的重命名 在前一篇博客中&#xff08; Spring项目创建与Bean…

RS485/RS232自由转ETHERNET/IP网关rs485和232接口一样吗

你是否曾经遇到过这样的问题&#xff1a;如何将ETHERNET/IP网络和RS485/RS232总线连接起来呢&#xff1f; 远创智控的YC-EIP-RS485/232通讯网关&#xff0c;自主研发的ETHERNET/IP从站功能&#xff0c;完美解决了这个难题。这款网关不仅可以将ETHERNET/IP网络和RS485/RS232总线…

访客报警定位管理系统:提升安全管理水平的创新解决方案

在当前日益复杂的安全环境下&#xff0c;保障人员安全、提高安全响应能力和管理效率成为了各行各业的首要任务。 作为一种先进的安全管理解决方案&#xff0c;访客报警定位管理系统凭借其独特的优势和广泛的应用场景&#xff0c;正逐渐成为各行业安全管理的重要工具。 那么&a…

「深度学习之优化算法」(十七)灰狼算法

1. 灰狼算法简介 (以下描述,均不是学术用语,仅供大家快乐的阅读)   灰狼算法(Grey Wolf Algorithm)是受灰狼群体捕猎行为启发而提出的算法。算法提出于2013年,仍是一个较新的算法。目前为止(2020)与之相关的论文也比较多,但多为算法的应用,应该仍有研究和改进的余…

数学建模-时间序列分析 实例

实例1销量数据预测和实例2人口数据预测实例3上证指数预测和实例4gdp增长率预测 数据-定义时间 不加置信区间清晰点 例二 实例3

性能测试-Jmeter之Linux下压力测试

我们在做测试的时候&#xff0c;有时候要运行很久&#xff0c;公司用的测试服务器一般都是linux&#xff0c;就可以运行在linux下面&#xff0c;linux下面不能像windows一样有图形化界面&#xff0c;那怎么运行脚本呢&#xff0c;就先在windows上把脚本做好&#xff0c;然后在l…

云计算和云架构是什么 有什么用途?

云计算是一种基于互联网的计算方式&#xff0c;它通过网络将计算资源(如计算能力、存储、网络带宽等)以服务的形式提供给用户&#xff0c;并允许用户根据需求进行灵活的资源调配和管理。云计算通常分为三个层次&#xff0c;即基础设施即服务(IaaS)、平台即服务(PaaS)和软件即服…