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

相关文章

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

在不同系统间迁移Python程序的方法与教程

《在不同系统间迁移Python程序的方法与教程》本文介绍了几种将Windows上编写的Python程序迁移到Linux服务器上的方法,包括使用虚拟环境和依赖冻结、容器化技术(如Docker)、使用An... 目录使用虚拟环境和依赖冻结1. 创建虚拟环境2. 冻结依赖使用容器化技术(如 docker)1. 创

Go语言中三种容器类型的数据结构详解

《Go语言中三种容器类型的数据结构详解》在Go语言中,有三种主要的容器类型用于存储和操作集合数据:本文主要介绍三者的使用与区别,感兴趣的小伙伴可以跟随小编一起学习一下... 目录基本概念1. 数组(Array)2. 切片(Slice)3. 映射(Map)对比总结注意事项基本概念在 Go 语言中,有三种主要

C语言中自动与强制转换全解析

《C语言中自动与强制转换全解析》在编写C程序时,类型转换是确保数据正确性和一致性的关键环节,无论是隐式转换还是显式转换,都各有特点和应用场景,本文将详细探讨C语言中的类型转换机制,帮助您更好地理解并在... 目录类型转换的重要性自动类型转换(隐式转换)强制类型转换(显式转换)常见错误与注意事项总结与建议类型

在Mysql环境下对数据进行增删改查的操作方法

《在Mysql环境下对数据进行增删改查的操作方法》本文介绍了在MySQL环境下对数据进行增删改查的基本操作,包括插入数据、修改数据、删除数据、数据查询(基本查询、连接查询、聚合函数查询、子查询)等,并... 目录一、插入数据:二、修改数据:三、删除数据:1、delete from 表名;2、truncate

Go语言利用泛型封装常见的Map操作

《Go语言利用泛型封装常见的Map操作》Go语言在1.18版本中引入了泛型,这是Go语言发展的一个重要里程碑,它极大地增强了语言的表达能力和灵活性,本文将通过泛型实现封装常见的Map操作,感... 目录什么是泛型泛型解决了什么问题Go泛型基于泛型的常见Map操作代码合集总结什么是泛型泛型是一种编程范式,允

Android kotlin语言实现删除文件的解决方案

《Androidkotlin语言实现删除文件的解决方案》:本文主要介绍Androidkotlin语言实现删除文件的解决方案,在项目开发过程中,尤其是需要跨平台协作的项目,那么删除用户指定的文件的... 目录一、前言二、适用环境三、模板内容1.权限申请2.Activity中的模板一、前言在项目开发过程中,尤

VScode连接远程Linux服务器环境配置图文教程

《VScode连接远程Linux服务器环境配置图文教程》:本文主要介绍如何安装和配置VSCode,包括安装步骤、环境配置(如汉化包、远程SSH连接)、语言包安装(如C/C++插件)等,文中给出了详... 目录一、安装vscode二、环境配置1.中文汉化包2.安装remote-ssh,用于远程连接2.1安装2

C语言小项目实战之通讯录功能

《C语言小项目实战之通讯录功能》:本文主要介绍如何设计和实现一个简单的通讯录管理系统,包括联系人信息的存储、增加、删除、查找、修改和排序等功能,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录功能介绍:添加联系人模块显示联系人模块删除联系人模块查找联系人模块修改联系人模块排序联系人模块源代码如下

基于Go语言实现一个压测工具

《基于Go语言实现一个压测工具》这篇文章主要为大家详细介绍了基于Go语言实现一个简单的压测工具,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录整体架构通用数据处理模块Http请求响应数据处理Curl参数解析处理客户端模块Http客户端处理Grpc客户端处理Websocket客户端