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

🎉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文件,这个文件可以执行,能看到我们程序实际跑出的结果是什么。
  • 37
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 36
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 36
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

封心锁爱的前夫哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值