计算机是怎么读懂C语言的?

news2025/2/28 3:36:40

文章目录

  • 前言
  • 程序环境
  • 翻译环境
    • 翻译环境分类
      • 编译
        • 预处理
          • 预处理符号
            • 预定义符号
            • #define
            • #undef
          • 命令行定义
          • 条件编译
          • 文件包含
            • 头文件包含查找规则
            • 嵌套文件包含
          • 其他预处理指令
        • 编译阶段
        • 汇编
      • 链接

🎉welcome🎉
✒️博主介绍:博主大一智能制造在读,热爱C/C++,会不定期更新系统、语法、算法、硬件的相关博客,浅浅期待下一次更新吧!
😘博客制作不易,👍点赞+⭐收藏+➕关注

前言

  • 在我之前的一篇文章中,写到了目前主流语言的优缺点,那其实对于语言来说,剖析到最底层,都是二进制,只是语法不同,那计算机是怎么区分语言,在程序写好到结束中间,发生了哪些事情?本篇文章从C语言角度出发,剖析一下从写好程序到运行发生了哪些事情。

程序环境

  • 在ANSI C下的任何程序当中,都有两种不同的环境:
  1. 翻译环境:这个环境当中可以将程序的源代码转换成可执行的机器指令。
  2. 执行环境:它用于实际执行代码。

翻译环境

  • 翻译环境可以将程序翻译成可执行的机器指令,对于其他语言也是这样,只有翻译成可执行的机器指令,计算机才可以识别,那C语言的翻译阶段是这样的:

在这里插入图片描述

  • 在C语言中,翻译的大致过程就如上图所示,在程序写好进行编译的时候,会编译器(集成开发环境)会对每个程序文件(.c)单独进行翻译,翻译成一个目标文件(在windows环境下面后缀是.obj),每个目标文件都进过链接器,链接器外部接链接库,通过链接库和链接器生成一个可执行程序(.exe)。

翻译环境分类

  • 翻译环境大致分为编译和链接两个阶段

编译

  • 编译时翻译环境最开始的阶段,他分成三个步骤:
  1. 预编译(预处理)
  2. 编译(处理)
  3. 汇编

预处理

  • 预处理阶段会进行对头文件的包含,对于用#define定义的符号进行替换和删除,还有注释的删除,和文本操作,其中#define定义和头文件的包含都用到了预处理符号。
预处理符号
预定义符号
  • 预定义符号是语言内置的符号,有以下几种:
__FILE__      //进行编译的源文件
__LINE__     //文件当前的行号
__DATE__    //文件被编译的日期
__TIME__    //文件被编译的时间
__STDC__    //如果编译器遵循ANSI C,其值为1,否则未定义
#define
  • #define在前面学习常量的时候是有进行简略的介绍的,用#define定义的标识符常量,但是#define是不仅仅可以定义标识符常量的,还可以定义一些宏,那这些宏具体可以干什么呢?可以把他理解为另类的函数,宏的定义方法如下
#define name(parament-list) stuff;
  • 其中name如何函数命一样,parament-list是一个符号表,可以理解为函数的参数,stuff可以理解为要实际做的事情,符号表内的符号会出现在stuff里面,对于宏来说,他实际是把name(parament-list)进行替换,替换成后面的是stuff,可以用代码进行验证一下:
#include<stdio.h>

#define ADD(x,y) x+y

int main()
{
	printf("%d", 3 * ADD(3, 4));
	return 0;
}

在这里插入图片描述

  • 表达式的结果式13,但是按照猜想得到的结果应该是21,3+4=7,在和3相乘,那13怎么得到的?上面说到,宏是进行的替换,将后面的3+4替换下来,那这个表达式实际上是3*3+4,就是9+4,那就是13,那可不可以让3+4先算在乘3呢?只需要加括号,对于宏来说,不要吝啬括号,那对上面代码进行修改:
#include<stdio.h>

#define ADD(x,y) (x+y)

int main()
{
	printf("%d", 3 * ADD(3, 4));
	return 0;
}

在这里插入图片描述

  • 那如果现在是一个乘法的宏呢?
#include<stdio.h>

#define MUL(x,y) (x*y)

int main()
{
	printf("%d", 3 * MUL(3+3, 4+4));
	return 0;
}

在这里插入图片描述

  • 和我们要得到的结果是不一样的,我们想要得到的是144,但是得到的是57,那我将内容替换到程序当中3+34+4,也就是3+12+4,
    得出19,19
    3,得到57,但是我们想要的是先相加在相乘的,那就还需要加括号,如下所示:
#include<stdio.h>

#define MUL(x,y) ((x)*(y))

int main()
{
	printf("%d", 3 * MUL(3+3, 4+4));
	return 0;
}

在这里插入图片描述

  • 这样替换下来的就是(3+3)*(4+4),结果是48,和我们想要得到的结果是一样, 所以对于宏而言,不需要吝啬括号。

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

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先
    被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上
    述处理过程。

#和##的作用

  • u在正常使用printf打印的时候可以将不用%s打印两个字符串吗?是可以的:
#include<stdio.h>
int main()
{
    printf("这个数字是:""%d", 10);
    return 0;
}

在这里插入图片描述

  • 那在宏中可以吗?也是可以的:
#include<stdio.h>
#define PRINT(A ,B) printf("数字是"A"\n",B);
int main()
{
    PRINT("%d",10);
    return 0;
}

在这里插入图片描述

  • 但是这样只是对于字符串是参数的时候才能将字符串放进去,还有一种方法,就是利用#,它的作用是将一个宏参数变成字符串,如果现在要计算一个加法表达式的结果,就可以用这个来更直观的表达:
#include<stdio.h>
#define PRINT(A ,B) printf(#B"的结果是"A"\n",B);

int main()
{
	PRINT("%d",1+2);
	return 0;
}

在这里插入图片描述

  • ##的作用
    ##可以把位于它两边的符号合成一个符号。
    它允许宏定义从分离的文本片段创建标识符。
#include<stdio.h>
#define _ADD(A ,B) num##A+=B;

int main()
{
	int num5 = 5;
	_ADD(5, 10);
	printf("%d", num5);
	return 0;
}

在这里插入图片描述
宏的副作用

  • 对于宏来说,有些是有副作用的,比如++操作符,看下面的代码:
#include<stdio.h>

#define MAX(A,B) ((A)>(B)?(A):(B))

int main()
{
	int a = 1;
	int b = 2;
	int c = MAX(a++, b++);
	printf("a= %d b= %d c=%d", a, b, c);
	return 0;
}
  • 结果是什么?按照猜想的结果,a++是2,b++是3,然后比大小赋给c,3比2大,所以b++在执行一次,是4,那现在a是2,b是4,c也是4,结果是正确的吗?运行起来看看:

在这里插入图片描述

  • c是3,这就是因为,宏本质上还是替换,赋值给c的是((a++)>(b++)?(a++):(b++));这个表达式的结果是b++,而b++是先进行了一次++,得到3,表达式结果还是b++,但是是后置++,那就是先使用在++,那就是先赋值3,在进行++。

宏对比函数的优缺点

  • 那宏和函数都可以实现某种功能,那他们有什么区别吗?就是单纯的书写格式不一样吗?不仅仅是这样,宏的优点在于宏的速度是优于函数的,并且对于宏,是不需要去定义类型的,那宏就没有缺点吗?有,当我们使用宏的时候,一份宏定义的代码将插入到程序中,除非宏比较短,否则可能大幅度增加程序的长度,而且宏是没法调试的,我们是不能直接进入宏调试的,因为宏是替换到程序当中,编译器是认得,但是我们是不知道内部有无问题的,而且上面说到宏没有定义类型,也就不够严谨,并且宏可能会带来运算符优先级的问题,导致我们想的和实际跑出来的内容不一样。

宏的命名

  • 对于宏的命名而言,通常是全部大写的,这也是一个约定俗成的东西,而函数的命名就通常不是大写的,这也可以让其他程序员在看程序的时候,一眼看出来哪个是宏哪个是函数。
#undef
  • 如果在写程序的时候,宏目前的功能不满足当前的程序或者不满足当前我们想要得到的效果,但是我们知道,直接修改宏内代码是个不太好的习惯,那有没有办法可以不动我们程序内本身就有的东西,然后修改掉宏实现的内容呢?这里就有一个新的操作符——#undef,它的作用并不是修改一个宏,而是移除,那怎么去使用的?它的语法格式是这样的:
  • #undef NAME
  • 那现在可以使用一下,看看#undef的功能是不是和我说的是一样的:
#include<stdio.h>

#define MAX(A,B) ((A)>(B)?(A):(B))
#undef MAX

int main()
{
	int a = 1;
	int b = 2;
	int c = MAX(a++, b++);
	printf("a= %d b= %d c=%d", a, b, c);
	return 0;
}

在这里插入图片描述

  • 可以看到是有报错的,那现在在编译器眼中,就没有了MAX这个宏,这个时候就可以在写一个MAX的宏来实现我们现在想要实现的内容了。
#include<stdio.h>

#define MAX(A,B) ((A)>(B)?(A):(B))
#undef MAX
#define MAX(A,B) ((A)<(B)?(B):(A))

int main()
{
	int a = 1;
	int b = 2;
	int c = MAX(a++, b++);
	printf("a= %d b= %d c=%d", a, b, c);
	return 0;
}

在这里插入图片描述

命令行定义
  • C语言的大部分编译器都提供了一种功能,允许在命令行当中定义符号,用于编译过程的程序,通常是在Linux环境下使用,在windows环境下一般是不可以的,那这个是什么意思呢?举个例子:
#include<stdio.h>
int main()
{
    int arr[SIZE];
    int i=0;
    for(i=0;i<SIZE;i++)
    {
        arr[i]=i;
    }
    for(i=0;i<SIZE;i++)
    {
        printf("%d" ,arr[i]);
    }
    printf("\n");
    return 0;
}
  • 可以看到上面的程序当中有一个没有初始化和定义的变量SIZE,那程序正常情况下是跑不起来的,是会报错的,那应该怎么在没有修改程序的情况下,让它跑起来,在Linux环境下面,可以在命令行规定,规定格式是这样的:
gcc -D SIZE=10 test.c
  • 这样编译器就会知道SIZE是10,程序就可以正常运行起来。
条件编译
  • 在编译一个程序的时候,当我们有部分语句不需要的时候,可以进行注释,那有没有其他方法呢?有,可以用条件编译指令,那条件编译指令具体怎么使用,往下看:
  • 在我们的程序当中,会有一些调试性的代码,这个时候,这些代码就和鸡翅一样,食之无味,弃之可惜,那现在就可以使用选择性的编译:
#include<stdio.h>

#define _ DEBUG _

int main()
{
	int i = 0;
	int arr[10] = { 0 };
	for(i = 0; i<10;i++)
	{
		arr[i] = i;
		printf("%d", arr[i]);//为了知道上面的数是否放到数组里面
	}
	return 0;
}
  • 可以看到上面代码中,for循环内的printf是用于验证我们的数字成功放到了数组当中没有,那现在我们知道放成功了,不需要它了,但是又不想删除和注释,这个时候使用条件编译指令当中的一个:
#include<stdio.h>

#define _ DEBUG _

int main()
{
	int i = 0;
	int arr[10] = { 0 };
	for(i = 0; i<10;i++)
	{
		arr[i] = i;
		#ifdef _ DEBUF _//如果_ DEBUF _为真,进入到里面执行语句
		printf("%d", arr[i]);//为了知道上面的数是否放到数组里面
		#endif//结束标志
	}
	return 0;
}
  • 那这样写,对于_ DEBUF _,它的值是为假的,那就不会走ifdef,那就不会运行printf,这样就实现了我们不想删除也不想注释,但是让程序不跑某些代码的功能了,常见条件编译指令还有很有:
//1
#if  //常量表达式
//...
#endif
//这里的常量表达式由预处理求值
//比如上面程序当中的_ DEBUG _,编译器会知道是什么,但是必须要提前声明
//2.多个分支的条件编译
#if //常量表达式
//...
#elif //常量表达式
//...
#else
//...
#endif
//3.判断是否被定义
#if defuned(symbol)
#ifdef symbol

#if !defined(symbol)
#ifndef symbol
//4.嵌套指令
#if defined(OS_UNIX)
	#ifdef OPTION1
		unix_version_option1();
	#endif
	#ifdef OPION2
		unix_version_option2();
	#endif
#elif defined(OS_MSDOS)
	#ifdef OPION2
		msdos_version_option2();
	#ednif
#endif
文件包含
  • 我们知道#include是用于包含头文件的,在进入这个头文件的时候,就对头文件指向的文件进行了编译,这个时候在编译器眼里#include的地方就是包含的头文件的内容,所以,我们包含了多少次头文件,就会替换多少次,所以我们包含了多少次头文件,就编译了多少次头文件,所以最好不要重复包含同一个头文件很多次
头文件包含查找规则
  • 在包含头文件的时候,我们发现,有两种包含方式,一种是<>,另一种是"",那它们两个包含方式有什么区别呢?
  • 对于""而言,它会先在源文件所在目录下查找,如果没有找到这个头文件,编译器就像查找库函数头文件一样在标准位置查找头文件,如果没有在标准位置查找到,就提示编译错误,而标准位置就在编译器的下载目录当中,比如我用的vs2020,那标准位置就在我的vs2020的默认下载路径。
  • 而<>,会直接在标准位置查找,找不到就报错,对于库函数的头文件,也可以用""来引,但是就会降低效率,因为本来就存放在标准位置,直接就可以查,但是用“”先查本地,相比较就会慢一点,而且也不容易区分本地的头文件和库函数的头文件,所以对于头文件用什么引,要根据情况而定。
嵌套文件包含

在这里插入图片描述

  • 如上图所示,comn.h和comn.c是公共模板,test1.h和test1.c使用了公共模块,test2.h和test2.c也使用了公共模块,test.h和test.c使用了test1模块和test2的模块。
  • 那这样test是不是同时包含了两份comn.h的内容,那就重复包含了,那有没有什么方法解决这个问题呢?使用条件编译,在每个头文件最前面加上条件编译指令,如下所示:
#ifdef _TEST_H_
#define 
//头文件的包含
#endif
  • 或者:
#pragma once
  • 这样可以避免掉我们的头文件重复包含
其他预处理指令
  • 还有一些预处理指令:
#error
#pragma
#line
...

编译阶段

  • 预处理结束后,就要开始我们的编译阶段,编译阶段会生成一个test.s的文件,这个文件里面会放我们程序转成的汇编代码,编译器通常会在这个阶段进行语法分析、词法分析、语义分析,符号汇总,那前面三个分析的作用可以猜到,是将我们的C语言程序进行分析然后转换成汇编代码,那符号汇总是什么?它会干些什么事?符号汇总其实就是将我们的全局变量和全局函数进行汇总,然后对其进行汇编,然后就会到汇编这一步。

汇编

  • 汇编阶段会在编译阶段之后进行,它会生成一个.obj,.obj是在windows环境下生成的,在Linux环境下生成.o文件,那这些文件内容是什么?里面全是二进制指令,还会生成符号表,符号表就和编译阶段的符号汇总有关,那我们的.obj或者.o文件是二进制指令,能不能打开看看呢?我们是直接打不开的,可以使用一个工具readelf,可以打开文件看到这些二进制指令;那符号表是什么呢?符号表就是用于判断在编译过程中是否有使用符号。

链接

  • 链接阶段是链接库和我们的生成的.obj或者.o文件生成一个.exe文件,这个文件可以执行,能看到我们程序实际跑出的结果是什么。

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

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

相关文章

Linux 目录操作命令

目录操作命令 进入目录 cd命令 cd 目录名 进入指定目录 cd / 进入根目录 cd … 返回上级目录 cd - 返回上一次所在的目录 cd ~ 进入当前用户的家目录 cd /usr 进入指定目录&#xff0c;绝对路径 创建目录 mkdir 目录名 在usr目录下创建dd目录 进入目录 cd 目录名 进入us…

TypeScript 学习笔记

最近在学 ts 顺便记录一下自己的学习进度&#xff0c;以及一些知识点的记录&#xff0c;可能不会太详细&#xff0c;主要是用来巩固和复习的&#xff0c;会持续更新 前言 想法 首先我自己想说一下自己在学ts之前&#xff0c;对ts的一个想法和印象&#xff0c;在我学习之前&a…

性能测试知多少?怎样开展性能测试

看到好多新手&#xff0c;在性能需求模糊的情况下&#xff0c;随便找一个性能测试工具&#xff0c;然后就开始进行性能测试了&#xff0c;在这种情况下得到的性能测试结果很难体现系统真实的能力&#xff0c;或者可能与系统真实的性能相距甚远。 与功能测试相比&#xff0c;性能…

适合打游戏用的蓝牙耳机有哪些?吃鸡无延迟的蓝牙耳机推荐

现在手游的兴起&#xff0c;让游戏市场变得更加火爆&#xff0c;各种可以提高玩家体验的外设也越来越多&#xff0c;除了提升操作的外置按键与手柄外&#xff0c;能带来更出色音质与舒心使用的游戏耳机&#xff0c;整体氛围感更好&#xff0c;让玩家在细节上占据优势&#xff0…

2023年2月中国数据库排行榜:OTO新格局持续三月,人大金仓、AnalyticDB排名创新高

玉兔迎春至&#xff0c;榜单焕新颜。 2023年2月&#xff0c;兔年开年的 墨天轮中国数据库流行度排行 火热出炉&#xff0c;本月共有259个数据库参与排名&#xff0c;排行榜样式去掉了较上月和半年前得分差的数据显示&#xff0c;更加聚焦各产品之间的排名变化以及当月的得分差距…

景区展馆客流量数据如何统计

客流统计系统是了解掌握景区客流量的小助手&#xff0c;只有知道每天景区来往客流的情况&#xff0c;才能对经营管理、员工分配等多方面要素进行合理布局安排。使用客流统计系统&#xff0c;利用大数据管理预测&#xff0c;以客流数据为基础&#xff0c;提高景区对突发事件的应…

【云原生】kubeadm部署k8s集群、Dashboard UI以及连接私有仓库

一、kubeadm 部署 K8S 集群架构 主机名IP地址安装组件master&#xff08;2C/4G&#xff0c;cpu核心数要求大于2&#xff09;192.168.2.66docker、kubeadm、kubelet、kubectl、flannelnode01&#xff08;2C/2G&#xff09;192.168.2.200docker、kubeadm、kubelet、kubectl、fla…

基本bash shell(浏览、管理、查看文件/目录)

基本bash shell使用shell启动shell$cat /etc/passwd文件存储用户账号列表和基本配置信息shell提示符默认&#xff1a;$表示&#xff1a;shell在等待用户输入bash手册man命令$man man表示&#xff1a;查看手册页内容$man -K terminal关键字搜索。表示&#xff1a;查找与终端相关…

弄懂 Websocket 你得知道的这 3 点

1. WebSocket原理 WebSocket同HTTP一样也是应用层的协议&#xff0c;但是它是一种双向通信协议&#xff0c;是建立在TCP之上的。 WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket API也被W3C定为标准。 WebSocket使得客户端和服务器之间的数据交换变得更加简…

扬帆优配|联通港股创近两年新高!A股资源类股爆发,食品饮料领跌

今日上午&#xff0c;A股商场和港股商场均现较大起伏震动&#xff0c;临近上午收盘出现一波跳水&#xff0c;不过&#xff0c;到上午收盘&#xff0c;上证指数仍微涨0.10%&#xff0c;煤炭等资源类板块明显上涨。 港股商场上午走弱&#xff0c;科技股领跌。 沪指微涨0.10%资源…

进程间通信(二)/共享内存

⭐前言&#xff1a;在前面的博文中分析了什么的进程间通信和进程间通信的方式之一&#xff1a;管道&#xff08;匿名管道和命名管道&#xff09;。接下来分析第二种方式&#xff1a;共享内存。 要实现进程间通信&#xff0c;其前提是让不同进程之间看到同一份资源。所谓共享内存…

虹科直播 | 2月25日相约虹科Pico学院,教您CAN总线测试与波形诊断分析

​​2月25日 直播预告 网络总线的测试与波形诊断分析 2月18日 直播回顾 2月18日&#xff0c;虹科Pico汽车示波器学院成功开展第六课的直播课程。戈华飞老师向各位学员讲解了车身天线信号&#xff08;无钥匙进入系统&#xff09;、倒车雷达信号、车内超声波监控信号测试与波形…

操作系统真相还原_第6章:完善内核

文章目录6.1 函数调用约定简介6.2 汇编语言和C语言混合编程汇编调用CC调用汇编6.3 实现打印函数流程程序编译并写入硬盘执行6.4 内联汇编简介汇编语言AT&T语法基本内联汇编扩展内联汇编6.1 函数调用约定简介 调用约定&#xff1a; calling conventions 调用函数时的一套约…

OpenCV基础(28)使用OpenCV进行摄像机标定Python和C++

摄像头是机器人、监控、太空探索、社交媒体、工业自动化甚至娱乐业等多个领域不可或缺的一部分。 对于许多应用&#xff0c;必须了解相机的参数才能有效地将其用作视觉传感器。 在这篇文章中&#xff0c;您将了解相机校准所涉及的步骤及其意义。 我们还共享 C 和 Python 代码以…

2023面试准备之--mysql

文章目录mysql存储引擎索引聚簇索引和非聚簇索引事务锁MVCC机制&#xff08;类似于copy on write&#xff09;主从复制为什么要主从同步&#xff1f;怎么处理mysql的慢查询&#xff1f;mysql clint ---->server ----> 存储引擎 存储引擎 Innodb 是MySQL5.5版本及之后默…

1.3 Spring Boot 框架集成Knife4j(官网)

1.3 Spring Boot 框架集成Knife4j&#xff08;官网&#xff09; 本次示例使用Spring Boot作为脚手架来快速集成Knife4j,Spring Boot版本2.3.5.RELEASE,Knife4j版本2.0.7&#xff0c;完整代码可以去参考knife4j-spring-boot-fast-demo pom.xml完整文件代码如下 <?xml ver…

性能测试的二八原则

性能测试二八原则&#xff0c;响应时间2/5/8原则 所谓响应时间的“2-5-8原则”&#xff0c;简单说&#xff0c;就是 当用户能够在2秒以内得到响应时&#xff0c;会感觉系统的响应很快; 当用户在2-5秒之间得到响应时&#xff0c;会感觉系统的响应速度还可以; 当用户在5-8秒以内得…

电路模型和电路定律——“电路分析”

各位CSDN的uu们你们好呀&#xff0c;今天小雅兰的内容是我这学期的专业课噢&#xff0c;首先就学习了电路模型和电路定律&#xff0c;包括电路和电路模型、电流和电压的参考方向、电功率和能量、电路元件、电阻元件、电压源和电流源、基尔霍夫定律。那么现在&#xff0c;就让我…

FastReport .NET 2023.1.13 Crack

FastReport .NET 使用来自 ADO .NET 数据源的数据。它可以排序和过滤数据行&#xff0c;使用主从关系和查找数据列。一切都可以通过点击几下鼠标来完成。 直接连接到 ADO、MS SQL 或基于 xml 的数据库。其他连接器可作为插件使用。 能够从 IEnumerable 类型的业务对象中获取数…

hadoop入门介绍及各组件功能运行关系

文章目录Hadoop 组成部分1.HDFS2.MapReduce 架构概述3. yarn 架构概述4.HDFS、YARN、MapReduce三者关系Hadoop 组成部分 1.HDFS Hadoop Distributed File System&#xff0c;简称 HDFS&#xff0c;是一个分布式文件系统。 HDFS 架构概述 主要分为 NameNode (mn):存储文件的元…