程序环境和预处理(详解版)

我们已经学到这里,这就是关于C语言的最后一个集中的知识点了,虽然它比较抽象,但是了解这部分知识,可以让我们对C代码有更深层次的理解,知道代码在每一个阶段发生什么样的变化。让我们开始学习吧!


目录

1.程序的翻译环境和执行环境

2.编译+链接

2.1编译环境

2.2编译本身的几个阶段

2.3运行环境

3.预处理(预编译)

3.1预定义符号(这些符号都是语言内置的)

3.2#define和#undef

3.3命令行的定义

3.4条件编译

3.5文件包含

3.5.1头文件被包含的方式

3.5.2嵌套文件包含

4.其他预处理指令


1.程序的翻译环境和执行环境

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

1.第一种是翻译环境,在这个环境中,源代码被转换为可执行的机器指令。

2.第二种是执行环境,它用于实际的代码执行

我们举个例子来理解它

例子:我们在2022VS中写了一段代码,我们是放在了.c文件中,他在执行时生成解决方案会产生一个.exe文件,这个exe文件就是这段代码的翻译环境

我们的.c文件要变为.exe文件要经过编译和链接,其中这个编译就是指我们的翻译环境

我们接下来详细学习编译和链接

2.编译+链接

2.1编译环境

1.组成一个程序的每个源文件通过编译过程分别转换成目标代码

2.每个目标文件由编译器捆绑在一起,形成一个单一而完整的可执行程序

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

每个源文件(.c)都对应一个目标文件(.exe)

我们用一个图谱来理解它

知道这个以后,我们来了解编译本身包括的几个阶段

2.2编译本身的几个阶段

(我们用gcc编辑器语法来说明,gcc比VS效果更明显)

我们首先画一个图供大家了解

我们已经 知道编译本身几个阶段就是这四个,我已经在图中标好序号,方便我们接下来的说明:

1.预处理(预编译):

命令:gcc -E test.c -o test.i

(其中:-E代表在预处理结束后代码停止执行

            -o代表指定生成的文件名是test.i)

执行的操作包括:#include头文件的包含

                             #define定义符号的替换和删除

                             注释的删除(文本删除)

2.编译:

命令:gcc -S test.i        生成了test.s文件

在这一步,它把C语言翻译成了汇编代码

执行的操作包括:语法分析

                             词法分析

                             语义分析

                             符号汇总(只有全局符号汇总,汇总的是全局变量)

3.汇编:

命令:gcc -c test.s   生成了test.o文件

                         在gcc中,这个test.o文件就是目标文件,存放的是二进制数据

执行的操作包括:把汇编代码翻译成了二进制指令(存放与test.o文件中)

                             行成符号表,生成对应表格(以编译的符号汇总为基础生成的)

例:假设我们有三个全局变量被符号汇总

4.链接:

在一个大型工程中,我们有多个test.c文件,这就代表我们要生成多个test.o的目标文件

执行的操作:符号表的合并和定位

在生成的符号表中查找跨文件的符号和数据

例如:

我们在这里创建两个.c文件,一个存放总体代码,一个存放函数功能的实现

2.3运行环境

也就是程序执行的过程:

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

2.程序开始执行,接着便调用main函数

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

4.终止程序,正常终止main函数,也可能意外终止

3.预处理(预编译)

3.1预定义符号(这些符号都是语言内置的)

我们常见的预定义符号:

__FILE__            进行编译的源文件

__LINE__            当前文件的行号

__DATE__          文件被编译的日期

__TIME__           文件被编译的时间

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

__FUNCTION__    当前函数的名称

如果感兴趣,可以看一下它们的详细描述

我们看个代码例子:

//预定义符号int main()
{printf("文件路径为:> %s\n", __FILE__);printf("文件的对应行号是:> %d\n", __LINE__);printf("文件被编译的日期是:> %s\n", __DATE__);printf("文件被编译的时间是:> %s\n", __TIME__);//printf("%s\n", __STDC__);VS2022中已经不能使用这个预定义符号了printf("当前执行函数的名称是:> %s\n", __FUNCTION__);return 0;
}

3.2#define和#undef

由于#define还是很有说法的,所以我们把这一对放在  详解#define  这一篇博客单独讲解,这样更好理解,在这里只要知道它们都是预定义符号,以及#define是只进行对应位置的替换的,是不计算的就好

3.3命令行的定义

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

当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处

例如我们在gcc环境下编译以下代码

int main()
{int arr[sz];//我们在这里不指定sz的大小,我们在命令行输入int i = 0;for (i = 0; i < sz; i++){arr[i] = i;printf("%d ", arr[i]);}printf("\n");return 0;
}

在gcc编译器命令行,我们输入

编译:gcc  test.c  -D  sz=10  -o  test.exe

其中 -D是指定sz的大小,在这里我们设置sz=10

        -o是指定生成的文件名是test.exe

运行:.\test.exe

然后我们发现显示了 0 1 2 3 4 5 6 7 8 9 这几个数字

sz的值的代码的替换也是在预处理工程中完成了,我们可以输入以下命令来查看细节

gcc  test.c  -D  sz=10  -E  -o  test.i

这里的-E是在预处理结束后代码停止

这时打开test.i文件中我们查看代码,我们会看到,代码sz的位置被替换为10

(这种方式就是命令行的方式,我们以后要学的linux操作系统这也这种编译方式)

3.4条件编译

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

我们先了解一些常见的条件编译指令

1.#if   常量表达式

   ……

   #endif

2.多个分支的条件编译

#if

……

#elif

……

#else

……

#endif

3.判断是否被定义

#if defined(symbol),如果定义了symbol就执行下面的代码,否则就跳过

这条等价于#ifdef symbol

#if !defined (symbol),如果没有定义symbol,就执行下面的代码,否则就跳过

这条等价于#ifndef symbol

4.嵌套指令

#if  defined (os_UNIX)

#ifdef  OPTIONI

   unix  _version_  option1();

#endif

#ifdef  OPTION2

   unix  _version_  option2();

#endif

#elif defined(os_MSDOS)

#ifdef  OPTION2

   msdos _version_ option2();

#endif

#endif

接下来我们再来看个代码例子

#define PRINT 1
int main()
{
#ifdef PRINT//如果定义了PRINT,就打印hehe,否则不执行这段代码printf("hehe\n");
#endifreturn 0;
}

3.5文件包含

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

这种替换方式很简单:

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

我们介绍我们C语言中用到的文件包含:头文件包含和嵌套文件包含

3.5.1头文件被包含的方式

1.本地文件包含(就是指自己写出来的文件)

格式:#include“filename”

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

2.库文件包含(我们平时用到的库函数,库里面有的文件)

格式:#include<filename.h>

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

这时我们可以得到一个结论,库函数也可以使用“ ”的形式包含,但我们不采用,因为它效率低

3.5.2嵌套文件包含

我们用图解来说明:

解决办法:条件编译

在每个头文件开头添加条件

1.方法一:#ifndef  _TEST_H_

                 #define _TEST_H_

                ……头文件的内容

                #endif

2.方法二:

在开头写 #pragma once

这两种方法都可以避免头文件的重复引入,被多次包含

4.其他预处理指令

这里我们只介绍几个,如果对这部分内容感兴趣的话,可以自己去了解

#error   编译程序时,只有遇到#error就会生成一个编译错误的提示信息,并停止编译

#pragma   可以设定编译程序完成的一些特定的动作,允许向编译程序传送各种指令

#line     改变当前行数和文件名称

#pragma pack() 修改默认对齐数(这个我们用过)


好,今天的学习就到这里,我们下期再见!!!

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

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

相关文章

Flutter学习(四)如何取消listview的越界效果

背景 在flutter的开发过程中&#xff0c;ListView是很常见的一个组件&#xff0c;但是&#xff0c;由于ListView的某些自带的体验&#xff0c;导致不太好的用户体验。例如ListView中&#xff0c;滑动到顶部或者底部的时候&#xff0c;再次滑动&#xff0c;会有越界的效果&…

C语言——从键盘输人一个表示年份的整数,判断该年份是否为闰年,并显示判断结果。

#define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h> int main() {int year 0;printf("请输入年份&#xff1a;");scanf("%d",&year);if((year%4 0) && (year%100!0) || (year%400 0)){printf("%d是闰年\n",year);}else{p…

Python+jieba+wordcloud实现文本分词、词频统计、条形图绘制及不同主题的词云图绘制

目录 序言&#xff1a;第三方库及所需材料函数模块介绍分词词频统计条形图绘制词云绘制主函数 效果预览全部代码 序言&#xff1a;第三方库及所需材料 编程语言&#xff1a;Python3.9。 编程环境&#xff1a;Anaconda3&#xff0c;Spyder5。 使用到的主要第三方库&#xff1a;…

GeoTrust SSL数字安全证书介绍

一、GeoTrust OV证书的介绍 GeoTrust OV证书是由GeoTrust公司提供的SSL证书&#xff0c;它是一种支持OpenSSL的数字证书&#xff0c;具有更高的安全性和可信度。GeoTrust是全球领先的网络安全解决方案提供商&#xff0c;为各类用户提供SSL证书和信任管理服务。GeoTrust OV证书…

Ps:参考线

参考线 Guides用于帮助精确地定位图像或元素&#xff0c;显示为浮动在图像上的非打印线&#xff0c;可以移动或移除&#xff0c;还可以临时锁定。 Ps 中的参考线可分为三大类&#xff1a;画布参考线、画板参考线和智能参考线。 可在“首选项/参考线、网格和切片”中设置参考线的…

【nlp】3.2 Transformer论文复现:1. 输入部分(文本嵌入层和位置编码器)

Transformer论文复现:输入部分(文本嵌入层和位置编码器) 1 输入复现1.1 文本嵌入层1.1.1 文本嵌入层的作用1.1.2 文本嵌入层的代码实现1.1.3 文本嵌入层中的注意事项1.2 位置编码器1.2.1 位置编码器的作用1.2.2 位置编码器的代码实现1.2.3 位置编码器中的注意事项1 输入复现…

【管理运筹学】背诵手册(五)| 动态规划

五、动态规划 基本概念 阶段&#xff08;Stage&#xff09;&#xff1a;将所给问题的过程&#xff0c;按时间或空间特征分解成若干相互联系的阶段&#xff0c;以便按次序去求解每阶段的解&#xff0c;常用字母 k k k 表示。 状态&#xff08;State&#xff09;&#xff1a;…

深度解析 Docker Registry:构建安全高效的私有镜像仓库

文章目录 什么是Docker Registry&#xff1f;Docker Hub vs. 私有RegistryDocker Hub&#xff1a;私有Registry&#xff1a; 如何构建私有Docker Registry&#xff1f;步骤一&#xff1a;安装Docker Registry步骤二&#xff1a;配置TLS&#xff08;可选&#xff09;步骤三&…

《微信小程序开发从入门到实战》学习二十七

3.4 开发参与投票页面 3.4.2 借用伪造数据开发功能 为了便于开发&#xff0c;新建一个编译模式&#xff1a; 之前没看文章&#xff0c;每次都习惯性填完投票创建的信息提交再跳转看效果。好累。 添加变异模式开发真方便。 另外&#xff0c;点击提交后没跳转到投票页面&#…

<JavaEE> 什么是线程(Thread)?进程和线程有什么区别?

目录 一、线程&#xff08;Thread&#xff09;的概念 二、线程存在的意义 2.1 并发编程 2.2 比进程更“轻量” 三、使用线程时应该注意 四、进程和线程的区别 五、Java中的线程和操作系统中的线程是不同的概念 六、多线程编程 一、线程&#xff08;Thread&#xff09;的…

Linux进程通信——信号(一)

原理 对于 Linux来说&#xff0c;实际信号是软中断&#xff0c;许多重要的程序都需要处理信号。 信号&#xff0c;为 Linux 提供了一种处理异步事件的方法。比如&#xff0c;终端用户输入了ctrlc来中断程序&#xff0c;会通过信号机制停止一个程序。 概述 信号的名字和编号 …

nodejs 如何将 Buffer 数据转为 String

问题说明 使用webSocket的时候出现了一个问题&#xff0c;前端小程序和nodejs后端建立websocket连接后&#xff0c;使用send方法发送到后端为buffer格式&#xff0c;以下为我前后端代码 1、前端小程序代码 //创建webSocket连接 const socket uni.connectSocket({url: wss…