C语言内存管理基础
1.什么是内存
从硬件角度看:内存实际上是电脑的一个配件,一般叫内存条。根据不同的硬件实现原理还可以把内存分成SRAM
和DRAM
。
从逻辑角度看:内存是这样一种东西,它可以随机访问,只要给一个地址,就可以访问这个内存地址;并且可以读写,当然了逻辑上也可以限制其为只读或者只写。内存在编程中天然是用来存放变量的,就是因为有了内存,所以C
语言才能定义变量,C
语言中的一个变量实际就对应内存中的一片空间。
内存的逻辑抽象图,或者说内存的编程模型,从逻辑角度来讲,内存实际上是由无限多个内存单元格组成的,每个单元格有一个固定的地址叫内存地址,这个内存地址和这个内存单元格唯一对应且永久绑定。
以大楼来类比内存是最合适的,逻辑上的内存就好象是一栋无限大的大楼,内存的单元格就好象大楼中的一个个小房间。每个内存单元格的地址就好象每个小房间的房间号,内存中存储的内容就好象住在房间中的人一样。逻辑上来说,内存可以有无限大,因为数学上编号永远可以增加且没有尽头。但是现实中实际的内存大小是有限制的,比如32位的系统,32位系统指的是32位数据线,但是一般地址线也是32位,这个地址线32位决定了内存地址只能有32位二进制,所以逻辑上的大小为2的32次方,内存限制就为4GB,实际上32位的系统中可用的内存是小于等于4G的。
2.程序运行为什么需要内存
编写程序的目的是为了去运行这个程序,得到一定的结果。计算机程序其实都是在计算数据,所以计算机程序中很重要的部分就是数据。
- 计算机程序 = 代码 + 数据
用函数来类比:函数的形参就是待加工的数据,函数内还需要一些临时数据,也就是局部变量,函数本体就是代码,函数的返回值就是结果,函数体的执行过程就是过程。
计算机程序的运行过程,其实就是程序中很多个函数相继运行的过程。程序是由很多个函数组成的,程序的本质就是函数,函数的本质是加工数据的动作。
在冯诺依曼结构中,数据和代码是放在一起的。例如在三星S5PV210
上运行的Linux
系统,运行应用程序时所有的应用程序的代码和数据都在DRAM
上,所以这种结构就是冯诺依曼结构。而在哈佛结构中,数据和代码是分开存放的。比如在单片机中,我们把程序代码烧写到Flash
中,然后程序在Flash
中原地运行,程序中所涉及到的数据(全局变量、局部变量)不能放在Flash
中,必须放在RAM
中。
总结:为什么需要内存呢?内存是用来存储可变数据的,数据在程序中表现为全局变量、局部变量等,在
gcc
中,其实常量也是存储在内存中的,而大部分单片机中,常量是存储在Flash
中的,也就是在代码段,对我们写程序来说非常重要,对程序运行更是本质相关。所以内存对程序来说几乎是本质需求。越简单的程序需要越少的内存,而越庞大越复杂的程序需要更多的内存。内存管理是我们写程序时很重要的话题。数据结构是研究数据在内存中如何组织的,算法是为了用更优秀更有效的方法来加工数据,既然跟数据有关就离不开内存。
2.计算机中如何管理内存
对于计算机来说,内存容量越大则可以存放的数据越多,所以大家都希望自己的电脑内存更大。我们写程序时如何管理内存就成了很大的问题,如果管理不善,可能会造成程序运行消耗过多的内存,这样迟早内存都被这个程序吃光了,当没有内存可用时程序就会崩溃。所以内存对程序来说是一种资源,所以管理内存对程序来说是一个重要技术和话题。
当有操作系统时:操作系统掌握所有的硬件内存,因为内存很大,所以操作系统把内存分成一个一个的页面,其实就是一块一块的,一般每块的大小是4KB,然后以页面为单位来管理,这就是我们在操作系统里学习的页式存储。页面内用更细小的方式来以字节为单位管理。操作系统内存管理的原理非常麻烦、非常复杂、非常不人性化。但是对于使用操作系统的人来说,其实不需要了解这些细节。操作系统给我们提供了内存管理的一些接口,我们只需要用API即可管理内存。比如如在C
语言中使用malloc()
和free()
接口来申请和始放动态内存。
没有操作系统时:在没有操作系统,其实就是裸机程序中,程序需要直接操作内存,编程者需要自己计算内存的使用和安排,如果不小心把内存用错了,错误结果需要自己承担。
再从语言角度来讲:不同的语言提供了不同的操作内存的接口。比如在汇编中,汇编语言根本没有任何内存管理,内存管理全靠程序员自己,汇编中操作内存时直接使用内存地址,比如0xd0020010,非常麻烦;而在C
语言中,编译器帮我们管理直接内存地址,我们都是通过编译器提供的变量名等来访问内存的,操作系统下如果需要大块内存,可以通过接口函数来申请系统内存;C++
语言对内存的使用进一步封装:我们可以用new
来创建对象,其实就是为对象分配内存,然后使用完了用delete
来删除对象,其实就是释放内存。所以C++
语言对内存的管理比C
要高级一些,容易一些。但是C++
中内存的管理还是靠程序员自己来做。如果程序员new
了一个对象,但是用完了忘记delete
就会造成这个对象占用的内存不能释放,这就是内存泄漏。Java/C#
等语言不直接操作内存,而是通过虚拟机来操作内存。这样虚拟机作为我们程序员的代理,来帮我们处理内存的释放工作。如果我的程序申请了内存,使用完成后忘记释放,则虚拟机会帮我释放掉这些内存。听起来似乎Java/C#
等语言比C/C++
有优势,但是其实虚拟机回收内存是需要付出一定代价的,所以说语言没有好坏,只有适应不适应。当我们程序对性能非常在乎的时候,比如操作系统内核就会用C/C++
语言;当我们对开发程序的速度非常在乎的时候,就会用Java/C#
等语言。
4.一些描述内存大小的单位
位和字节:内存单元的大小单位有4个:位(1bit) 字节(8bit) 半字(一般是16bit) 字(一般是32bit)。在所有的计算机、所有的机器中,不管是32位系统还是16位系统还是以后的64位系统,位永远都是1bit,字节永远都是8bit。
字和半字:历史上曾经出现过16位系统、32位系统、64位系统三种,而且操作系统还有Windows
、Linux
、iOS
等很多,所以很多的概念在历史上曾经被混乱的定义过。
建议大家对字、半字、双字这些概念不要详细区分,只要知道这些单位具体有多少位是依赖于平台的。实际工作中在每种平台上先去搞清楚这个平台的定义,字是多少位,半字永远是字的一半,双字永远是字的2倍大小。编程时一般根本用不到字这个概念,那我们区分这个概念主要是因为有些文档中会用到这些概念,如果不加区别可能会造成你对程序的误解。
在Linux+ARM
这个软硬件平台上,字是32位的。
5.内存管理之栈stack
栈是一种数据结构,C
语言中使用栈来保存局部变量,栈是被发明出来管理内存的。栈的特点是入口即出口,只有一个口,另一个口是堵死的。所以先进去的必须后出来。
栈管理内存的特点:管理小内存,申请释放自动化。栈管理内存,好处是方便,分配和最后回收都不用程序员操心,C语言自动完成。
栈的应用举例:局部变量,C
语言中的局部变量是用栈来实现的。我们在C
中定义一个局部变量时(比如int a;
),编译器会在栈中分配一段4字节的空间给这个局部变量用,分配时栈顶指针会移动给出空间,给局部变量a
用的意思就是,将这4字节的栈内存的内存地址和我们定义的局部变量名a
给关联起来,对应栈的操作是入栈。这里栈指针的移动和内存分配是自动的,栈自己完成,不用我们写代码去操作。然后等函数退出的时候,局部变量要灭亡。对应栈的操作是出栈。出栈时也是栈顶指针移动将栈空间中与a
关联的那4个字节空间释放,这个动作也是自动的,也不用人写代码干预。
分析一个现象:
C
语言中定义局部变量时如果未初始化,则值是随机的,为什么?定义局部变量,其实就是在栈中通过移动栈指针来给程序提供一个内存空间和这个局部变量名绑定。因为这段内存空间在栈上,而栈内存是反复使用的,脏的,上次用完没清零的,所以说使用栈来实现的局部变量定义时如果不显式初始化,值就是脏的。如果显式初始化则会将这片脏空间重新赋值。
C
语言是通过一个小手段来实现局部变量的初始化的:
int a = 15; // 局部变量定义时初始化
C
语言编译器会自动把这行转成:
int a; // 局部变量定义
a = 15; // 普通的赋值语句
栈的约束:预定栈大小不灵活,怕溢出。首先,栈是有大小的。所以栈内存大小不好设置,如果太小怕溢出,太大怕浪费内存,这个缺点有点像数组;其次,栈的溢出危害很大,一定要避免。所以我们在C
语言中定义局部变量时不能定义太多或者太大,比如不能定义局部变量时 int a[10000000000];
,使用递归来解决问题时一定要注意递归收敛的问题。
6.内存管理之堆heap
堆也是一种内存管理方式。内存管理对操作系统来说是一件非常复杂的事情,因为首先内存容量很大,其次内存需求在时间和大小块上没有规律,操作系统上运行着的几十、几百、几千个进程随时都会申请或者释放内存,申请或者释放的内存块大小随意。
堆这种内存管理方式特点就是自由:随时申请、释放且申请内存块的大小随意。堆内存是操作系统划归给堆管理器来管理的,堆管理器是操作系统中的一段代码,属于操作系统的内存管理单元,它向使用者(用户进程)提供接口malloc()
和free()
来使用堆内存。当我们需要的内存容量比较大时,以及需要反复使用及释放时,就可以使用堆内存。
堆管理内存的特点:
- 特点一:容量不限,常规使用的需求容量都能满足。
- 特点二:申请及释放都需要手工进行,手工进行的含义就是需要程序员写代码明确进行申请及释放。如果程序员申请内存并使用后未释放,这段内存就丢失了,在堆管理器的记录中,这段内存仍然属于你这个进程,但是进程自己又以为这段内存已经不用了,再用的时候又会去申请新的内存块,这就叫吃内存,称为内存泄漏。在
C/C++
语言中,内存泄漏是最严重的程序Bug
,这也是有的人认为Java/C#
等语言比C/C++
优秀的地方。
7.malloc()和free()函数的初步使用
堆内存申请时,有3个可选择的类似功能的函数:
void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size); // nmemb个单元,每个单元size字节
void *realloc(void *ptr, size_t size); // 改变原来申请的空间的大小的
比如要申请10个int
元素的内存:
int*p1 = (int*)malloc(40);
int*p2 = (int*)malloc(10*sizeof(int));
int*p3 = (int*)calloc(10, 4);
int*p4 = (int*)calloc(10, sizeof(int));
堆内存释放时最简单,直接调用free()
释放即可
free(p1);
free(p2);
free(p3);
free(p4);
数组定义时必须同时给出数组元素个数,即数组大小,而且一旦定义再无法更改。在Java
等高级语言中,有一些语法技巧可以更改数组大小,但其实这只是一种障眼法,它的工作原理是:先重新创建一个新的数组大小为要更改后的数组,然后将原数组的所有元素复制进新的数组,然后释放掉原数组,最后返回新的数组给用户。
堆内存申请时必须给定大小,然后一旦申请完成大小不变,如果要变只能通过realloc()
接口,该函数的实现原理类似于上面说的Java
中的可变大小的数组的方式。
8.复杂数据结构
复杂数据结构有链表、哈希表、二叉树、图等。因为现实中的实际问题是多种多样的,问题的复杂度不同,所以需要解决问题的算法和数据结构也不同,所以当你处理什么复杂度的问题,就去研究针对性解决的数据结构和算法。