C语言关键字(7)
一、汇编角度理解return 的含义
return大家都太熟悉不过了,我们天天写,天天见,今天就带大家更深的理解一下return
这份代码很明显打印的是随机值,因为str在栈上开辟,随函数栈帧销毁,*s这时候就是一个随机值
但这只是最肤浅的理解。
深度理解:
局部变量为什么具有临时性?
question1:c语言有字符串类型吗?答案是c只有字符串,没有字符串类型。java有string,c++有string容器。
question2:计算机中删除文件具体的过程是什么?它究竟删了吗?这里我们需要知道,删除的过程并不是把我们的数据全部清零。我们可以这么想,如果全部清零,那我们需要给内存中写入和程序相同大小的0或者1,那么安装和删除的时间应该是一样的。显然不是这样,我们一瞬间就删除了一个程序。那么答案其实是:计算机中要清空数据,只需要设置该数据无效即可
我们实验一下:
当我们释放栈帧,里面的数据并不会被清空。
然而当我们跑完printf,它就变成了无效的代码了。这又是为什么呢?
因为printf也是一个函数,当我们执行printf时,也要开辟新的栈帧空间,并会把之前的栈帧进行覆盖。printf凭什么可以覆盖show函数的栈帧呢?就是因为函数栈帧销毁时,把那一个空间标记成无效空间。
那么这里还有一个问题,我们是如何知道函数开辟的空间的大小是否够用呢?
其实虽然我们的函数还没有调用就已经开辟了空间,其实在开辟空间的时候,编译器会进行核对关键字,预估所需要的空间。所以我们C语言内置32个关键字就是用来给编译器开空间用的。
那我们现在来回答一下问题:为什么临时变量具有临时性?
C语言是一个面向过程的语言,程序中会充满大量的函数,函数内的变量基本上都是属于临时变量,都在函数的栈帧内开辟。栈帧结束销毁,我们就要把临时变量标记为无效,允许被覆盖。
我们继续来谈谈return!
先看看这份代码
首先x是一个临时变量,而当我们用y接收这个临时变量时。为什么可以接收的到呢?函数结束栈帧销毁,临时变量怎么可以再来赋值给y。
y是如何读取到临时变量的值呢?
x虽然为临时变量,但是我们返回后拿到的并不是真正的x,而是x的内容。并且栈帧销毁不是把数据清空,而是标记可覆盖。
我们来看看反汇编就能理解这句话的意义了。
我们可以看到,return x 真正执行的代码是,把x 的值存在eax这个通用寄存器中。
所以我们就可以给出结论:函数的返回值,通过寄存器的方式,返回给函数调用方。
那我们如果不接收这个返回值,不要y变量。会发生什么呢?
我们看到,还是把x 的值放在eax中。不论你接不接收,return 返回值的方式都是一样的。
而且函数的返回值具有常量属性。
结论:
函数的默认返回值具有临时性,且通过寄存器的方式进行返回。
这里有一个小问题:
main函数的返回值给了谁?为什么都要返回0呢?
二、 const的各种应用场景
const的各种小细节
C语言面试高频的关键字:static 、 const 、 sizeof 、 volatile
我们的8篇c深度剖析–关键字博客 (也是我自己的学习过程),总结理解了C语言32个关键字的细节问题。有兴趣的小伙伴可以看看。
const关键字:大部分是给编译器看的。
作用:
-
为变量赋上只读属性,让变量不可直接被修改
这里的直接是指可以修改,因为const并不是一个强限制的关键字,不是必须不能修改。
那我们来修改一下const 变量吧!
这里a是一个被const 修饰的变量,而我们用p指针接收a 的地址。这时候我们再去解引用 p,就可以找到a变量,并对其进行修改了。但是这里有一个小警告:
不同的const限定符,意味着我们&a 是一个 const int * 的变量,而 p是一个 int *的变量,这样属于把a的权限扩大了。但编译器仅仅是给了一个warning,并不会影响程序的编译链接。所以我们上文说,const不是一个强限制的关键字。
如果我们想要处理这个warning,只需要把a强转一下就好了。
-
const和修饰的位置是没有关系的。但是我们建议把const放在类型之前。这样看起来大家都认可且习惯。
总结:const的作用:
- const是给编译器看的。让编译器对const修饰的变量进行语法检查,有修改const变量的情况,编译器直接进行报错。让我们提前被编译器告知,不该修改的变量进行了修改。这样也能证明这是一份优质的代码。这一特性叫:错误被提前报告。不然后期发现了一个错误,debug好几个小时是最崩溃的。
- 给其他程序员看。const 使程序有了自身描述性。让别人懂你的代码。什么地方是不能修改的。
还有一个这样的代码:
我们没有办法对这个常量字符串进行解引用修改。程序会直接崩溃。
这里的报错是:发生了访问冲突,禁止写入
字符串是真正意义上的不可被修改。
这里不是c语言提供的保护,而是操作系统层面给我们提供的保护。
而const只是编译器级别的限制,也是在编译工程中的限制,并不是运行阶段的限制。并且限制级别很弱。
除了这个问题还有小的细节:
数组的中括号中,只能是常量,const修饰的并不叫真正的常量。
当然这也只是在vs环境中的结果。
如果我们搬运到gcc中看一下
linux下是可以编译通过的。这是因为以前提到了linux中的c不是标c 而是GNUC的概念。
GNUC是标准C的扩展。但是GNU标准使用的并不是很普遍。为了代码的跨平台性,我们还是推荐使用标C
。
还有细节还没完
const要在定义的时候初始化,不能进行二次赋值。这个从字面就能理解。
const 修饰数组
如果你想拥有一个只读数组,那么可以这样定义哦。
const修饰指针
指针和指针变量的区别
- 指针:为了提高查找和定位的速率,我们对每一个字节进行编址,其中这每一个字节都叫做指针。指针就是地址。它不需要开空间,且是通过硬件编址决定的?不然我们要寻找一个地址,要通过从头到尾遍历来寻找吗?
- 指针变量:指针也就是地址,它是一种数据。只要是数据,我们就需要去保存它。我们保存这个地址用4个字节或是8个字节。其中这4、8个字节的空间就叫做指针变量。用来保存地址。
很多地方会将指针和指针变量混谈,虽然他们不区分,但是我们在阅读的时候,需要内心清楚到底是什么。
当然这里还有左值和右值的概念。任何一个变量名,在不同的应用场景下,会有不同的含义。
其中代表x 的空间的,叫变量属性,也叫做左值。
其中代表x 的数据的,叫数据属性,也叫做右值。
我们需要知道,内存寻址是以字节为基本单位的。
那么这里我们定义两个变量
其中 a 和 p 这两个变量,分别存了一个10 (字面常量) &a (地址常量)
我们了解 a 有 4个字节,也就是有四个地址。我们在寻址的时候,就是找低字节的地址。
c语言中,任何变量取地址,都是从最低的地址开始。
我们再来详细谈一下解引用
解引用:变量的使用。当然还存在变量的定义 *p = 20 这就是变量的定义。其中我们拿到p指向变量的内容。
int b = * p;
我们先找到p的内容,就是a的地址,在进行解引用,拿到a的内容。然后把a 的内容赋值给b。
其中:p指的是a 的左值,第二个p就是a的右值。
tips:类型相同时,对指针进行解引用操作,就是指针所指的目标进行操作
我们正式进入正题 const修饰指针
1.
这里const修饰的是* 也就是p所指向的对象的内容,使之无法直接被改变。我们可以看到p是可以修改的。
在c语言中关键字是不能直接修饰关键字的,所以这里尽管const距离int最近,但是const修饰的还是*。
第二种和第一种是完全一样的。
第三种就正好和前两种相对,const修饰的是p,这里p指向对象的内容可以被修改,但是p不可以。a是可以被修改的。
两种都不能被修改
这种情况我们可以看出,我们把a 的地址传给p,并且把它用const修饰。所以这里的p就是一个const int * 类型的 变量。
而当我们把p赋值给q ,非const 的int * 变量。这样可以吗?
答案是可以的,但是和前文很相似,会有一个报错。
这是因为我们把一个const 类型的变量,赋值给非const类型,那么它的权限其实是被放大的,从只读变成了可读可写,如果我们把非const赋值给const,那么就不会有这个报错了,这也为什么说const对变量的修饰限定时不严格的。。为了避免这样的问题我们有两种解决方法:
- 把p强转成(int * )
- 把const放在 * 后面,进行对 p 的修饰,这样我们把const赋值的时候,其实并没有修改p。所以也不会报错了。相当于对内置类型的修饰。虽然被const修饰,但是可以给别人赋值。我并没有改变我自己。
const修饰函数
const修饰函数参数
看代码时间:
这份代码没问题吧!
传入指针变量p,用 int *接收。
show只是打印代码,并不会对变量进行修改,所以作为高素质程序员,我们直接给参数加上const修饰。告诉编译器也告诉程序员请勿修改。
这里的思想是预防性编程。如果有人在函数中对p进行解引用,修改变量的值,编译器直接给你报错。
当然,函数在传参的时候会形成临时变量。
那么我们调用show 传指针 会形成临时变量吗?
p就是一个变量,只不过他比较特殊,存的是地址,但依旧开辟在栈帧,所以一定要生成临时拷贝。
所以在C语言中,任何函数传参,都一定会生成临时变量,包括指针变量。无论你是传值还是传址。
光说不练,假把式。所以我们来做一个小证明:
我们发现两个p是完全不一样的地址。这样就完全可以证明指针传参其实是通过临时变量进行传参的。
const修饰返回值
如果我们这样写,我们会发现又会报一个warning
我们对这一现象进行解释:
static修饰变量,其生命周期变为全局,作用域依旧是函数作用域内。如果我们想要扩大它的作用域,可以通过返回指针来达到。这样我们返回了a的地址。他就可以在其他地方进行修改了。但是如果我仅仅想要读,不想被修改。这时候就要加上const 来修饰这个返回值了。当然了,又是这句话,const并不执行强约束。你如果想要修改也是可以的。只不过会报那个警报,当然我们完全有能力应付这个warning。原理同上。
当然了,我们还可以对接收返回值的变量进行const修饰。那么这样就无法被人修改了。
这个时候就不仅仅是一个警报的问题了。当你在解引用的时候,编译器会直接报错。无法通过编译,你无法修改这个变量。
如果p不加const 那就可以修改。只是有warning问题。
三、 volatile的基本理解与实验证明
volatile 它是什么意思:易变的,不稳定的。当然c语言命名的时候也是采用这个意思。
volatile 和 const一样,属于类型修饰符。
功能:使编译器不对特定的代码进行优化。
解释:
当我们在运行一个while循环的时候,正常的情况是,我们的循环变量flag会被加载到cpu 的寄存器中,cpu通过逻辑运算单元对其进行运算。然后再去判断是否执行while代码块。每循环一次都要从内存中读取一次。但是由于我们会写大量的单线程的代码,编译器在进行判断后,如果你的flag一直没有改变,那么编译器会对代码进行优化,使cpu不再从内存中读取flag,而是一直使用寄存器中的值。这个情况叫做 内存被覆盖。
然而这个时候问题就出现了,如果我们是多线程编程,我们flag在其他线程中被改变,这个时候,如果cpu仅读取寄存器种的数据,就会发生错误。这时候volatile就要发挥它的作用了,你不要优化了,就去内存中一遍一遍读吧,不要偷懒。
当然这是为了方便大家理解volatile的功能。真实的情况并不简单如此。如果遇到死循环,编译器直接在汇编代码直接跳转自己,根本就不会读数据。
针对这一段话进行解释:
我们实验主要用到这三个命令
我们可以直观的看到,我们不使用优化的选项和优化后的选项的文件大小是完全不同的。当然优化可以使文件变大,也可以使其变小。这里的a.out 使没有用volatile 进行修饰的。
我们打开我们的 a.s 汇编代码
我们发现在循环的时候,先把pass变量加载到eax寄存器中。然后test,eax自己和自己比较,这里使用的是按位与(&)或者逻辑与(&&)进行比较。jne 如果 这里不等于0,就转到40040这一行,也就是开始一直自己跳转自己。也就是我们上文解释的已经优化非volatile 的情况。
当然我们来看一下加上volatile的情况。
我们可以看到,这里就是进行回第一行代码,进行循环且不断往eax中写入pass的数据。
最后我们在点出一个小细节:
我们可以
volatile这个关键字并不常用,但是却很重要。不常用是因为我们不用c语言进行高级语言的开发。但是它一定是很重要的关键字。当然它的名字也会引起很大的误解,易变的,不稳定的和const的不可修改的很容易让人觉得。这两个功能是相反的。其实他们两个完全是两码事。
const :不要写入
volatile:读取时候,每次都从内存中进行读取。