static,const,volatile,extern,register关键字深入解析

一.最快的关键词-register

1.可执行的文件执行的本质是什么

我们都知道一个源文件要生成我们计算机课执行的文件要经过:
源文件(test.c)—>预编译—>编译—>链接—>可执行文件(test.exe)

对这个过程的详细描述请看 《预处理指令》

不管是源文件还是生成的可执行文件它们都是文件,而且都放在硬盘中!!

可执行的文件其实存放的是计算机可执行的指令(二进制代码),当我们去执行这个文件的时候屏幕会打印出执行后的结果,但大家有没有想过我们执行的这个文件的过程经历了什么。

运行可执行文件的本质是将数据(二进制指令)加载到内存,然后让CPU再去内存中读取数据(暂存在cpu中的寄存器或高速缓存中),经过CPU的计算好的数据又重新加回内存,然后从内存把结果打印到屏幕上
在这里插入图片描述

但这里有个问题,为啥CPU要经过一个内存读取数据,直接向硬盘读取数据不更好嘛,根本原因就是内存读写数据的速度比硬盘快的多.

这里就能理解定义变量为什么是在内存开辟空间:当程序执行起来的时候数据(二进制代码)已经加载到内存,包括定义变量的二进制指令,此时存储变量的空间只能是在内存中开辟。

2.最快的关键词register

CPU主要是负责进行计算的硬件单元,但是为了方便运算,一般第一步需要先把数据从硬盘写入内存,再提前将数据从内存读取到CPU内,那么也就需要CPU具有一定的数据临时存储能力,所以集成了一组叫做寄存器的硬件,用来做临时数据的保存,然后CPU一条一条指令执行运算,执行完后结果会写回内存。
在这里插入图片描述
既然寄存器的速度很快那我们能不能将变量直接存储在寄存器中呢,答案是可以的!!

register修饰变量

尽量将所修饰变量,会将变量一步到位放入CPU寄存器中,从而达到提高CPU效率的目的。

这里为什么是尽量呢,虽然我们用register修饰了变量,但编译器会判断这个变量值不值得放入寄存器,和能不能放入寄存器,所以编译器不一定会将该变量放入寄存器。

这样就引申出什么样的变量适合放入寄存器

  • 局部变量,全局变量最好不要,(全局变量生命周期是整个程序运行期间,所以会导致CPU寄存器被长时间占用)

  • 不会被写入(改变变量的值)的变量(因为一旦写入,运行结果就需要写回内存,后续还要读取检测的话,register修饰就没有意义了)

  • 变量会被高频被读取

  • register不能大量使用,因为寄存器数量有限

被register修饰的变量不能被取地址,原因就是地址是内存的中的概

在这里插入图片描述

二.声明关键字-extern

1.变量的生命周期与作用域

1.变量的定义与声明

变量的定义:需要在内存中开辟空间要存储数据,同时也可以初始化变量的值,但一个变量的定义只能有一次

声明:不管是函数的声明还是变量的声明,声明只是一个告知并不会开辟内存,告知编译器我们已经定义了可以使用,而且一个函数或变量的声明可以多次。

2.变量的生命周期与作用域

生命周期:指的是该变量从定义(开辟内存保存数据)到被释放存储空间的时间段记住它是一个时间概念。

作用域:指的是该变量能够被使用的范围,只有在作用域范围之内变量名才可以被使用。

3.全局变量与局部变量

  • 全局变量

定义:在函数外定义的变量叫做全局变量,存储在全局数据区,具有全局性

生命周期:定义完成后程序运行的整个生命周期内一直都有效。
作用域:整个工程

  • 局部变量

定义:包含在代码块( { } )中的变量叫做局部变量,存储栈区,局部变量具有临时性。

生命周期:从进入代码块形成局部变量开辟空间,退出代码块释放空间而经历这个过程的时间叫做局部变量的生命周期
作用域:代码块内,代码块:用{ }括起来的区域,就叫作代码块。

举例:
局部变量的出了作用域,变量将不能正常被访问
在这里插入图片描述
全局变量的作用域是整个工程
在这里插入图片描述
当全局变量与局部变量命名冲突时优先使用局部变量
在这里插入图片描述

2.STM32工程中头文件的作用

在一个工程中应该有多个外设源文件,以及一个包含一个main函数的源文件,各位外设源文件实现的功能不一样(功能函数),肯定避免不了各个外设源文件相互调用函数情况,此时我们只需要每个源文件对应一个外设头文件。
而头文件包含:
1.其他外设头文件和系统头文件
2.所有全局变量的声明
3.所有的函数声明
4.#define,结构体声明,枚举声明,typedef类型等等
当其他源文件想调用该外设源文件的内容时只要包含这个外设源文件对应头文件就OK了
头文件的作用:组织项目结构的时候,减少大型项目的维护成本

外设相关文件
stm32f10x.h这个文件是非常牛逼的啦
stm32f10x.h:实现了内核(CPU)之外的所有外设的寄存器映射,以及一些外设库函数的参数
stm32f10x_xx.c: 外设的驱动函数库文件
xx: GPIO、USRAT、I2C、SPI、FSMC…

这里我截取的一了部分
在这里插入图片描述

stm32f10x_xx.h: 存放外设的初始化结构体,外设初始化结构体成员的参数列表,外设固件库函数的声明
在这里插入图片描述

3.声明关键字extern

1.extern修饰全局变量
在这里插入图片描述
为了体现头文件的作用我把声明统统放到test.h这个头文件中
在这里插入图片描述
将声明放入头文件中,只要包含这个源文件的头文件就可以使用这些声明,根本原因是在预编译过程会将源文件包含的头文件的内容全部包含到该源文件中,详情请参考—》预编译指令

注意:
声明并不会开辟内存,只是一个告知作用,不能用声明来初始化全局变量的值。
在这里插入图片描述

2.extern声明函数
在这里插入图片描述

注意:声明函数前面的extern可以省略(不会报错),声明全局变量的前面extern不能省略。

在这里插入图片描述
虽然声明函数可以省略extern关键字,但建议不要省略,使代码更加严谨。

前面test.h头文件中忘记加条件编译防止头文件被重复包涵这里补上:具体原理请看–>预编译指令
**
在这里插入图片描述

三.static关键字三个作用(重点)

1.static修饰全局变量

static修饰全局变量:该变量的只在本文件内被访问,不能被外部其他直接访问。

原理:static修饰全局变量改变的是全局变量的作用域,作用域从整个工程变为本文件内,全局变量的生命周期并没有变。

在这里插入图片描述
虽然不能直接访问但是可以通过函数调用的方式进行间接访问
在这里插入图片描述

2.static修饰函数

static修饰函数,该函数只能在本文内被访问,不能在外部其他文件直接访问。

原理:static修饰函数也是改变的函数的作用域,作用域从整个工程变为本文件内。
在这里插入图片描述
static修饰函数,当然函数也可以间接被访问,不就在嵌套一个函数嘛。

在这里插入图片描述

3.static修饰局部变量

static修饰局部变量,则该局部变量存储在静态数据区,更改局部变量的生命周期从局部变量生命周期改成全局生命周期也就是说程序运行的整个生命周期内一直都有效,但作用域仍然不变还是一个代码块内

先看一段代码,局部变量不加static关键字修饰:
在这里插入图片描述
局部变量加static关键字修饰:

在这里插入图片描述
static修饰局部变量,局部变量的作用域仍然不变还是在一个代码块内

在这里插入图片描述
出了代码块,变量并不能被正常访问,static修饰局部变量你只能说局部变量活的更久了,就是没有被释放该变量的内存,一定要细细品味!!

为什么static修饰局部变量生命周期具有全局性:
在这里插入图片描述

根本原因是存储的位置发生了变化,被static修饰局部变量被存放在全局数据区。

还有一个问题局部变量为啥具有临时性,这里简单提一下叭
在这里插入图片描述
这里简单提一下关于函数栈帧还有很多细节,这里不是我们的重点,到后面会专门出一篇关于函数栈帧的详解,系统阐述具体原理。

四.只读关键字-const(重点)

1.const修饰变量

1.const修饰变量:变量不可以直接被修改

在这里插入图片描述

注意:
这里const并不会修饰关键字int,所以int的位置并不影响结果。
所以const int a 与int const a 是等价的 const修饰的都是变量a但习惯是第一种。

虽然不能直接修改,但可以通过指针操作间接修改变量的值:

在这里插入图片描述
对与用const修饰的变量,如果真正想改还是可以做到的,那问题来了const的意义是什么呢

如果我们用const去修饰一个变量肯定希望变量不应该被修改
1.让编译器进行直接修改式检查 ,当直接修改const修饰的变量时编译器报错提醒。
2. 告诉其他程序员(正在改你代码或者阅读你代码的)这个变量后面尽量不要改,改了可能会出问题。

const定义的常量不是标准意义上的常量,作为数组的元素个数
在这里插入图片描述
字符串真正意义不能被修改
在这里插入图片描述

2.const修饰数组

2.const修饰数组:数组的每个元素不能被直接修改
在这里插入图片描述
利用指针间接修改数组元素
在这里插入图片描述

3.const修饰指针

const关键字不能修饰关键字(int ),所以直接忽略int的位置,然后往const的右边看离谁近就修饰谁,这里只能修饰 * 操作符 或 指针 p。

  • 1.const int *p; //const修饰 * 操作符,p指针可变,p指向的对象(*p)不可变。

  • 2.int const *p; //const修饰 * 操作符,p指针可变,p指向的对象(*p)不可变。与上式一样

在这里插入图片描述

  • 3.int * const p; //const修饰 p 指针,p指针不可变,p指向的对象(*p)可变。
    在这里插入图片描述

  • 4.const int * const p; //前一个const修饰 *操作符,后一个const修饰 p 指针,指针p与指针指向的对象*p都不能变。
    在这里插入图片描述

4.const修饰函数参数

这里只是为了代码更加严谨,因为这个函数只是一个打印的函数,所以我们加一个const关键字修饰 参数的 *操作符使函数内并不能修饰我们的a变量的值。
在这里插入图片描述
在这里插入图片描述

5.const修饰函数的返回值

一般用来修饰返回的指针。
在这里插入图片描述

正确的方式应该接收的指针前面也加const修饰

在这里插入图片描述

五.最易变的关键字-volatile(重点)

1.理解volatile关键字

volatile是易变的,不稳定的意思。

要用volatile关键字修饰的变量,表示该变量可能被操作系统,硬件(例如单片机的外设),或者其他线程等更改,如果我们用volatile关键字去修饰这个变量,则编译器对访问该变量的的代码就不在进行优化(我自身对优化的理解:编译器可能会认为我们自己写的代码不会修改这个变量,则把该变量直接拿到CPU中,而之后CPU不会再去内存读取该变量,但殊不知虽然我们写的代码不会修改该变量,但硬件可能会对该变量进行修改,而修改之后的值是存放在内存之中,而CPU此时并不会去内存读取该变量的值,最后导致变量的值不能及时更新),可以提定对特殊地址的稳定访问。

接下来在linux环境下做个实验:

不加volatile:

在这里插入图片描述

在这里插入图片描述
总结:不加volatile关键字修饰pass这个变量则编译器(会认为pass这个变量不会被修改)可能会进行优化,之后CPU不会再从内存中读取pass变量的值,那么问题来了如果pass的值此时被操作系统,或者硬件将pass的值改成0,按理说CPU应该退出循环,但此时CPU并不会去内存读取pass的值(0),则CPU并不会退出循环那就出现了问题!!

加加volatile:

在这里插入图片描述
总结:加上volatile修饰变量,此时会忽略编译器的优化,也就是说CPU会一直向内存读取的pass的值再进行判定是否执行循环,此时就算pass的值被硬件改成0,则CPU也可以读取到然后终止循环

2.volatile关键字在STM32中的运用

你仔细观察你会发现基本上SMT32中定义的所有寄存器都前面都加上了
volatile关键字来进行修饰。

在这里插入图片描述

每个结构体成员前增加了一个“__IO”前缀,它的原型是volatile关键字,这些结构体内的成员,都代表着寄存器,而寄存器(在内存当中)很多时候是由硬件(外设或 STM32 芯片状态)修改的,也就是说即使 CPU 不执行代码修改这些变量,变量的值也有可能被外设修改、更新,所以每次使用这些变量的时候,我们都要求CPU 去该变量的地址重新读取这些寄存器。 若没有这个关键字修饰,在某些情况下,编译器认为没有代码修改该变量,就直接从 CPU 的某个缓存(CPU内部的寄存器或高速缓存)获取该变量值,这时虽然可以加快执行速度,但该缓存中的是陈旧数据,与我们要求的寄存器最新状态可能会有出入。

总结:volatile关键字叫编译器不要进行优化,也就是说让CPU每次都老老实实去内存中读取的寄存器中的值,慢点就慢点但是可以保证CPU读取的寄存器的值都是最新的!!!

结尾

上面加了重点二字面试极有可能考到,基本上是C语言中最重要的几个关键字,运用比较广泛,我们必须理解并掌握它,如果觉得本文对有有小小的帮助就快快点赞收藏叭!!!!

猜你喜欢

转载自blog.csdn.net/k666499436/article/details/124270545