C语言内存,数组,结构体
1. 程序运行为什么需要内存
- 程序在运行的过程中,会产生很多的数据,内存就是用来存放这些产生的数据
2. 内存的简单模型(逻辑)
- 内存在逻辑上可以类比为一栋大楼
- 整栋大楼的每一层都一模一样
- 每一层之间有许多房间,房间之间也都一模一样
3. 内存自身拥有的两种属性
3.1 内存空间
- 内存上面存在很多小的格子,每个小格子类似一个房间
- 每个小格子大小为一个字节,通过字节进行编码。
3.2 内存的地址
- 内存上面的每一个小格子,都有一个对应的地址编号,类似于房间的门牌号,每个地址空间都与一个对应的地址绑定
- 每个地址都是独一无二的
- 每个单独相邻的小格子之间的地址是连续的
4. 语言如何操作内存
4.1 从一个简单的变量定义开始
char a;/*编译器帮助我们在内存大楼中申请了一个房间,并且把房间的门牌号(内存的地址)和变量名 a 绑定在一起*/
a = 10;/*因为 a 和 内存的地址绑定了,所以编译器通过 a 可以找到申请的房间,找到了之后就把 10 扔进这个房间*/
a += 4;//编译器把 a 对应的房间中的数字拿出来,送到 CPU 中和 4 相加之后,再把运算结果送回 a 对应的房间
4.2 数据类型的本质
- 数据类型最本质的含义就是在使用内存时,判断需要用几个字节和什么样的翻译方法去翻译对应内存中的内容
4.2.1 使用多少个字节的问题
char a = 10;//编译器申请了一个字节的内存,然后把 10 放进去
int b = 10;//编译器申请了四个字节的内存,然后把 10 放进去
在以上的过程中,假设我们拥有一个内存的地址(0x1),如果我们给这个字节一个类型
- 这个内存地址本身只有一个字节,如果我们给他这个内存一个整数类型,那么,编译器会把 0x1,0x2,0x3,0x4
这四块内存空间当作一个整体,然后进行操作 - 如果给这个内存单元一个字符型,那么编译器就会把0x1这块内存空间当作一个整体
4.2.2 翻译方法的问题
int x = 10;
printf("%d\n",x);//把 x 对应的内存空间中的内容以整型的方式翻译
printf("%f\n",x);//把 x 对应的内存空间中的内容以float型的方式翻译
- 可以肯定的是,把整型的数据当作 float 型翻译肯定是不可以的,因为编码方式本身就不一样
使用整型的翻译方法翻译浮点型肯定不行 - 但是 x 对应的这块内存单元里面的数据是不变的,就像语文老师常常说的“一切景语皆情语”一样
同样的事物,不同的心境看到,想到的却不一样,但是事物的确是一样的,不变的.
4.3 函数名的本质
- 同样,在C语言中,函数名类似于变量名,编译器会寻找一块内存来存放整个函数的语句
而函数名就是这块地址的首地址,在调用函数的过程中,我们只需要找到被调用函数的首
地址,那么程序就会继续顺理成章的往下执行.
5. 数组和结构体
5.1 数据结构的意义
数据结构就是用来研究和组织数据在内存中的存放方式和处理方式。
5.2 最简单的数据结构-数组
为什么要有数组?
在程序中拥有很多个相同类型的变量需要进行管理的时候:
//比如我们需要五个整形数据需要管理,按照一般的方式,需要申明五个整形的变量。
int a1, a2, a3 ,a4, a5;
//数据量比较少的时候这样做也行,当我们需要一万个,十万个……数据的时候,这种写法就很令人窒息了,所以就有了数组
int b[10000];//申请了一万个整形的变量
当我们定义了一个数组的时候,编译器就会在内存中划分出一块连续的空间,用于存放我们申请的数组,比如int a[4];
我们申请了一块连续的内存空间用来存放这个数组。
数组的优势:数组的结构简单,并且可以随机访问,通过下标可以只访问任意一个成员
数据的限制:
(1)数组中所有的元素的类型必须相同;
(2)数组的大小是定义的时候给出来的,而且确定了以后就不可以更改了,设计的灵活性较差;
(3)删除元素麻烦,因为数组在内存中是连续的一片空间,如果需要删除其中的某个数据的时候,只能让后面的数据覆盖前面的数据。
5.3 数组的升级版-结构体
结构体发明出来就是为了解决数组所有元素必须相同的缺陷;
示例1:管理三个学生的年龄
//使用数组的写法
int student_age[3];
//查看第二个学生的年龄
student_age[1];
//使用结构的写法
struct Students
{
int student1_age;
int student2_age;
int studnet3_age;
};
struct Students students;
//查看第二个学生的年龄
students.student2_age;
使用两种方法都可以达到我们的目的,但是使用数组更加的方便,并且我们需要管理一万个学生的年龄的时候,使用结构体还是会出现定义一万次的缺点……;
示例2:管理三个学生的基本信息,包括年龄,语文的分数,数学的分数,英语的分数,总分,平均分等……
//使用数组的话,需要定义 6 个数组,每个学生对应所有数组的同一个下标
int students_age[3];
double students_chinese[3];
double students_math[3];
double students_english[3];
double students_sum[3];
double students_ave[3];
//获取第一个学生的信息
students_age[0]
········
//使用结构体的话
struct students
{
int student_age;
double student_chinese;
double student_math;
double student_english;
double student_sum;
double student_ave;
}
struct students st1;
struct students st2;
struct students st3;
从逻辑上来和操作上来说,结构体要比数组简单得多,比如这个时候,第二个学生因为成绩太差被退学了,需要删除第二个学生的所有信息,那是不是操作所有的数组删除一次就让人感到窒息……
5.4 数组使用下标访问原理
5.4.1 数组下标为什么从0 开始
数组从下标从 0 开始的主要意义为地址偏移量的确定的简洁性
比如我们需要访问数组中第三个元素,数组首地址为1000,i = 2, 数据类型的大小为 4,通过简单的计算,可以得到第三个数据的地址计算结果:addr = 1000 + 2 * 4 = 1008
,可以看到刚好是图中第三个元素的首地址,而需要访问第一个元素的时候,计算结果为:addr = 1000 + 0 * 4 = 1000
如果数组下标是从 1 开始的话,按照上面的公式的思路,定位地址的公式就要变成 第 i 个元素的地址 = 首地址 + (i - 1) * 数据类型的大小
,那么很明显的就能看出来,数组下标从 0 开始更加的简洁优雅,并且不需要多余的计算,效率比数组下标从 1 开始更好。
5.4.2 数组为什么能用下标访问
因为数组中,所有元素的类型都是一样的,数据类型的大小一样,所以按照地址定位公式,通过下标和首地址的计算,就能很轻松的访问到数组中的元素。
5.5 结构体的访问方式
- 因为结构可以用来存放不同的数据类型,例如:
struct test
{
char t1;
int t2;
double t3;
short t4;
}
我们知道第一个元素的地址,怎么算出第二个元素,第三个元素的地址呢?
答案是不可能算的出来的。所以结构体只能使用 ‘.’ 运算符进行访问,例:test.t1
,test.t4
……
结构体的字节对齐:
为了满足硬件上的效率,结构体的大小不是所有结构成员的大小简单相加,而是会有字节对齐的操作;
当我们创建上面的结构体test
的变量的时候:
编译器会在内存中找到能被 1 整除的内存地址,然后给
t1
分配内存空间;再往下寻找可以被 4 整除的内存地址,然后给
t2
分配内存空间;再往下寻找可以被 8 整除的内存地址,然后给
t3
分配内存空间;再往下寻找可以被 2 整除的内存地址,然后给
t4
分配内存空间;最后还会检查结构体的大小能否被最大的 8 字节整除,如果不能,还会在最后一个元素之后增加填充字节。
由上图可以看出,最终花费的地址空间为 24 个字节,设计程序验证;
#include<stdio.h>
struct test
{
char t1;
int t2;
double t3;
short t4;
};
int main()
{
struct test t;
printf("t 占的空间为:%d",sizeof(t));
return 0;
}