C语言-了解程序环境和预处理看这一篇(超详解)

1.程序环境

在ANSIC的任何一种实现中,都会存在两个不同的环境。第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令,第2种是执行环境,它用于实际执行代码。如下图所示:

1.1 翻译环境

翻译环境会分几个步骤:

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

由上我们便可以知道翻译环境分成编译器、链接器两个部分分别完成相应的功能,那么编译器这一部分又可以分为预编译(预处理)、编译、汇编这三个部分,如下图:

预编译:预编译过程主要处理的是那些源代码文件中以"#"开始的预编译指令,如#include、#define等,具体有哪些如下:

  • 将所有的"#define"删除,并且展开所有的宏定义。
  • 处理所有条件预编译指令,比如"#if"、"#ifdef"、"#elif"、"#else"、"#endif"
  • 处理"#include"预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这
  • 个过程是递归进行的,也就是说被包含的文件可能还包含其他文件
  • 删除所有的注释
  • 保留所有的#pragma编译器指令,因为编译器须要使用它们
  • 添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号

编译:编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析、生成相应的符号汇总及优化后生产相应的汇编代码文件

汇编:汇编就是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令,以及生成符号表

符号汇总,符号表:

编译中的符号汇总就是会把源文件当中类似于全局变量,函数名等汇总起来,局部变量不会总因为局部变量只有在程序执行时才会定义,生命周期短。

汇编当中的符号表就是把各原文件当中所汇总的符号整合到一起,把全局变量,以及函数等它们的真正地址都整合起来,一边在链接器链接时找到它们的真正位置。具体如下所示:

链接(链接器):把多个目标文件和连接库进行链接的,主要进行合并段表和符号表的合并与重定位 。

1.2 执行环境

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

2.预处理

2.1 预定义符号

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

所谓预定义就是在我们预处理之前就已经定义好了,可以直接使用,这些能用来干什么呢?一般可以用来我们在写代码的时候用作标记,当工程比较复杂的时候,我们可以在其中穿插这样的代码,类似于写日志,写入文件当中,以便编译时发现其中的错误。如下代码所示:

int main()
{FILE* pf = fopen("log.txt", "a+");if (pf == NULL){perror("fopen");return 1;}int i = 0;for (i = 0; i < 10; i++){fprintf(pf, "%s %d %s %s\n ", __FILE__, __LINE__, __DATE__, __TIME__);}fclose(pf);pf = NULL;return 0;
}

 

2.2 #define

#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__ )   
  • #define MAX 1000 这个就是我们最常用的把一个常量定义给MAX方便我们后续的修改
  • #define reg register 对于寄存器定义的变量直接写register会比较长,可以定义成这种形式方便我们定义变量
  • #define do_forever for(;;) 这是一种死循环,在代码中直接写入do_forever就会进行死循环
  • #define CASE break;case 有的编程语言switch语句中的case不需要break,但是c语言当中是要求break的,所以就有了这样的定义,在switch语句中CASE就可以不用写break,但需要注意第一个case需要小写,实际上#define就是替换。case:  CASE(break;case): 
  • 需要注意:#define后面尽量不要加入      

#define定义宏

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

其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。需要注意:参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。看如下代码:

//#define SQUARE( x )  x * x
//#define SQUARE( x ) (x) + (x)
#define SQUARE( x ) ((x) + (x))
int main()
{printf("%d \n", SQUARE(5));//25printf("%d \n", SQUARE(2 + 3));//11实际上printf("%d \n", 2+3*2+3)//改正#define SQUARE( x )  (x) * (x)//但又会遇到新的问题如下printf("%d \n", 10*SQUARE(2 + 3));//想要的结果是10*(5+5)=100//实际上10*5+5=55//因此#define定义宏括号要尽量加上return 0;
}

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

#define替换规则

#define定义的符号和宏,替换的时候会涉及如下几个步骤:                     

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

需要注意:

  •  宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归
  • 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索

2.3 #和##

上面我们知道,字符串常量的话,#define所定义的符号它是不会替换的,那怎么样让它去替换呢?下面我们讲述比较奇妙的知识点,#和##。

#define PRINT(V, FORMAT) printf("the value of "#V" is " FORMAT"\n", V )
int main()
{//如果我们想要打印the value of a is 0//the value of b is 1,the value of c is 2//定义函数的话,其中的abc是不是不能以一个通用的方式去替换int a = 0;PRINT(a, "%d");float b = 1;PRINT(b, "%f");return 0;
}#define VALUE(x, y) x##y
int main()
{int value111 = 100;printf("%d\n", VALUE(value, 111));//100return 0;
}

#的含义就是把宏定义的参数转换成所对应的字符串,之后才插入字符串之中。##就是把两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。需要注意:

  • #和##只能在宏定义下才能去使用。
  • 只有当字符串作为宏参数的时候才可以把字符串放在字符串中,如果直接在字符串中去替换,而我们的参数不是字符串,就不会替换
  • ##这样的连接必须产生一个合法的标识符。否则其结果就是未定义的

2.4 带副作用的宏参数 

带有副作用的宏参数和上述加不加括号所带来的影响是不同的。具体的就是宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么就有可能会出现危险。副作用就是表达式求值的时候出现的永久性效果。例如:

x+1//正常的加法运算没有副作用
x++//这会产生副作用,表达式使用的是x原本的值,但是执行之后x+1了不再是原来的值

产生的副作用看下例:

#define MAX(a, b)  ( (a) > (b) ? (a) : (b) )
int main()
{int x = 5;int y = 8;int z = MAX(x++, y++);printf("x=%d y=%d z=%d\n", x, y, z);//结果是不是我们想的6 9 8呢?//事实上结果是6 10 9 
}

事实上的结果与我们所想的大相径庭,这是为什么呢?这就是由于副作用的表达式在宏的定义中出现的不止一次,对最后的结果产生了较大的影响,最终的运算见下图:                                  MAX(x++, y++)  ( (x++) > (y++) ? (x++) : (y++) )

2.5 宏和函数的对比

一定程度下我们是不是会感觉宏和函数的功能差不多,但是呢它们还是有一定的区别,区别是:

  • 当一个运算较为简单的时候,用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹
  • 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。但是这个宏可以适用于整形、长整型、浮点型等可以用于>来比较的类型,如上例的ab整型我们可以传,字符型的我们也可以传。因此宏是类型无关的
  • 宏的参数可以出现类型,但是函数做不到。如下代码所示
#define MALLOC(num, type)\(type *)malloc(num * sizeof(type))
int main()
{MALLOC(10, int);//类型作为参数//预处理器替换之后:(int*)malloc(10 * sizeof(int));return 0;
}

当然,有时候优点也是缺点,是一把双刃剑和函数相比它的缺点如下:

  • 每次使用宏的时候,一份宏定义的代码都会在预处理阶段插入到程序中。运算简单时无可厚非,但当代码足够复杂时,这样的宏使用多次,我们的代码将会大幅度增加。
  • 由于它是类型无关,那也就不够严谨
  • 宏由于预处理阶段就会插入的程序当中去,因此在编译运行进行调试的时候也就看不到我们所定义的这些符号和宏,因此也就没办法进行调试。
  • 如上面加不加括号所引起的运算符优先级也是一个问题,很容易导致出错

这里有一个不成文的规定, 一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者,因此呢C语言程序员的一个习惯就是:把宏名全部大写,函数名不要全部大写。我们也尽量这样做,这样会显得高级!

2.6 #undef

这个指令就和#define正好相反,前者定义一个符号或宏,后者取消定义,如果在代码中我们想让这个符号重新被定义,就应该先把之前的移除。 

2.7 命令行定义

什么意思呢?就是说我们可以在命令行中进行定义,应用场景也比较有限,一般就是在一些内存空间比较有限,会根据不同的机器我们去定义一个不同大小的变量,gcc编译环境下就可以实现这一的一个功能。

2.8 条件编译

顾名思义就是满足条件就编译,不满足就不编译,比如一些调试性代码删除可惜,保留又碍事,所以我们可以选择性的编译。具体有以下几种条件编译指令:

//1
#if 0printf("张三");//真就执行,假就不执行
#endifreturn 0;
//2
#if  0printf("张三");//真就执行,假就不执行
#elif 1printf("李四");//真就执行,假就不执行,但如果第一条执行了这个即使是真也不执行
#elif 0printf("王二");//真就执行,假就不执行,但如果前两条执行了这个即使是真也不执行
#endif
//3
#define NAME  0
int main()
{
#ifdef NAME  // 等价于#if defined (NAME)printf("张三");//检测的是否被定义,即使定义的为假也执行
#endif
#ifndef NAME//==#if !defined (NAME)printf("张三");//检测的是否被定义不定义才执行
#endifreturn 0;
}
//4嵌套,类似于判断语句条件编译指令也是支持嵌套的

2.9 文件包含

我们在写C语言代码时常常会写一句#include<stdio.h>,什么意思呢?其实这条语句的意思就是包含头文件 stdio.h,在预处理阶段这条代码就会被替换成我们所包含文件的代码。在前面所介绍的通讯录代码中是不是还有另外一种包含头文件的方式#include"",那种方式我们用来包含我们自己定义的头文件。

所以头文件包含有两种方式:#include<>和#include""。它们的区别就是查找策略不同,如下:

  • #include<>:查找头文件直接去标准路径下去查找,如果找不到就提示编译错误
  • #include"":先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样再去标准位置查找头文件

那我们对于库函数下的头文件可不可以用#include""进行包含呢?当然也是可以的,但是它是不是会先在我们的源文件的目录下去查找,再去标准位置去查找,这样效率也就低了。因此我们要按照标准去编写我们的代码,这样我们的代码会显得高级! 

2.10 嵌套文件包含

通过对预处理的了解,你想没想到过这样的场景呢?

可能我们并没有在意这些,但当我们仔细去观察就会发现,在一些时候我们还是写出了这样的代码的, comm.h和comm.c是公共模块,add.h和add.c使用了公共模块,sub.h和sub.c使用了公共模块,test.h和test.c使用了add模块和sub模块。那我们可想而知在预处理之后,common的代码是不是就在test中出现了两次,一旦这些代码足够的长,那效率就会降下来了。怎么样预防这样问题的出现呢?有两种解决方式:

//第一种是在头文件中写入
#pragma once
//第二种
#ifndef __TEST_H__
#define __TEST_H__
//endif之前是头文件的内容
#endif   
//这三条语句的意思就是1.ifndef __TEST_H__不定义就执行
//因此#define __TEST_H__和头文件内容就会插入在程序当中
//当有第二个头文件要插入时遇到了#define __TEST_H__
//因为定义了#ifndef __TEST_H__就不会再执行

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

大数据处理从零开始————9.MapReduce编程实践之信息过滤之学生成绩统计demo

1.项目目标 1.1 需求概述 现在我们要统计某学校学生的成绩信息&#xff0c;筛选出成绩在60分及以上的学生。 1.2 业务分析 如果我们想实现该需求&#xff0c;可以通过编写一个MapReduce程序&#xff0c;来处理包含学生信息的文本文件&#xff0c;每行包含【学生的姓名&#x…

HCIP--以太网交换安全(二)端口安全

端口安全 一、端口安全概述 1.1、端口安全概述&#xff1a;端口安全是一种网络设备防护措施&#xff0c;通过将接口学习的MAC地址设为安全地址防止非法用户通信。 1.2、端口安全原理&#xff1a; 类型 定义 特点 安全动态MAC地址 使能端口而未是能Stichy MAC功能是转换的…

使用 Vertex AI Gemini 模型和 Elasticsearch Playground 快速创建 RAG 应用程序

作者&#xff1a;来自 Elastic Jeff Vestal 在这篇博客中&#xff0c;我们将使用 Elastic 的 Playground 和 Vertex AI API 将 Elasticsearch 连接到 Google 的 Gemini 1.5 聊天模型。将 Gemini 模型添加到 Playground 使 Google Cloud 开发人员能够快速建立 LLM、测试检索、调…

算法闭关修炼百题计划(四)

仅供个人复习 1.两数相加2.寻找峰值6.岛屿的最大面积3.最大数4.会议室5.最长连续序列6.寻找两个正序数组的中位数 1.两数相加 给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请…

滑动窗口_⽔果成篮找到字符串中所有字⺟异位词

⽔果成篮 904. 水果成篮 - 力扣&#xff08;LeetCode&#xff09; 相当于求数字种类不超过2的最长字字符串 我们先看一看例4.从第一个元素开始最长字符串3331&#xff0c;下一次从第二个位置数吗&#xff1f;没必要&#xff0c;因为只有当字符串中数字种类变为1时&#xff0c;…

pycharm里debug时如何看到数据的维度

使用表达式计算&#xff08;Evaluate Expression&#xff09; 调试时&#xff0c;使用 PyCharm 的 “Evaluate Expression” 功能可以动态查看或修改数据。具体步骤如下&#xff1a; 在调试模式中按 Alt F8&#xff08;Windows&#xff09;或 Option F8&#xff08;Mac&…

机器学习 | 特征选择如何减少过拟合?

在快速发展的机器学习领域&#xff0c;精确模型的开发对于预测性能至关重要。过度拟合的可能性&#xff0c;即模型除了数据中的潜在模式外&#xff0c;还拾取训练集特有的噪声和振荡&#xff0c;这是一个固有的问题。特征选择作为一种有效的抗过拟合武器&#xff0c;为提高模型…

JAVA科技赋能共享台球室无人系统小程序源码

科技赋能共享台球室无人系统 —— 智慧台球新体验 &#x1f3b1; 科技引领&#xff0c;台球室迎来无人新纪元 在这个日新月异的科技时代&#xff0c;共享经济的浪潮席卷而来&#xff0c;为我们的生活带来了诸多便利。而今天&#xff0c;我要为大家介绍的&#xff0c;正是科技…

【高等代数笔记】线性空间(十九-二十四上半部分)

课程视频剪辑得太抽象了&#xff0c;一节课不能完整学完&#xff0c;拆的零零散散得。 3. 线性空间 3.19 满秩矩阵 【推论4】设 rank ( A ) r \text{rank}(\boldsymbol{A})r rank(A)r&#xff0c;则 A \boldsymbol{A} A的不为0的 r r r阶子式所在的列&#xff08;行&#x…

2.3MyBatis——插件机制

2.3MyBatis——插件机制 1.基本用法2.原理探究2.1加载过程2.2执行过程2.2.1 插件的执行点2.2.2 SQL执行的几个阶段2.2.3 如何梳理出执行流程 合集总览&#xff1a;Mybatis框架梳理 插件机制是一款优秀框架不可或缺的组成部分&#xff0c;比如spring、dubbo&#xff0c;还有…

链表(2)_两两交换链表中的节点_面试题

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 链表(2)_两两交换链表中的节点_面试题 收录于专栏【经典算法练习】 本专栏旨在分享学习算法的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c;…

T11:优化器对比实验

T11周&#xff1a;优化器对比实验 **一、前期工作**1.设置GPU,导入库 **二、数据预处理**1.导入数据2.检查数据3.配置数据集4.数据可视化 **三、构建模型****四、训练模型****五、模型评估**六、总结 &#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&a…

【前端碎片记录】大文件分片上传

大文件分片上传&#xff0c;主要是为了提高上传效率&#xff0c;避免网络问题或者其他原因导致整个上传失败。 HTML部分没什么特殊代码&#xff0c;这里只写js代码。用原生js实现&#xff0c;框架中可参考实现 // 获取上传文件的 input框 const ipt document.querySelector(…

aws(学习笔记第五课) AWS的firewall SecurityGroup,代理转发技术

aws(学习笔记第五课) AWS的firewall– SecurityGroup&#xff0c;代理转发技术 学习内容&#xff1a; AWS的firewall– SecurityGroup代理转发技术 1. AWS的filewall– SecurityGroup 控制进入虚拟服务器的网络流量 通常的firewall(防火墙)配置 AWS上使用安全组进行网络流量…

息肉检测数据集 yolov5 yolov8适用于目标检测训练已经调整为yolo格式可直接训练yolo网络

息肉检测数据集 yolov5 yolov8格式 息肉检测数据集介绍 数据集概述 名称&#xff1a;息肉检测数据集&#xff08;基于某公开的分割数据集调整&#xff09;用途&#xff1a;适用于目标检测任务&#xff0c;特别是内窥镜图像中的息肉检测格式&#xff1a;YOLO格式&#xff08;边…

Transactional注解导致Spring Bean定时任务失效

背景 业务需要定时捞取数据库中新增的数据做数据处理及分析&#xff0c;更新状态&#xff0c;处理结束。而我们不能随意定义线程池&#xff0c;规定使用统一的标准规范来定义线程池。如在配置文件中配置线程池的属性&#xff1a;名称&#xff0c;线程核心数等&#xff0c;任务…

04-SpringBootWeb案例(中)

3. 员工管理 完成了部门管理的功能开发之后&#xff0c;我们进入到下一环节员工管理功能的开发。 基于以上原型&#xff0c;我们可以把员工管理功能分为&#xff1a; 分页查询&#xff08;今天完成&#xff09;带条件的分页查询&#xff08;今天完成&#xff09;删除员工&am…

Linux_kernel内核定时器14

一、内核定时器 1、内核定时器 使用方法&#xff1a; 2、系统时钟中断处理函数 1&#xff09;更新时间 2&#xff09;检查当前时间片是否耗尽 Linux操作系统是基于时间片轮询的&#xff0c;属于抢占式的内核 3&#xff09;jiffies 3、基本概念 1&#xff09;HZ HZ决定了1秒钟产…

OCP迎来新版本,让OceanBase的运维管理更高效

近期&#xff0c;OceanBase的OCP发布了新版本&#xff0c;全面支持 OceanBase 内核 4.3.2 及更低版本。新版本针对基础运维、性能监控、运维配置、外部集成等多个方面实现了 20余项的优化及强化措施&#xff0c;增强产品的易用性和稳定性&#xff0c;从而帮助用户更加高效地管理…

中国地级市生态韧性数据及城市生态韧性数据(2000-2022年)

一测算方式&#xff1a; 参考C刊《管理学刊》楚尔鸣&#xff08;2023&#xff09;老师的做法&#xff0c;城市生态韧性主要衡量一个城市在面临生态环境系统压力或突发冲击时&#xff0c;约束污染排放、维护生态环境状态和治理能力提升的综合水平。 参考郭海红和刘新民的研究&a…