【C进阶】C语言终话,了解计算机的程序环境和预处理过程

news2024/9/24 6:28:47

目录

🏂前言🏂:

🏇一、详解编译与链接🏇:

        1.程序的翻译环境与执行环境:

        2.翻译环境:

        3.翻译阶段:

        ①.编译:

        ②.链接:

        4.运行环境:

🚵‍♂️二、预处理详解🚵‍♂️:

        1.预定义符号:

        2.#define:

        ①. #define 定义标识符:

        ②.#define 定义宏:

        ③. #define 替换规则:

        ④.带副作用的宏参数:

🤹总结🤹:


🛰️博客主页:✈️銮同学的干货分享基地

🛰️欢迎关注:👍点赞🙌收藏✍️留言

🛰️系列专栏:💐【进阶】C语言学习

                       🧧  C语言学习

🛰️代码仓库:🎉VS2022_C语言仓库

        家人们更新不易,你们的👍点赞👍和⭐关注⭐真的对我真重要,各位路过的友友麻烦多多点赞关注,欢迎你们的私信提问,感谢你们的转发!

        关注我,关注我,关注我,你们将会看到更多的优质内容!!


🏡🏡 本文重点 🏡🏡:

🚅 详解编译与链接 🚃 预处理详解 🚏🚏

🏂前言🏂:

        上节课中我们完整的学习了文件操作,实现了在没有数据库的情况下对数据在本地磁盘中的读写,并且依托文件操作,对我们的通讯录进行了进一步的优化。而从这节课开始,我们就将进入到 C 语言的最后一个板块预处理的相关知识的学习中了。废话再多也没用,我们直接上干货!

🏇一、详解编译与链接🏇:

        1.程序的翻译环境与执行环境:

        在研究程序的编译与链接细节之前,我们首先要了解我们程序的翻译以及执行环境,我们要知道,在 ANSI C 的任何一种实现中,都存在着两种环境

★ 翻译环境:在该环境中,我们所写下的 .c 等源代码将被转化成可执行的机器指令

★ 执行环境:在该环境下,将会真正执行经过转化后生成的 .exe 可执行文件

        2.翻译环境:

        在翻译环境中执行的操作,简单来说可以分为三个步骤

第一步:组成一个程序的每个源文件通过编译过程分别转换成目标代码(.obj)

第二步每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序

第三步:链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人 的程序库,将其需要的函数也链接到程序中。

        其中,链接库指的是链接我们的工程中所引用的库函数所依赖的函数库以及各种第三方函数库

        3.翻译阶段:

        如果我们再细分下来,翻译阶段又可以分为两个阶段,即编译与链接

(注:VS 2022中难以观察这部分过程,须在 Linux 系统中查看,这部分的知识未涉及,故不再通过实例进行验证,小伙伴们只需要知道实现过程的理论即可,不必深究)

        ①.编译:

        编译阶段又可以细分为三个阶段

★ 预编译(预处理):头文件的包含、#define 定义符号的替换、注释的删除,均为文本操作。

★ 编译:将 C 语言的代码翻译成汇编代码,其具体动作包含了语法分析、词法分析、语义分析、符号汇总等。

★ 汇编:将汇编指令翻译为二进制指令,具体动作包含了形成符号表等。

        ②.链接:

        连接阶段就负责各个文件的链接相关操作,其具体动作包括了:

★ 合并段表

★ 合并符号表

★ 重定位符号表

        4.运行环境:

        在这个环境下,我们的程序就真正进入了运行阶段。我们程序的执行过程可以简述为下面四个步骤

①. 程序载入内存中:在有操作系统的环境中该步骤通常由操作系统完成;而在独立环境中则必须由我们自己手动完成;独立环境中的程序也有可能通过执行可执行代码置入只读内存来完成。

②. 调用 main 函数:程序正式开始执行。

③. 顺序执行程序代码:在这个阶段中,我们的程序会使用一个运行时堆栈来存储函数的局部变量和返回地址。同时程序也可以使用静态内存来存储变量,并且这些存储于静态变量中的变量在整个程序的执行过程中将始终保留它们的值。

④. 终止程序:一般情况下会正常终止 main 函数,但我们的程序也有可能会意外终止

🚵‍♂️二、预处理详解🚵‍♂️:

        1.预定义符号:

        在 C 语言中有一些内置的预处理符号

★ __FILE__:进行编译的源文件

★ __LINE__:文件当前的行号

★ __DATE__:文件被编译的日期

★ __TIME__:文件被编译的时间

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

        这些符号都是语言内置的,可以直接使用

#include<stdio.h>

int main()
{
	printf("进行编译的源文件:\n");
	printf("%s\n", __FILE__);
	printf("文件当前的行号:\n");
	printf("%d\n", __LINE__);
	printf("文件编译的日期:\n");
	printf("%s\n", __DATE__);
	printf("文件编译的时间:\n");
	printf("%s\n", __TIME__);

	return 0;
}

        直接进行编译运行即可查看结果:

        2.#define:

        #define 的用处非常多,就比如我们常用的定义标识符常量定义宏等等,而在这个过程中,也有一些细节值得我们去注意。

        ①. #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 的最后,到底要不要加上分号 “ ; ” 呢?我们说最好不要添加,这是因为有可能会导致语法错误

#define MAX 1000;

#include<stdio.h>

int main()
{
	int a = 1;
	int max = 0;
	if (a)
	{
		max = MAX;
	}
	printf("max = %d\n", max);

	return 0;
}

        例如这个例子,在上面我们说过,在预编译(预处理)阶段,会进行 #define 定义标识符的替换,于是 if 语句中的代码将会被替换为:

将被替换为:max = (MAX;);

即:max = MAX;;

        此时我们发现,经过符号替换后,出现了语法错误

        ②.#define 定义宏:

        各位小伙伴们要知道,在#define 的机制中,包括了一个规定,这个规定允许把参数替换到文本中,这种实现通常称为定义宏(或简称为宏)

        宏的申明方式为:

#define name( parament-list ) stuff

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

        并且在这里各位小伙伴们千万要注意参数列表的左括号必须与 name 紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分

        例如我们定义一个宏 sqr 用来计算平方:

#define sqr(x) x*x

        这个宏将会在执行时接受一个参数 x,如果在上述声明之后,将下面的代码置于程序中:

sqr( 5 );

        则预处理器就会将上面的表达式替换为:

5 * 5;

        ⚠️警告:这个宏的书写仍是不严谨的,随时可能导致错误的产生。⚠️

        例如在下面这样的情况中:

#define sqr(x) x*x

#include<stdio.h>

int main()
{
	int a = 5;
	printf("%d\n", sqr(a + 1));
	return 0;
}

        这段代码乍一看好像并没有什么问题,但事实上,这段代码在经过预编译的符号替换后将会变成:

#define sqr(x) x*x

#include<stdio.h>

int main()
{
	int a = 5;
    printf("%d\n", a + 1 * a + 1);
	return 0;
}

        原本想要计算 6 * 6,却在预编译的符号替换后出现了错误,打印了 5 + 1 * 5 +1

        我们的解决方法很简单,只需要对宏定义进行简单修改即可:

#define sqr(x) (x)*(x)

#include<stdio.h>

int main()
{
	int a = 5;
	printf("%d\n", sqr(a + 1));
	return 0;
}

        通过两个小括号的使用,在预编译阶段,这段代码将被替换为

printf("%d\n",(a+1)*(a+1));

        但是你认为这样就没有错误了吗?其实这样写还有可能出现下面这样的错误

#define sqr(x) (x)+(x)

#include<stdio.h>

int main()
{
	int a = 5;
	printf("%d\n", 10 * sqr(a));
	return 0;
}

        这段代码经过了与编译后又将会被替换为:

printf("%d\n", 10 * (a)+(a));

        此时,由于操作符中 * 的优先级高于 +,因此计算结果又将出现差错。

        我们的解决方案是再使用一对小括号对宏定义进行简单修改即可:

#define sqr(x) ((x)+(x))

#include<stdio.h>

int main()
{
	int a = 5;
	printf("%d\n", 10 * sqr(a));
	return 0;
}

        这时的代码在经过预编译后将会被替换为:

printf("%d\n", 10 * ((a)+(a)));

        总结:

 在用于对数值表达式进行求值的宏定义中,都应该用这种方式加上括号避免在使用宏时由于参数中操作符或邻近操作符之间不可预料的相互作用,从而导致在经过预编译阶段的符号替换后出现错误

        ③. #define 替换规则:

        #define 在进行符号替换时,遵循以下规则

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

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

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

        但是各位小伙伴们在这里要注意两点

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

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

        ④.带副作用的宏参数:

        这里所说的副作用是指表达式求值的时候出现的永久性效果

x + 1;//不带副作用
x++;//带有副作用

        而这样的代码会通过下面这样的方式对结果造成影响

#define MAX(a,b) ((a)>(b)?(a):(b))

#include<stdio.h>

int main()
{
	int x = 5;
	int y = 8;
	int z = MAX(x++, y++);
	printf("x = %d y = %d z = %d\n", x, y, z);
	return 0;
}

        这段代码的实际打印结果为:

        由这个例子我们可以得知,当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就有可能会出现危险,导致不可预测的后果

🤹总结🤹:

        到这里,我们关于预处理的前半部分就学习结束啦,这部分的内容不会直接在我们的代码中体现出来,但是经过预处理后,在我们看不见的地方就有可能会产生一些令我们意想不到的错误。所以希望小伙伴们下去以后对于这部分的知识好好研究,牢固掌握,尽最大可能写出更加优质的代码,避免尽可能多的错误,最大程度的减少我们自己的工作量

        🔥🔥傲不行长,欲不行纵,乐不行极,志不行满🔥🔥

        更新不易,辛苦各位小伙伴们动动小手,👍三连走一走💕💕 ~ ~ ~  你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!

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

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

相关文章

Java 并发编程 (一)Semaphore和Exchanger的使用

Semaphore和Exchanger的使用 Semaphore 功能介绍 Semaphore 主要作用就是限制线程并发数量。 Semaphore 的构造函数中permits 可以控制最大并发数。每个线程可以acquire指定数量的 permit 。但是acquire n个 则需要释放n个。防止被阻塞 public class MySemaphore {// permits…

【python】鞭炮快乐响,春联贴门上,祝福送到你身旁

前言 大家早好、午好、晚好吖 ❤ ~ 现在贴春联已成风俗&#xff0c;红色的对联贴在大门上&#xff0c;房子顿时生辉。 正如诗云&#xff1a;“喜气临门红色妍&#xff0c;家家户户贴春联&#xff1b;旧年辞别迎新岁&#xff0c;时序车轮总向前。” 今天&#xff0c;我们就用p…

静态路由的拓展配置

静态路由的拓展配置&#xff1a; 1、负载均衡 当路由器访问同一个目标具有多条开销相似的路径&#xff08;经过路由器的数量&#xff09;时&#xff0c;可以让流量拆分后沿多条路径同时传输&#xff0c;可以达到叠加带宽的效果。&#xff08;当开销不相似时做负载均衡&#xff…

数据可视化系列-03AIPL消费者行为全链路可视化模型实践

文章目录4.AIPL消费者行为全链路可视化模型实践4.1、用户画像用户画像产生的原因用户画像概述用户画像构成原则第一类用户画像 User Persona第二类用户画像User Profile4.2、标签体系标签体系简介标签分类贴标签的方式标签的优化用户画像标签和权重4.3、用户画像大数据应用4.4、…

USB基础

一 USB 芯片组成 同以太网类似&#xff0c;USB芯片也分为Controller部分(主机控制器/设备控制器)和PHY部分(收发器) 两大部分组成。 Controller部分 主要实现USB的协议和控制&#xff0c;内部逻辑主要有 MAC层&#xff0c;CSR层&#xff0c;FIFO层等。 MAC层实现安装USB协议…

一文看懂 Redis 主从同步的原理

Redis 主从同步的基本原理有三种&#xff0c;分别是&#xff1a;全量复制&#xff0c;基于长链接的命令传播&#xff0c;增量复制。 接下来分别说说这三种主从间同步的原理。 全量复制 当我们启动多个 Redis 实例的时候&#xff0c;它们相互之间就可以通过 replicaof&#x…

新人转行IC该怎么选择岗位?(内附各岗位学习视频)

最近看到不少同学在后台提问&#xff1a;新人转行IC该怎么选择岗位&#xff1f;其实转行本身就是一件大事&#xff0c;转行之前一定要做好调研&#xff0c;选择适合自己的岗位&#xff0c;这样才能规划好职业生涯。 IC行业这几个岗位是最常见的&#xff1a;数IC前端设计工程师…

PoolFormer实战:使用PoolFormer实现图像分类任务(二)

文章目录训练部分导入项目使用的库设置随机因子设置全局参数图像预处理与增强读取数据设置Loss设置模型设置优化器和学习率调整算法设置混合精度&#xff0c;DP多卡&#xff0c;EMA定义训练和验证函数训练函数验证函数调用训练和验证方法运行以及结果查看测试热力图可视化展示完…

JSON数据解析

1.基础 JSON(JavaScript Object Notation) 是一种通用的轻量级数据交换文本格式。它很容易让人阅读和编写&#xff0c;也便于机器进行解析和生成。它使用JavaScript语法来存储和描述数据对象&#xff0c;但是JSON完全独立于JavaScript。JSON可适用于多种流行编程语言。这些特性…

JavaScript 用法

文章目录JavaScript 用法<script><script><script> 标签<body><body><body> 中的 JavaScriptJavaScript 函数和事件在 <head><head><head> 或者 <body><body><body> 的JavaScript<head><hea…

计算机网络知识解析

本篇文章主要讲的是计算机网络相关的内容&#xff0c;需要有一定的计算机网络的基础知识才能汲取更多的知识。当然没基础也可以看懂&#xff0c;会对计算机网络有一个基础的理解。在这一篇介绍中&#xff0c;我尽可能的覆盖一些面试中的问题&#xff0c;通过本篇文章&#xff0…

2022年吃瓜事件拆解,打造爆款,让你拥有顶级营销思维!

大环境下&#xff0c;谁又是幸存者&#xff1f;2022年吃瓜事件拆解&#xff0c;打造爆款&#xff0c;让你拥有顶级营销思维&#xff08;完整版&#xff09;&#xff01;小伍带你了解【商业营销内核】&#xff01;咱们开讲&#xff01;【事件1】张兰 vs 大S&#xff0c;大女主张…

【PyTorch学习1】B站刘二大人《PyTorch深度学习实践》——线性模型(Linear Model)

b站课程链接&#xff1a;线性模型 1.基本思想&#xff1a; 给出了一组一维数据&#xff0c;定义了一个简单的线性拟合函数&#xff0c;通过穷举法来列出一些权重&#xff08;拟合函数的系数&#xff09;&#xff0c;并计算这些权重对应的拟合损失函数&#xff08;使用均方误差…

C/C++智能指针详解

系列文章目录 文章目录系列文章目录前言一、什么是智能指针&#xff1f;二、使用方法1.shared_ptr2.unique_ptr3.weak_ptr前言 对C/C学习感兴趣的可以看看这篇文章哦&#xff1a;C/C教程 本章主要介绍一些C/C中智能指针的实现原理以及如何使用 一、什么是智能指针&#xff1…

计算机组成原理复习:存储器系统

计算机组成原理复习&#xff1a;存储器系统3.1 存储器的层次化结构3.2 半导体随机存储器&#xff08;RAM&#xff0c;易失性存储器&#xff09;3.2.1 静态随机存储器&#xff08;SRAM&#xff09;3.2.2 动态随机存储器&#xff08;DRAM&#xff09;3.2.3 只读存储器&#xff08…

token认证、Express中实现token的过程

一、token认证 1. 什么是token token的意思是“令牌"&#xff0c;是服务端生成的一串字符串&#xff0c;作为客户端进行请求的一个标识。当用户第一次登录后&#xff0c;服务器生成一个token并将此token返回给客户端&#xff0c;以后客户端只需带上这个token前来请求数据…

Jenkins搭建 笔记总结

文章目录1. 什么是CI/CD(DI)&#xff1f;2. 环境 安装部署3. 搭建 GitLab服务器&#xff08;ssh方式搭建&#xff09;4. 搭建 GitLab服务器&#xff08;Docker方式搭建&#xff09;4.1 搭建Docker环境4.2 docker容器 安装gitlab5. GitLab 提交代码6. Jenkins 配置Maven Git自动…

【信息论与编码 沈连丰】第五章:离散信道及其信道编码

【信息论与编码 沈连丰】第五章&#xff1a;离散信道及其信道编码第五章 离散信道及其信道编码5.1 信道的分类及其描述5.2 无扰离散信道的传输特性5.3 有扰离散信道的传输特性5.4 译码准则5.5 有扰离散信道的信道编码定理5.6 信道编码定理的应用第五章 离散信道及其信道编码 5…

6\8. 语义分析和中间代码生成

6&8. 语义分析和中间代码生成 从几个问题说起&#xff1a; 要计算 34*5 的值如何计算&#xff1f;按照以前的方法&#xff0c;我们会想到利用符号栈和数值栈来完成这个运算。但是有了语法分析之后我们不再需要进行这么复杂的代码构造了。第一&#xff0c;我们可以使用自顶…

AcWing1206.剪格子——学习笔记(未AC)

题目&#xff1a;1206. 剪格子 - AcWing题库https://www.acwing.com/problem/content/description/1208/ import java.util.Scanner;public class Main {public static void main(String args[]){//读数据Scanner input new Scanner(System.in);String MN input.nextLine();S…