问题:程序是在内存中运行的,是暂存一些临时变量的场所,这些内存是如何分配给数据的呢?也就是说这些数据是如何在内存中排布的呢?
首先要明白一个排布的工具:数据结构
数据结构:数据结构就是研究数据如何组织加工的
常见的数据结构有:数组(最简单的数据结构)、结构体、栈、堆、队列、链表、哈希表、二叉树…………
内存管理是由操作系统完成的,操作系统就是“包租婆”
(1)内存本质上是一个硬件
(2)程序使用内存:获取内存(在操作系统登记临时征用该内存的使用权限)、使用内存、释放内存(将内存归还给操作系统)。操作系统有点像“包租婆”,租房就是获取内存,入住就是使用内存,最后不住了将房子(内存)归还包租婆。
数组
数组管理内存和普通变量本质上没有太大差别
普通变量int a——首先,编译器在内存中申请一段内存(4字节),获得该内存的地址(编译器知道地址,我们程序员不知道地址是多少,也没必要知道);
然后将内存分配给a,再将数据变量名a与该地址绑定起来;
最后,等待a赋值,将赋值的数存入该内存。
数组int b[10]——首先,编译器在内存中申请一段大小为4×10=40字节的内存,并获得该内存的地址(实际是首地址);
然后,将内存分配给b,再将数组名b与首地址绑定;
最后,将赋值的数组内容存入内存
结构体
其实结构体就是更加“强大”的数组。因为在数组中有一个特点,也是数组的局限性——数组中元素的数据类型必须是相同的,也就是说同一个数组中不可能出现int和char类型。
所以,结构体作为“拯救者”出现了,它打破了这个局限性,在同一个结构体中可以出现多个不同的数据类型,如下;
struct studengt
{
char name[];
char sex[];
int ages;
};
栈(stack)
栈是一种数据结构,在c语言中使用栈来管理普通局部变量(静态局部变量放在数据段上),是用来管理内存的。操作系统都会为每一个程序分配一段栈内存
栈管理内存的特点:先进后出(队列是先进先出)
C语言中的局部变量是如何利用栈实现的呢?
例如:在处于C语言中定义一个局部变量int a
首先,编译器会在栈中分配一段空间(4字节)给这个局部变量用(分配时栈顶指针会移动给出空间,给局部变量a用的意思就是,将这4字节的栈内存的内存地址和我们定义的局部变量名a给关联起来),对应栈的操作是入栈。
注意:这里栈指针的移动和内存分配是自动的(栈自己完成,不用我们写代码去操作)。
然后等我们函数退出的时候,局部变量要灭亡。对应栈的操作是弹栈(出栈)。出栈时也是栈顶指针移动将栈空间中与a关联的那4个字节空间释放(“”释放”可以理解为这个内存单元没有人住了,但是之前的租户没有把房子里面的东西收拾干净带走,很脏)。这个动作也是自动的,也不用人写代码干预。
上面的表达的意思是:a出栈后,内存仍然有a变量在,出栈相当于将a复制了一份拿出去,内存不变。
所以,我们在这里思考一下,虽然出栈使得内存得到了释放,但是内存中仍然“很脏”(没有清零),假如之前的int a=2,这个时候我们又要重新定义另外一个局部变量int d,但是我们没有将d初始化;将会发生什么意想不到的事情呢?
可能编译器正好将之前的int a地址内存分配给int b.这个时候b的值就是2。
经过上面的解释,我们就很容易明白在C语言中为什么变量一定要记住初始化了吧!嘿嘿
同时也可以理解这两段程序等价的原因:
int a; a=2; //等价于int a=2;
栈的优点:栈管理内存,好处是方便,分配和最后回收都不用程序员操心,C语言自动完成。
栈的局限性:栈的大小有限,不能定义太多局部变量,否则会出现栈溢出。例如:int d[100000]这种使用递归解决的问题一定要注意递归收敛,防止溢出风险!
堆(heap)
堆是由操作系统堆管理器管理,堆管理器是操作系统中的一个模块,管理内存方式;按需分配。
堆(heap)是一种内存管理方式。内存管理对操作系统来说是一件非常复杂的事情,因为首先内存容量很大,其次内存需求在时间和大小块上没有规律(操作系统上运行着的几十、几百、几千个进程随时都会申请或者释放内存,申请或者释放的内存块大小随意)。
堆这种内存管理方式特点就是自由(随时申请、释放;大小块随意)。堆内存是操作系统划归给堆管理器(操作系统中的一段代码,属于操作系统的内存管理单元)来管理的,然后向使用者(用户进程)提供API(malloc和free)来使用堆内存。
我们什么时候使用堆内存?
需要内存容量比较大时,需要反复使用及释放时,很多数据结构(譬如链表)的实现都要使用堆内存。
堆管理内存的特点(大块内存、手工分配&使用&释放):
特点一:容量很大(常规使用的需求容量都能满足)用多少申请多少
特点二:申请及释放都需要手工进行,手工进行的含义就是需要程序员写代码明确进行申请malloc及释放free。
如果程序员申请内存并使用后未释放,这段内存就丢失了(在堆管理器的记录中,这段内存仍然属于你这个进程,但是进程自己又以为这段内存已经不用了,再用的时候又会去申请新的内存块,这就叫吃内存),这称为内存泄漏。在C/C++语言中,内存泄漏是最严重的程序bug,这也是别人认为Java/C#等语言比C/C++优秀的地方。
特点三:其实堆内存在malloc()之后和free()之前都是“”脏的”
C语言操作堆内存的接口(malloc、 free)
堆内存释放时最简单,直接调用free释放即可。
void free(void *ptr);
堆内存申请时,有3个可选择的类似功能的函数:
malloc, calloc, realloc
void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size);
- nmemb个单元,每个单元size字节
void *realloc(void *ptr, size_t size);
- 改变原来申请的空间的大小的
譬如要申请10个int元素的内存:
malloc(40); malloc(10*sizeof(int));
calloc(10, 4); calloc(10, sizeof(int));
数组定义时必须同时给出数组元素个数(数组大小),而且一旦定义再无法更改。在Java等高级语言中,有一些语法技巧可以更改数组大小,但其实这只是一种障眼法。它的工作原理是:先重新创建一个新的数组大小为要更改后的数组,然后将原数组的所有元素复制进新的数组,然后释放掉原数组,最后返回新的数组给用户;
堆内存申请时必须给定大小,然后一旦申请完成大小不变,如果要变只能通过realloc接口。realloc的实现原理类似于上面说的Java中的可变大小的数组的方式。
堆的优势和劣势(管理大块内存、灵活、容易内存泄漏)
优势:灵活;
劣势:需要程序员去处理各种细节,所以容易出错,严重依赖于程序员的水平
数据段(.data)
编译器在编译程序的时候会将程序分为代码段、数据段、bass段三个部分。
代码段:程序中可执行的部分,直观上理解就是函数堆叠而成的。
数据段;直观上理解就是C语言中的全局变量(注意:全局变量才算是程序的数据,而局部变量不算是程序的数据,只能算是函数的数据),并且全局变量初始化为非0.或者静态局部变量(static )也是放在数据段。
bass段:又叫ZI(zero initial)段。本质上也属于数据段,是全局变量初始化为0的数据段或者说是未显式初始化的全局变量(在c语言中未显式初始化的全局变量默认初始化为0)
注意:也有一些特殊的数据会被放到代码段,这里就不详细描述,后面将会专门写一篇博客来描述!