关键字深度剖析,集齐所有关键字可召唤神龙?【三】

今天继续上一次的关键字,学习《C语言深度剖析》,由于深度剖析,简单的东西就不赘述,继续集齐剩下的龙珠吧!

img

今日“龙珠”

⚡️汇编角度理解return的含义

⚡️const 的各种应用场景

⚡️volatile 的基本理解与实验证明

1. return关键字

1.0 before return

为什么计算机当中,我们拷贝一部电影的时候要花5分钟,但是删除却只要花两到三秒?
计算机中,释放空间真的要将我们的数据清空吗?

其实计算机中的清空数据,只要设置该数据无效即可

其实我们可以设置一个代码块来表示这个数据是否有效,所以只要我们改变这个代码块的显示认为这块区域已经不需要了就可以完成删除任务,就可以了,而不是全部清空

你把东西放到回收站,相当于移动垃圾文件到一起,所以删除大文件的时候这一步很慢。
你点了清空回收站之后,磁盘直接把回收站范围内存储的内容标记为已删除的空闲部分供系统使用,因为只是标记一下,所以这一步非常快。直到这一步,删除的文件还可以通过一些手段恢复。
等你再写入文件的时候,如果要用到这片区域,文件就会直接覆盖。可能就是你所谓的物理删除,因为这一步真正把磁盘相应区域的磁粒翻转了。到这一步,删除的文件就彻底没有了。
而还有一种就是类似于360强力删除的东西,它是直接把文件覆盖成1111…或者000…,有时还会反复覆盖,最后再把这片区域标记为已删除的空闲区域供系统使用。

1.1 熟悉的问题,函数调用开辟栈帧

💥在这一段代码中,函数调用的栈帧是如何开辟和销毁的呢?

#include <stdio.h>
#include <windows.h>
char* show()
{
	char str[] = "hello bit";
	return str;
}
int main()
{
	char* s = show();
	printf("%s\n", s);
	return 0;
}

如下的示意图显示了栈区的位置

在栈区中,首先系统为main函数分配了一个栈帧
image-20211219204712292

然后当我们要去调用show函数的时候,会自顶向下的开辟空间空间,以及初始化show函数中的字符串数组的空间

image-20211219205409584

💥我们也都知道,当我们使用玩这个show函数之后,将会销毁这个栈帧,那么结合之前的观点,请问这段空间是否会被清空呢?

答案是不是清空,并不会修改这里指向的内容,栈帧结构虽然被释放了,但是里面的数据并没有被释放

不相信?你看👇

image-20211219205731224

调完函数之后,s指向的值依然还在😲

但是再往下走经过printf之后却没了,这是为什么?

image-20211219210539000

因为在调用完这个show之后函数栈帧结构被销毁了,但是数据没有清空,但是printf也是个函数啊,调用printf函数之后在原来的位置开辟栈帧,把原来的位置覆盖了,所以走完printf之后显示指向的数据是乱码

💥编译器怎么知道要申请多少大小是和合理的栈帧空间呢?

函数即使没有调用,但是函数在调用的时候,编译器其实就是会预估空间,所需要的空间的大小,来自动生成一个合理的栈帧空间

1.2 返回值临时变量接收的本质

return 用来终止一个函数并返回其后面跟着的值。
return (Val);//此括号可以省略。但一般不省略,尤其在返回一个表达式的值时。

🎃函数是怎么通过return返回给函数的呢?

int GetData()
{
	int x = 0x11223344;
	printf("run get data!\n");
	return x;
}
int main()
{
	int y = GetData();
	printf("ret : %x\n", y);
	return 0;
}

看看return的过程

反汇编

/

1.3 return 注意事项

正是由上面的要点所以我们知道

	char * Func(void)
	{
		char str[30];return str;
	}

str 属于局部变量,位于栈内存中,在Func 结束的时候被释放,所以返回str 将导致错误。

👻所以return 语句不可返回指向“栈内存”的“指针”,因为该内存在函数体结束时被自动销毁。

注: 后面讲const的时候会讲到函数的返回值具有常量属性

2. const 关键字也许该被替换为readonly

2.0 before const

const 是constant 的缩写,是恒定不变的意思,也翻译为常量、常数等
image-20211121220154632
这个关键字我们之前有提到过

如何写出一段好的代码(以实现strlen为例)

2.1 一些注意事项

🎄const修饰变量,意义何在?

  1. 让编译器进行直接修改式检查
  2. 告诉其他程序员(正在改你代码或者阅读你代码的)这个变量后面不要改哦。也属于一种“自描述”含义

🎄const修饰的变量,可以作为数组定义的一部分吗?

不能

本身VS编译器是编不过的,但是Linux是可以编译过的

image-20211219220301181

2.2 const应用场景

2.2.1 const修饰一般变量

这个比较简单const修饰的变量之后都被叫做常变量

const 可以用在类型说明符前,也可以用在类型说明符后。例如:
int const i=2; 或const int i=2;

2.2.2 const修饰数组–>只读数组

image-20211219220731311

2.2.3 修饰指针

const可以修饰指针

这里我们务必要补充一下指针的知识

2.2.3.0 指针补充知识

在C中,任何变量都是从最低地址开始的
a是一个int类型有4个字节,那&a就是指向地址最低的那一个

2.2.3.1 理解修饰指针变量

修饰变量的时候变量变成了常变量,不能让你改变值,但是可以通过指针改变

	const int n = 10;
	int* p = &n;

那么怎么不让被指针修改呢?

const 放在*的左边
const修饰的指针指向的内容,表示指针指向的内容不能通过指针来改变
但是指针变量本身是可以改变的

	const int*  p = &n;
	*p = 20;
//err

const 放在*的右边
const修饰的是指针变量本身,指针变量的内容不能被修改
但是指针指向的内容是可以通过指针来改变的

	int* const p = &n;
	p = &m;
//err

那么来总结一下记忆方法

先忽略类型名(编译器解析的时候也是忽略类型名),我们看const 离哪个近。“近水楼台先得月”,离谁近就修饰谁。

int main()
{
	int a = 0;
		const int *p = &a; //p指向的变量不可以直接被修改,也就是无法被解引用修改
	int const *p = &a; //p指向的变量不可以直接被修改,同上
	int * const p = &a; //p的内容不可直接被修改,p指向不能改,不可以写p=100
	const int *const p = &a;//都不可以改
	return 0;
}

2.2.4 修饰函数参数

const 修饰符也可以修饰函数的参数,当不希望这个参数值被函数体内意外改变时使用。例如:

void Fun(const int i);

告诉编译器i 在函数体中的不能改变,从而防止了使用者的一些无意的或错误的修改。

2.2.5 修饰函数返回值

一般不建议这样写,因为这样还是能改这个值,而且编译器也报了警告

const int* GetVal()
{
	static int a = 10;
	return &a;
}

int main()
{
	int* p = GetVal();
	//const int* p = GetVal();
	return 0;
}

建议这样写,后续使用也不能修改

const int* GetVal()
{
	static int a = 10;
	return &a;
}

int main()
{
	//int* p = GetVal();
	const int* p = GetVal();
	return 0;
}

3. 最易变的关键字 - volatile

3.1 书中的话理解volatile

volatile 是易变的、不稳定的意思。很多人根本就没见过这个关键字,不知道它的存在。也有很多程序员知道它的存在,但从来没用过它。我对它有种“杨家有女初长成,养在深闺人未识” 的感觉。
volatile 关键字和const 一样是一种类型修饰符,用它修饰的变量表示可以被某些编译器未知的因素更改,比如操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问

3.2 使用场景

我们这里有这样一个场景,左边是CPU,右边是内存,现在我们执行中间的语句while循环,但是我们发现,由于flag为1,所以程序会执行很多次,在此之前,CPU都要从内存中拿数据,当拿多了之后发现一直是一样的,于是就直接重复执行CPU的数据而不去访问内存了,但是如果我们此时又有一个平行语句去修改flag的值为0,我们希望他停止循环,但是又因为CPU不去访问内存了也就是不知道flag为0了,所i一还会一直死循环下去,这种现象就是内存被覆盖的情况,所以我们就不要CPU去优化,就会用到我们的关键字

为了直观显示这个效果,我们利用Linux系统

3.2.1 未加volatile

先在vim中写下这个

 int pass = 1; //加上volatile
int main()
{
	while (pass) {
	}
	return 0;
}

在命令行下输入

gcc test.c -O2 -g //以O2级别进行代码优化
$ objdump -S -d a.out > aa.s //对形成的a.out可执行程序进行优化
$ vim aa.s //查看汇编代码

image-20211221223618351

3.2.2 加volatile

volatile int pass = 1; //加上volatile
int main()
{
	while (pass) {
	}
	return 0;
}

image-20211221223151768

每次访问都会有内存级的访问

小结:

volatile 忽略编译器的优化,保持内存可见性。

3.3 const和volatile

const volatile int i=10;这行代码有没有问题?

又是易变的又是可变的,矛盾吗

不矛盾

const要求你不要进行写入就可以。volatile意思是你读取的时候,每次都要从内存读。
两者并不冲突。
虽然volatile就叫做易变关键字,但这里仅仅是描述它修饰的变量可能会变化,要编译器注意,并不是它要求对应变量必须变化

4.最会帽子的关键字 - extern

4.1 了解extern

extern 可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,下面的代码用到的这些变量或函数是外来的,不是本文件定义的,提示编译器遇到此变量和函数时在其他模块中寻找其定义。就好比在本文件中给这些外来的变量或函数带了顶帽子,告诉本文件中所有代码,这些家伙不是土著。

4.2 使用extern

下面给出一个使用实例

下面分为三个文件,两个源文件,一个头文件

main.c

#include "test.h"

int main()
{
	printf("%d\n", x);
	show();
	return 0;
}

test.c

#include "test.h"

int x = 1234;

void show()
{
	printf("hello show()\n");
}

test.h

#pragma once
#include<stdio.h>

extern int x;
extern void show();

extern 的使用可以使得main函数可以在头文件中发现改属性或者方法被声明,然后可以去test.c寻找定义

5. struct 关键字

struct 是个神奇的关键字,它将一些相关联的数据打包成一个整体,方便使用。

在网络协议、通信控制、嵌入式系统、驱动开发等地方,我们经常要传送的不是简单的字节流(char 型数组),而是多种数据组合起来的一个整体,其表现形式是一个结构体。

平时我们要求函数的参数尽量不多于4 个,如果函数的参数多于4 个使用起来非常容易出错(包括每个参数的意义和顺序都容易弄错),效率也会降低。这个时候,可以用结构体压缩参数个数。

5.0 谈过多遍的struct关键字

对于结构体struct类型之前介绍过很多次,定义结构体,本质是制作类型

欢迎翻阅我之前的博客请回答c语言-结构体【入门】

补充注意点:

  1. 当我们去赋值变量的时候

虽然int类型可以这样声明

	x.age = 18;

类型是char[]是一个字符串数组往往不可以这样写

x.name = "张三";//err

要定义的话得这样定义

strcpy(x.name, "张三");
  1. 为什么结构体访问有两种方式?
    C语言是面向过程的语言,即有大量函数,有函数就要传参,又在我们传参的时候可能会传结构体,但是结构体非常大,我们传值过去临时拷贝就大,效率就低,所以就建议传指针。简单说,结构体在定义的时候,用 . 方便些,在传参的时候 -> 方便些。

然而struct远没那么简单

5.1 空结构体的大小

在VS中空结构体是不允许定义的

在Linux系统中是0,也就是gcc中为0,且能定义变量,但是不能使用

5.2 柔性数组

也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员,但结
构中的柔性数组成员前面必须至少一个其他成员。柔性数组成员允许结构中包含一个大小可变的数组。sizeof 返回的这种结构大小不包括柔性数组的内存。包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

柔性数组一定要放在结构体内

下面是柔性数组

struct data{
	int num;
	//....
	int arr[0];
};
int main()
{
	struct data* p = malloc(sizeof(struct data) + sizeof(int) * 10);
	p->num = 10;
	for (int i = 0; i < p->num; i++) {
		p->arr[i] = i;
	}
	free(p);
	return 0;

5.2.1 柔性数组的地址说明

由下图可以看出最上面的地址最低,也就是地址由低到高

image-20211222225009093

5.3 struct 与class 的区别

在C++里struct 关键字与class 关键字一般可以通用,只有一个很小的区别。struct 的成
员默认情况下属性是public 的,而class 成员却是private 的。很多人觉得不好记,其实很容易。你平时用结构体时用public 修饰它的成员了吗?既然struct 关键字与class 关键字可以通用,你也不要认为结构体内不能放函数了。

好了今天的内容就到这里了哈!!!

今天的“龙珠”集齐了5颗

下次继续,请持续关注

干净又卫生,别忘了一键三连

  • 12
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

言之命至9012

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

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

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

打赏作者

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

抵扣说明:

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

余额充值