【C进阶】C程序是怎么运作的呢?-- 程序环境和预处理(上)

news2024/11/17 0:01:45

前言:

        由于c语言的程序编译链接的这块知识点不清楚,回来复习一遍,以便于好理解c++知识,我会尽快更新下一篇文章。

目录

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

2.翻译环境(编译+链接)

编译(编译器)

预编译(预处理)

1.头文件的包含

2.注释的测试

编译过程

汇编过程

链接

1.合并段表

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

计算机语言的发展

运行环境(翻译之后)

3.预处理详解 

3.1 预定义符号

3.2 #define

3.2.1 #define 定义标识符

3.2.2 #define 定义宏  

3.2.3#define 替换规则

3.2.4 #和##

3.2.5 带副作用的宏参数


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

总体过程:

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

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

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

2.翻译环境(编译+链接

我们常说一个test.c文件要经过以下步骤才能产生可执行的程序,那么具体是怎么做到的呢?

在vs编译器上是要经过以下过程的:

源文件和目标文件的关系:

链接库?

        VS是一个集成开发环境,集成很多的功能ctr1+F5,不方便观察每个细节的功能接下来,我使用gcc这个编译器给大家演示

        由于上图的操作把这个编译运行的步骤一次走完了,我们一步步来,咱们用指令让程序停在预编译的阶段:

编译(编译器)

预编译(预处理)

将控制台的内容放到test.i之后,预处理阶段所做的事情:

1.头文件的包含

2.注释的测试

编译过程

功能:把C语言代码翻译成汇编代码
经历四个过程:涉及一门课 -- 《编译原理》
1.语法分析
2.词法分析
3.语义分析
4.符号汇总
函数名,全局变量不会汇总 局部变量(函数运行起来才行)
推荐书籍:《程序员的自我修养》
链接:https://pan.baidu.com/s/1cVcZRTj772hJXm0mO0Q_cw?pwd=1234 
提取码:1234
编译阶段所做的事情,gcc操作过程:

汇编过程

1.把汇编代码转换成二进制的指令

2.形成符号表 

链接

1.合并段表

2符号表的合并和重定位

1、2的作用其实是在链接期间为这种跨文件的代码进行协作的时候起作用的。

在vscode中操作:

过程:

1.合并段表

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

        把无效的地址给替换掉,符号表和段表的关系是:符号表是段表的内容

那该如何体现这个作用呢?

        假设我们在test.c文件底下extern了一个函数Add,但是在add.c底下没有实现这个Add函数,就会输出以下错误(链接型错误)

        如果说这个函数写错的话,在main函数内调用大写的,但是在add.c内定义成小写的,是依然找不到

计算机语言的发展

运行环境(翻译之后)

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

3.预处理详解 

3.1 预定义符号

这些预定义符号都是语言内置的。
__FILE__       //进行编译的源文件
__LINE__     //文件当前的行号
__DATE__     //文件被编译的日期
__TIME__     //文件被编译的时间

执行:

这个预定义符号也是语言内置的

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

 而放在gcc编译器上面是可以执行通过的:

        编译器在代码编译的时候,会对函数和变量名重命名的,C++ 中会更加复杂

        在C语言中重命名的规则基本就是:加_

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 MAX 1000

 #define reg register//为 register这个关键字,创建一个简短的名字  

#define do_forever for(;;)//用更形象的符号来替换一种实现 

#define CASE break;case //在写case语句的时候自动把 break写上。

长行如何拆分,\后面只能是换行,不能是其他 

 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。

提问:
在define定义标识符的时候,要不要在最后加上 ; ?
比如:
#define MAX 1000;
#define MAX 1000
建议不要加上 ; , 这样容易导致问题。
比如下面的场景:
if ( condition )
        max = MAX ;
else
        max = 0 ;

这里会出现语法错误.

3.2.2 #define 定义宏  

下面是宏的声明方式:
#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 ) );
但是这样也会受到外部因素的影响:
#define DOUBLE(x) (x) + (x)

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

int a = 5 ;
printf ( "%d\n" , 10 * DOUBLE ( a ));
这将打印什么值呢?
warning
看上去,好像打印 100 ,但事实上打印的是 55.
我们发现替换之后:
printf ("%d\n",10 * (5) + (5));
乘法运算先于宏定义的加法,所以出现了55 .
这个问题,的解决办法是在宏定义表达式两边加上一对括号就可以了。
#define DOUBLE( x)   ( ( x ) + ( x ) )
提示:
所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。

所以这就涉及到#define的替换规则了

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(num, format) \ 
                        printf("the value of #num is "format)

并且让它输出:

这时候发现需要正确输入它们的数值:

这里只有当字符串作为宏参数的时候才可以把字符串放在字符串中。
1. 另外一个技巧是:
使用 # 把一个宏参数变成对应的字符串
比如:

代码中的 #value  会预处理器处理为: "value"
## 的作用
## 可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符。
注:这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。(也就是说这个拼接之后的变量名前提是要初始化一个值)

3.2.5 带副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
例如
x + 1 ; //不带副作用
x ++ ; //带有副作用
MAX 宏可以证明具有副作用的参数所引起的问题

在vscode2019内调试一下:

        想知道宏和函数的区别吗,欲知后事如何,请听下回分解。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

redis的keys命令和scan命令性能对比

项目场景 Redis的keys *命令在生产环境是慎用的,特别是一些并发量很大的项目,原因是Redis是单线程的,keys *会引发Redis锁,占用reids CPU,如果key数量很大而且并发是比较大的情况,效率是很慢的&#xff0c…

LLM之Agent(三):HuggingGPT根据用户需求自动调用Huggingface合适的模型

​ 浙大和微软亚洲研究院开源的HuggingGPT,又名JARVIS,它可以根据用户的自然语言描述的需求就可以自动分析需要哪些AI模型,然后去Huggingface上直接调用对应的模型,最终给出用户的解决方案。 一、HuggingGPT的工作流程 它的…

Hadoop的介绍与安装

1 Hadoop的简介 Hadoop是一个开源的大数据框架,是一个分布式计算的解决方案。Hadoop是由java语言编写的,在分布式服务器集群上存储海量数据并运行分布式分析应用的开源框架,其核心部件是HDFS与MapReduce。 HDFS是一个分布式文件系统&#x…

专业课145+总分440+东南大学920考研专业基础综合信号与系统数字电路经验分享

个人情况简介 今年考研440,专业课145,数一140,期间一年努力辛苦付出,就不多表了,考研之路虽然艰难,付出很多,当收获的时候,都是值得,考研还是非常公平,希望大…

单片机第三季-第四课:STM32下载、MDK和调试器

目录 1,扩展板使用的STM32芯片类型 2,使用普中科技软件下载程序 3,keil介绍 4,JLINK调试器介绍 5,使用普中的调试器进行debug 6,使用Simulator仿真 1,扩展板使用的STM32芯片类型 扩展版…

【EtherCAT详解】基于Wireshark的EtherCAT帧结构解析

写在前面 EtherCAT的报文比较繁琐,且一些参考书籍错误较多,且晦涩难懂,对于初学者,很难快速的入门。本文适用于有一定基础的研究者,如对报文有一些研究、对canopen协议有一定了解、并且对TwinCAT有了解的研究者。当然,对于初学者来说,也是很好的引导,少走很多弯路。本…

图中点的层次(图的BFS)

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环。 所有边的长度都是 1,点的编号为 1∼n。 请你求出 1 号点到 n 号点的最短距离,如果从 1 号点无法走到 n 号点,输出 −1。 输入格式 第一行包含两个整数 n 和 m。 接…

Nat easy IP ACL

0表示匹配,1表示任意(主机位0.0.0.255(255主机位)) rule deny source 192.168.2.1 0 设置拒绝192.168.2.1的主机通过 记住将其应用到接口上 [AR2]acl 2000 //创建基本ACL [AR2-acl-basic-2000]rule deny source 192…

WordPress发送邮件设置

WordPress在修改登陆邮箱或找回登陆密码的时候,通常都需要发送邮件来进行操作验证,但服务商又禁止了服务器对外发送邮件的25端口,很多虚拟主机本身也禁用了mail函数,根本发不了邮件。 此时我们可以使用QQ邮箱、网易邮箱或者其他企…

Win10安装ROS2遇到的小问题

按照网上教程安装ROS2,卡在了第一步。在cmd或powershell安装Chocolatey时,出现以下两种错误: “%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe” -NoPro …~here-string 标题后面和行尾之前不允许包含任何字符。 …… 或者 使…

字符串函数strlen的用法详解及其相关题目

strlne函数的使用 一.strlen函数的声明二.strlen函数的头文件三.相关题目代码1代码2题目1题目2题目3题目4题目5题目6 一.strlen函数的声明 size_t strlen ( const char * str );二.strlen函数的头文件 使用strlen函数我们需要使用以下头文件 #include <string.h>三.相…

LaTex入门简明教程

文章目录 写在前面安装Texlive的安装TeXstudio 的安装 LaTex 的使用节指令图指令表指令公式指令参考文献指令引用指令TeXstudio 编译 LaTex 的 \label{} 写法建议最后 写在前面 这篇文章面向没有任何 LaTex 基础的小白&#xff0c;主要讲解了 LaTex 的安装和使用。读完文章之后…

SVPWM原理及simulink

关注微♥“电击小子程高兴的MATLAB小屋”获得专属优惠 一.SVPWM原理 SPWM常用于变频调速控制系统&#xff0c;经典的SPWM控制主要目的是使变频器的输出电压尽量接近正弦波&#xff0c;并未关注输出的电流波形。而矢量控制的最终目的是得到圆形的旋转磁场&#xff0c;这样就要求…

大数据项目——基于Django协同过滤算法的房源可视化分析推荐系统的设计与实现

大数据项目——基于Django协同过滤算法的房源可视化分析推荐系统的设计与实现 技术栈&#xff1a;大数据爬虫/机器学习学习算法/数据分析与挖掘/大数据可视化/Django框架/Mysql数据库 本项目基于 Django框架开发的房屋可视化分析推荐系统。这个系统结合了大数据爬虫、机器学习…

SaToken利用Redis做持久化

官网解释 官网解释 教程 引入依赖 <!-- 提供Redis连接池 --> <dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId> </dependency><!-- Sa-Token 整合 Redis &#xff08;使用 jdk 默认序…

Linux--网络编程-ftp(TCP)网络通信-文件交互

项目要求&#xff1a;实现以下内容 远程控制&#xff1a; 1、查看服务器当前路径文件 ls 3、进入、退出服务器文件夹 cd 4、上传文件到服务器 put xxx 本地控制&#xff1a; 1、查看本地&#xff08;客户端&#xff09;文件 lls 2、进入客户端文件夹 lcd 3、获取服务器的文件…

【开发PaaS】基于Postgresql的开发平台Supabase

Supadase是开源的。我们选择可扩展的开源工具&#xff0c;使其易于使用。 Supadase不是Firebase的1对1映射。虽然我们正在构建Firebase提供的许多功能&#xff0c;但我们不会以同样的方式进行&#xff1a; 我们的技术选择大不相同&#xff1b;我们使用的一切都是开源的&#…

网络安全(二)-- Linux 基本安全防护技术

4.1. 概述 安全防护基础主要是会用Linux系统&#xff0c; 熟悉Linux基本操作命令。 在这个章节中&#xff0c;我们主要探讨自主访问控制&#xff08;许可位、ACL&#xff09;、文件属性、 PAM技术、能力机制等。 4.1.1. 补充命令 本章节中&#xff0c;涉及一些新的命令&#…

leetcode做题笔记1038. 从二叉搜索树到更大和树

给定一个二叉搜索树 root (BST)&#xff0c;请将它的每个节点的值替换成树中大于或者等于该节点值的所有节点值之和。 提醒一下&#xff0c; 二叉搜索树 满足下列约束条件&#xff1a; 节点的左子树仅包含键 小于 节点键的节点。节点的右子树仅包含键 大于 节点键的节点。左右…

@PostConstruct使用详解

一、简介 在Spring应用程序中启动时&#xff0c;有时需要在所有Bean都已加载&#xff0c;初始化并准备好之后执行某些自定义代码。这时&#xff0c;Spring提供了一个可用的方式&#xff0c;即使用PostConstruct注解。这个注解用于标记一个方法&#xff0c;这个方法将在Bean初始…