c语言的程序环境和预处理(一眼丁真)

2023-10-24 06:28

本文主要是介绍c语言的程序环境和预处理(一眼丁真),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言:

正所谓,万物c为首。在我们较为深入的学完c语言之后,可以说是打开了编程的第一扇大门。代码我们会敲了,可是这些代码到底是咋运行起来的呢?这些源文件,头文件里的代码又是怎么“整合”在一起的呢?从代码的生成到运行,中间经历了什么步骤呢?

带着以上问题,我与大家一起学习c语言之无敌内功修炼---程序环境和预处理。

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

1.编译环境是指在开发阶段,程序员使用的工具和设置,用于将源代码转换成可执行文件或库文件。编译环境通常包括编译器、链接器、调试器等工具。不同的编程语言可能有不同的编译环境,例如C语言常用的编译环境包括gcc、clang等。

2.执行环境是指在程序运行时,程序所依赖的软件和硬件环境。执行环境通常包括操作系统、运行时库、硬件平台等。不同的操作系统和硬件平台可能有不同的执行环境要求,例如在Windows操作系统上运行的程序需要使用Windows所提供的执行环境。

3.编译环境和执行环境之间密切相关,编译环境生成的可执行文件或库文件需要在相应的执行环境中才能正确运行

大概意思就是,一个程序的正常运行需要翻译和执行,翻译就是将你写的代码转换成一个可执行文件,这个可执行文件的正常运行又需要程序需要的环境。

2.编译和链接

2.1编译环境

程序编译的过程:

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

2.2编译的过程

 给出代码:

sum.c文件:

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

test.c文件:

#include<stdio.h>
#define a 10
#define b 20
extern int add(int x, int y);
int main() {printf("%d", add(a, b));//求两数之和return 0;
}

我们知道,源文件通过编译生成目标文件,那么这个“编译”的过程又是什么呢?

 编译有三个阶段:

1.预编译(预处理):

在预编译阶段,预处理器会处理源代码中的预处理指令,例如宏定义、条件编译等。它会根据这些指令对源代码进行修改和替换,并生成一个经过预处理的文件

根据代码,预处理阶段生成test.i文件和sum.i文件。

怎么看到这个预处理生成的文件呢?

用vs(2022)举例,鼠标右击项目,再单击属性:

将预处理文件这一项选择“是”,然后在debug文件夹里面就可以看到预处理生成的文件: 

test.i:

我们可以看到test.i文件里多了很多代码,在最后面才是我们看到的test.c文件里的源码,我们发现,跟test.c文件不一样的是,test.i文件里的注释不见了,被替换为了空格。头文件#include<>包含的信息也被替换了,而且define定义的符号a,b也直接被替换了。

其实,#include 和#define 都是预处理指令,而所有的预处理指令在预处理阶段也就是预编译阶段就被处理了(替换成了本来的面目),这些操作属于文本操作。

2.编译阶段:

编译器会将预处理后的文件进行词法分析、语法分析、语义分析和符号汇总,生成相应的中间表示形式(通常是汇编代码)。

1.词法分析:

词法分析(Lexical Analysis):词法分析将源代码划分为一个个词法单元(Tokens),如关键字、标识符、操作符等。词法分析器(Lexer)通过根据预先定义的词法规则来识别和生成这些词法单元。词法分析器会忽略空格、注释等不具有实质意义的字符,并将词法单元传递给语法分析器进行后续处理.

2.语法分析:

语法分析(Syntax Analysis):语法分析将词法单元组织成语法树(Syntax Tree)或抽象语法树(Abstract Syntax Tree,AST)。语法分析器(Parser)根据语法规则和上下文无关文法,验证源代码的语法结构是否正确。它确定语言的语法规则是否遵循,并产生一种结构化的表示形式以便后续的语义分析和代码生成

3.语义分析:

语义分析(Semantic Analysis):语义分析器对语法树进行静态检查,以验证源代码的语义正确性。它会检查变量的声明和使用是否匹配、类型的一致性、函数调用的正确性等。语义分析通过查找和解析符号引用,进行类型检查,并执行其他语义规则来捕获潜在的错误或不一致之处

4.符号汇总:

符号汇总(Symbol Table):符号汇总是在编译过程中维护的一个数据结构,用于记录程序中出现的符号(如变量、函数名等)的信息。符号汇总表存储了符号的名称、类型、作用域等信息,以及与之相关联的属性和值。符号汇总用于在语义分析和代码生成阶段解析符号引用,确保符号在正确的作用域内被引用和使用

总结:

在编译阶段,将预处理生成的test.i文件进行处理,生成汇编代码并存放在test.s文件中,这个时候的代码还是不能直接让机器运行,还需要下一步操作。

3.汇编:

汇编阶段是整个编译的最后一步,将比编译阶段生成的汇编代码翻译成二进制指令,生成目标文件(test.o),同时生成符号表,作用于链接阶段.

具体过程如下:

1.汇编指令生成:在汇编阶段,编译器将中间代码(如汇编代码或抽象语法树)转换为目标机器代码的汇编指令。每个汇编指令对应于特定的机器指令,用于执行特定的操作。这些指令描述了程序在底层硬件上的操作

2.符号解析和地址分配:编译器会解析各个标识符的引用,并为其分配具体的内存地址或位置。这包括变量、函数和常量等。符号解析和地址分配是为了生成正确的目标代码,使得代码中的符号引用能够正确地映射到内存中的对应位置

3.生成目标文件:在汇编阶段,编译器将生成转换后的目标机器代码,并将其保存为目标文件目标文件包含了汇编指令、符号表以及其他与链接和加载相关的信息。目标文件通常采用特定的格式,如ELF(Executable and Linkable Format)。

4.代码优化:在汇编阶段,有些编译器也会进行一些简单的代码优化。这些优化旨在提高目标代码的执行效率和性能。例如,常量合并、死代码消除、循环展开等技术可以在汇编阶段应用,以改善目标代码的质量。

2.3链接

在链接阶段,链接目标文件和链接库生成可执行程序二进制的程序。

1.合并段表

2.符号表的合并和重定位 

2.4 运行环境

程序执行的过程:

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

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

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

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

3.预处理详解

在上面的讲解中,我们已经大概知道了程序中的源代码是如何=生成可执行文件并运行的。

接下来我着重解释编译过程中的预处理阶段。

3.1预定义符号

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

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

举例:

其中__FILE__是当前编译源文件的路径 ,__LINE__表示的则是文件当前的行号。

3.2 #define

3.2.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__ )   

define定义标识符的时候要注意简洁,过于复杂则容易导致错误。

3.2.2 #define 定义宏

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

#define name( parament-list ) stuff

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

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

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

提示:

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

3.2.3 #define替换规则

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

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

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

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

注意:

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

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

3.2.4 #和##

#的作用

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

看以下代码:

char* p = "hello ""bit\n";
printf("hello"" bit\n");
printf("%s", p);

现字符串是有自动连接的特点的,所以这段代码输出“hello bit”.

于是我们可以这么写代码:

#define PRINT(FORMAT, VALUE) printf("the value of " #VALUE "is "FORMAT "\n", VALUE);
PRINT("%d", 3+5)

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

最终的输出的结果应该是:

the value of 3+5 is 8
## 的作用

##可以把位于它两边的符号合成一个符号。

它允许宏定义从分离的文本片段创建标识符

#define ADD_TO_SUM(num, value) sum##num += value;ADD_TO_SUM(5, 10);//作用是:给sum5增加10.

这里sum##num就变成了sum5.

需要注意的是,连接之后的符号必须是合法的标识符

4.常见预处理指令

指令用途
#include包含一个源代码文件
#define定义宏
#if取消已定义的宏
#undef如果给定条件为真,则编译下面代码
#ifdef如果宏已经定义,则编译下面代码
#ifndef如果宏没有定义,则编译下面代码
#elif如果前面的#if给定条件不为真,当前条件为真,则编译下面代码
#endif结束一个#if……#else条件编译块
#error停止编译并显示错误信息

这篇关于c语言的程序环境和预处理(一眼丁真)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/273293

相关文章

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

阿里开源语音识别SenseVoiceWindows环境部署

SenseVoice介绍 SenseVoice 专注于高精度多语言语音识别、情感辨识和音频事件检测多语言识别: 采用超过 40 万小时数据训练,支持超过 50 种语言,识别效果上优于 Whisper 模型。富文本识别:具备优秀的情感识别,能够在测试数据上达到和超过目前最佳情感识别模型的效果。支持声音事件检测能力,支持音乐、掌声、笑声、哭声、咳嗽、喷嚏等多种常见人机交互事件进行检测。高效推

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl

安装nodejs环境

本文介绍了如何通过nvm(NodeVersionManager)安装和管理Node.js及npm的不同版本,包括下载安装脚本、检查版本并安装特定版本的方法。 1、安装nvm curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash 2、查看nvm版本 nvm --version 3、安装

透彻!驯服大型语言模型(LLMs)的五种方法,及具体方法选择思路

引言 随着时间的发展,大型语言模型不再停留在演示阶段而是逐步面向生产系统的应用,随着人们期望的不断增加,目标也发生了巨大的变化。在短短的几个月的时间里,人们对大模型的认识已经从对其zero-shot能力感到惊讶,转变为考虑改进模型质量、提高模型可用性。 「大语言模型(LLMs)其实就是利用高容量的模型架构(例如Transformer)对海量的、多种多样的数据分布进行建模得到,它包含了大量的先验

【IPV6从入门到起飞】5-1 IPV6+Home Assistant(搭建基本环境)

【IPV6从入门到起飞】5-1 IPV6+Home Assistant #搭建基本环境 1 背景2 docker下载 hass3 创建容器4 浏览器访问 hass5 手机APP远程访问hass6 更多玩法 1 背景 既然电脑可以IPV6入站,手机流量可以访问IPV6网络的服务,为什么不在电脑搭建Home Assistant(hass),来控制你的设备呢?@智能家居 @万物互联

高并发环境中保持幂等性

在高并发环境中保持幂等性是一项重要的挑战。幂等性指的是无论操作执行多少次,其效果都是相同的。确保操作的幂等性可以避免重复执行带来的副作用。以下是一些保持幂等性的常用方法: 唯一标识符: 请求唯一标识:在每次请求中引入唯一标识符(如 UUID 或者生成的唯一 ID),在处理请求时,系统可以检查这个标识符是否已经处理过,如果是,则忽略重复请求。幂等键(Idempotency Key):客户端在每次

pico2 开发环境搭建-基于ubuntu

pico2 开发环境搭建-基于ubuntu 安装编译工具链下载sdk 和example编译example 安装编译工具链 sudo apt install cmake gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib 注意cmake的版本,需要在3.17 以上 下载sdk 和ex

pip-tools:打造可重复、可控的 Python 开发环境,解决依赖关系,让代码更稳定

在 Python 开发中,管理依赖关系是一项繁琐且容易出错的任务。手动更新依赖版本、处理冲突、确保一致性等等,都可能让开发者感到头疼。而 pip-tools 为开发者提供了一套稳定可靠的解决方案。 什么是 pip-tools? pip-tools 是一组命令行工具,旨在简化 Python 依赖关系的管理,确保项目环境的稳定性和可重复性。它主要包含两个核心工具:pip-compile 和 pip

EMLOG程序单页友链和标签增加美化

单页友联效果图: 标签页面效果图: 源码介绍 EMLOG单页友情链接和TAG标签,友链单页文件代码main{width: 58%;是设置宽度 自己把设置成与您的网站宽度一样,如果自适应就填写100%,TAG文件不用修改 安装方法:把Links.php和tag.php上传到网站根目录即可,访问 域名/Links.php、域名/tag.php 所有模板适用,代码就不粘贴出来,已经打