Python的很多教材中并没有讲内存方面的知识,但是内存的知识非常重要,对于计算机工作原理和方便理解编程语言是非常重要的,尤其是小白,因此需要把这一方面加上,能够更加深入的理解编程语言。这里引用了C语言关于内容的详细讲解,其实很多知识都是相同的。
第一部分:程序(计算机运行)为什么需要内存?
对于内存的理解是对编程语言直接相关的,如果没有对内存有很深的认识的话,对于编程语言也就是没有根本的认识,编程语言跟内存有千丝万缕的联系。
1.1 计算机程序运行的目的
计算机为什么需要编程?编程已经编了那么多年了,已经写了很多程序了,为什么还需要另外写程序?计算机有这个新的程序到底是为了什么?这些问题想过没有?程序的目的是为了去运行,程序运行是为了得到一定的结果和目的。计算机为什么叫计算机?计算机就是用来计算的,与你家里买的计算器本质上都是一样的。比如我们今天用计算机打游戏,跟计算关系非常大,有些游戏画面非常唯美,就是用算法计算出来的,计算机每时每刻都在计算,所有的计算机程序其实都是在做计算,计算就是计算数据。所以计算机程序中很重要的部分就是数据。因此我们要有下面的公式:
计算机程序 = 代码 + 数据
(计算机程序运行完得到的一个结果,就是说代码 + 数据经过运行后) = 结果
从宏观上来理解,代码就是动作,就是加工数据的动作;数据就是数字(对象),上就是被代码所加工的东西。那么可以得出结论:程序运行的目的不外乎2个:结果和过程。用函数类比:函数的形参就是待加工数据(函数内还需要一些临时数据,就是局部变量),函数本体就是代码,函数返回值就是结果,函数体的执行过程就是过程。
比如我们用C++写几个简单的函数:
int add(int a, int b)
{
return a + b;
} //这个函数的执行就是为了得到结果
void add(int a, int b)
{
int c;
c = a + b;
printf("c = %d.\n", c);
} //这个函数的执行重在过程(重在过程中的printf),返回值不重要
int add(int a, int b)
{
int c;
c = a + b;
printf("c = %d.\n", c);
return a + b;
} //这个函数是又重结果又重过程
从这三个函数来看,就可以知道在程序运行的过程中,结果和过程的相互概念。
1.2 计算机程序运行的过程分析
计算机程序运行过程,其实就是程序中很多个函数相继运行的过程。计算机程序是个名词,但是计算机的运行是个动词。程序时由很多个函数组成的,程序的本质就是函数,函数的本质就是加工动作。
1.3 冯.诺依曼结构和哈佛结构
冯.诺依曼结构:数据和代码放在一起。
哈佛结构:数据和代码分开存放。
什么是代码:函数
什么是数据:全局变量、局部变量等
在S5PV210中运行Linux系统,运行应用程序时,这个时候所有的应用程序的代码和数据都在DRAM。所以这种结构就是冯.诺依曼结构;在单片机中,我们把程序代码烧写到Flash(norFlash)中,然后程序在Flash中原地运行,程序中所涉及到的数据(全局变量、局部变量)不能放在Flash中,必须放在RAM中(SRAM)中。
1.4 动态内存DRAM和静态内存SRAM
DRAM是动态内存,SRAM是静态内存。
1.5 总结:为什么需要内存?
内存是用来存储可变数据的,数据在程序中表现为全局变量、局部变量等(在gcc中,其实常量也是存储在内存中的)(大部分单片机中,常量是存储在Flash中的,也就是在代码段),对我们写程序来说非常重要,对程序运行更是本质相关。所以内存对程序来说几乎是本质需求,越简单的程序需要越少的内存,而越庞大复杂的乘除需要更多的内存,内存管理是我们写程序时很重要的话题。我们以前学习的了解过的很多编程的关键其实是为了内存,譬如说数据结构(数据结构是研究数据如何组织的,数据是存放在内存中的)和算法(算法是为了用更优秀更有效的方法来加工数据,既然跟数据有关就离不开内存,衡量一个算法优秀的标准是,同样一个算法所使用的内存越少算法越优秀)。
1.6 深入思考:如何管理内存(无OS时,有OS时,有无操作系统)
对于计算机来说,内存容量越大则可能越大,所以大家都希望自己的电脑内存更大。我们写程序时如何管理内存就成了很大的问题,如果管理不善可能会造成程序运行消耗过多的内存,这样迟早内存都被你这个程序吃光了,当没有内存可用时程序就会崩溃。所以内存对程序来说是一种资源。所以管理内存对程序来说是一个重要的技术和话题。
我们先从操作系统角度来讲:操作系统掌管所有的硬件内存,因此内存很大,所以操作系统把内存分成每个页面(其实就是块,一般是4KB大小),然后以页面为单位进行管理。页面内用更细小的方式来以字节为单位管理。操作系统内存管理的原理非常麻烦、非常复杂、非常不人性化。那么对于我们使用这些操作系统的人来说,其实不需要了解这些细节。操作系统给我们提供了内存管理的一些接口,我们只需要用API即可管理内存。譬如在C语言中使用malloc、free这些结构来管理内存。如果没有操作系统时:没有操作系统(其实就是逻辑程序)中,程序需要直接操作内存,编程者需要自己计算内存使用和安排,如果编程者不小心把内存用错了,错误结果需自己承担。
再从语言角度来讲:不同的语言提供了不同的操作内存接口。譬如汇编:根本没有任何内存管理,内存管理全靠程序员自己,汇编操作内存是直接使用内存地址(譬如0xd0020010),非常麻烦;
譬如C语言:C语言中编译器帮我们管理直接内存地址,我们都是通过编译器提供的变量名来访问内存的,如果需要大块的内存,可以通过API(malloc、free等)来访问系统内存(有OS时)。如果在逻辑程序中需要大块的内存需要来自己定义数组等来解决。
譬如C++语言:对内存的使用进一步封装。我们可以用new来创建对象(其实就是为对象分配内存),然后用完了用delete来删除对象(其实就是释放内存),所以C++语言对内存的管理比C要高级一些,容易一些,但是C++zhong内存的管理还是靠程序员自己来做,如果程序员new了一个对象,但是用完了忘记了delete就会造成这个对象占用的内存不能释放,这就是内存泄漏(溢出)。
譬如java/C#等:这些语言不直接操作内存,而是通过虚拟机来操作内存,这样虚拟机作为我们程序员的代理,来帮我们处理内存的释放工作。如果我的程序申请了内存,使用完后忘记释放,则虚拟机会帮我释放掉这些内存。听起来似乎这些高级语言比C/C++有优势,但是其实他这个虚拟机回收内存是需要付出一定代价的,所以语言没有好坏,只有适应不适应。当我们程序对性能非常在乎的时候(譬如操作系统内核)就会用C/C++语言;当我们对开发程序的速度非常在乎的时候,就会用这些高级语言。
第二部分:位、字节、半字、字的概念和内存位宽
从逻辑上阐述内存的编程模型和逻辑认识,并且解释了内存单元的几个单位:位、字节、半字、字。从逻辑上对内存有一个认知,先建立起来大框架性概念。
2.1 什么是内存?(硬件和逻辑两个角度)
硬件角度:内存实际上是我们电脑的一个配件(一般叫内存条),如下图:黑色里面放着内存颗粒,绿色的板子就是把这些内存链接起来。
根据不同的硬件实现原理还可以把内存分成SRAM和DRAM(DRAM又有好多带,譬如最早的SDRAM,后来的DDR1,DDR2,DDR3,DDR4...、LPDDR等)这些百度相关知识都有,这里不是从硬件角度来讲,主要从逻辑上面来说。
逻辑角度:内存他是一个随机访问(随机访问的意思是只要给一个地址,就可以访问这个内存地址),并且可以读写(当然了逻辑上也可以限制其为只读或者只写);内存在编程中天热是用来存放变量的。也就是因为有了内存,所以C语言才能定义变量,C语言中的一个变量实际就是对应内存中的一个单元。
2.2 内存的逻辑抽象图(内存的编程模型)
这是一个32位的内存:
从逻辑角度来讲,内存实际上是由无限多个内存单元格组成的,每个单元格有一个固定的地址叫内存地址,这个内存地址和这个内存单元格唯一对应且永久绑定。以大楼来类比内存最合适,逻辑上的内存就好像是一栋无限大的大楼,内存的单元格就好像大楼中一个个小房间,每个内存单元格的地址就好像每个小房间的房间号。内存中存储的内容好像住在房间中的人一样。
逻辑上来说,内存可以有无限大(因为数学上编号永远可以增加,无尽头),但是现实中实际的内存大小是有限制的。
譬如32位的系统:内存限制就为4G。(32位系统指的是32位数据线,但是一般地址也是32位,这个地址线32位决定了内存地址只能32位的二进制,所以逻辑上的大小为2^32次方),实际上32位的系统中可用的内存就是小于等于4G的(32位CPU中装32位Windows,但实际电脑只有512M内存)。
2^32次方 = 4294967296bit / 1024 = 4194304KB / 1024 = 4096M / 1024 = 4G
2.3 位和字节
内存单元的大小单位有4个:位、字节、半字、字
位:大小为1个bit
字节:大小为8个bit
在所有的计算机、所有的机器中(不管是32位系统还是16位系统还是64位系统),为用于是1个bit,字节永远是8bit。位和字节是计算机最小的单位,也就是一层楼的4间房子,每间房子8个平方(8个bit或者1个字节)。
2.4 字和字节
半字:一般是16bit
字:一般是32bit
历史上出现过很多对于字的混乱,建议对字、半字、双字这些概念不要详细区分,只要知道这些单位具体有多少位是依赖于平台的,实际工作中在每种平台上先去搞清楚这个平台的定义(字是多少位,半字永远是字的一半,双字永远是字的两倍大小)
编程时一般根本用不到字这个概念,那我们区分这个概念主要是因为有些文档中会用到这些概念,如果不加区别可能会造成你对程序的误解。
2.5 内存位宽(硬件和逻辑两个角度)
(我们知道一个int类型的变量是32位,不同位宽读取的方式不同)
从硬件角度来讲:硬件内存的实现本身是有宽度的,也就是说有些内存条就是8位的,而有些就是16位的。那么需要强调的是内存芯片之间是可以并联的。通过并联后即使8位的内存芯片也可以做出来16位或32位的硬件内存。
从逻辑角度来讲:内存位宽是任意的,甚至逻辑上存在内存的位宽是24位的内存(但是实际上这种硬件是买不到的,也没有实际意义)。从逻辑角度来讲不管内存位宽是多少,我就直接操作即可,对我的操作不构成影响,因为你的操作不是纯逻辑而是需要硬件去执行的,所以不能为所欲为,所以我们实际的很多操作都是受限于硬件的特性的。譬如24位的内存逻辑上和32位的内存没有任何区别,但实际硬件是32位的,都按照32位硬件的特性和限制来干活。
这里特别说明:内存的位宽是衡量一个内存的最重要指标,如果CPU也是32位宽,内存也是相匹配的32位宽,在实际计算数据过程中,每次即可处理一层楼的数据。如果CPU是64位宽,内存是32位宽,每次给的一层楼的数据只有32位,这样会造成资源浪费,因此这是一个硬件匹配的重要指标。
可以理解为:每次处理数据的吞吐量 = 内存的位宽。
第三部分:内存编址和寻址、内存对齐
3.1 内存编址方法
内存在逻辑上就是一个一个的格子(8个平方或者说8bit或者说1KB),这些个子可以用来装东西(里面装的东西就是内存中存储的数),每个格子有一个编号,这个编号就是内存地址,这个内存地址(一个数字)和这个格子的动机(实质是一个空间)是一一对应且永久绑定的,这就是内存的编址方法。
在程序运行时计算机中CPU实际只认识内存地址,而不关心这个地址所代表的空间在哪里,怎么分布这些实体问题。因为硬件设计保证了按照这个地址就一定能找到这个格子,所以说内存单元的2个概念:地址和空间是内存单元的两个方面。
3.2 关键:内存编址是以字节为单位的
我随便给一个数字(譬如说是7),然后这个数字是一个内存地址,然后我问你这个内存地址对应的空间是多大?
这个地址的大小空间是固定的,就是一个字节(1KB = 8bit)。
如果把内存比喻为一栋大楼,那么这个楼里面的一个一个房间就是一个一个内存格子,这个格子的大小是固定的8bit,就好像这个大楼里面所有房间户型都是一样的,都是工工整整的。
3.3 内存和数据类型的关系
C语言中的最基本数据类型有:char(8bit) short(半个int) int(32bit) long float double
int 整型(整数类型,这个整体现在它和CPU本身的数据位宽是一样的)。譬如32位的CPU,整型就是32位,所以int 就是32位和CPU位宽是绑定的。
数据类型和内存的关系就在于:
数据类型是用来定义变量的,而这些变量需要存储、运算在内存中。所以说数据类型必须和内存相匹配才能获得最好的性能,否则可能不工作或者效率低下。
在32位系统中定义变量最好用int,因为这样效率高。原因就在于32位系统本身配合内存等也是32位,这样的硬件配置天生适合定义3位的int类型变量,效率高。也能定义8位的char类型变量或者16位short类型变量,但实际上访问效率不高。
有一个特点:刚好匹配是最好的!
特别说明:在很多32位环境下,我们实际定义bool类型变量(实际只需要1个bit就够了)都是用int来实现bool的,也就是说我们定义一个bool b1;时,编译器实际帮我们分配了32位的内存来存储这个bool变量b1。编译器这样做实际上是浪费了31位的内存,但好处是效率高。比如你去商店买火柴,我1分钱买8根火柴,老板一般都不卖给你,一般都是按一盒来买。
问题:实际编程时是以省内存为大还是以运行效率为重?答案是不定的,看具体情况。很多年前内存很贵,机器上内存都很少,那时候写代码以省内存为主。现在随着半导体技术的发展内存变得很便宜了,现在的机器都是高配,不在乎省一点儿内存,而效率和用户体验变成了关键。所以现在写程序大部分情况下都是以效率为重。
3.4 内存对齐
内存的对齐方式有很多种,譬如下面:
我们定义一个 int a;类型变量,在内存中必须分配4个字节(32bit = 4 * 1KB)来存储这个a变量。那么就可以任意提出2中不同的内存分配思路和策略:
第一种:0、1、2、3 对齐访问
第二种:1、2、3、4 或者:2、3、4、5 或者:3、4、5、6 非对齐访问
内存的对齐访问不是逻辑的问题,是硬件的问题,从硬件角度来说,32位的内存它0 1 2 3四个单元本身逻辑上就有相关性,这4个单元组合起来当做一个int硬件上就是合适的,效率就高。
比喻:4个人出差,办了4个房间号,发现3 4 5是在一起连着的,但是其他2号房间隔着好远。这是逻辑和现实方面的差异。
对齐访问配合硬件所以效率很高,而非对齐访问因为和硬件本身不搭配,所以效率不高。(因为兼容性的问题,一般硬件也都非对齐访问,但是效率要低很多。)
3.5 从内存编址看数组的意义
int 类型有4个地址,32个地址码,数组的出现是和内存编址天然对应的。
第四部分:C语言如何操作内存
4.1 C语言对内存地址的封装(用变量名来访问内存、数据类型的含义、函数名的含义)
用变量名来访问内容:
比如在汇编里面 #define GPJ0CON 0x0200240 中直接把内存地址进行宏定义GPJ0CON丢给编译器去执行内存。
譬如在C语言中:int a; a = 5; a += 4; //a = 9;
结合内存来解析C语言语句的本质:
int a; //编译器帮我们申请了1个int类型的内存格子(长度是4个字节,地址只有编译器知道,把这个细节隐藏了),并且把符号a和这个格子绑定
a = 5; //编译器发现我们要给a赋值,就会把这个值5丢到符号a绑定的那个内存格子中。
a += 4; //编译器发现我们要给a加值,编译器就会把a原来的值读出来,然后给这个值加4,再把加之后的和写入a里面去。
这就是C语言和汇编的一种对应关系。
数据类型的含义:
C语言中数据类型的本质含义是:表示一个内存格子的长度和解析方法。
譬如把0进行类型的强制变换:
(int *)0; //0地址里面存在的指针指向的是int类型,给定这个类型是多长。
(float *)0;
(short)0; //0地址执行一个short类型的变量
(char)0:
之前讲过一个很重要的概念;内存单元格子的编址单位是字节。数据类型决定了长度的含义,我们一个内存地址,本来这个地址只代表1个字节的长度,但实际上我们可以通过给他一个类型(int),让它有了长度(4),这样这个代表内存的数字就能表示从这个数字开头的连续的8个字节的内存格子了。
因此:内存地址数字(0x30000000)开始连续4个字节的内存格子(0x30000000, 0x30000001, 0x30000002, 0x30000003)
数据类型决定解析方法的含义,譬如我有一个内存地址(0x30000000),我们可以通过给这个内存地址不同类型来指定这个内存单元中二进制数的解析方法,譬如:我int 0x30000000,含义就是(0x30000000, 0x30000001, 0x30000002, 0x30000003)这4个字节链起来共同存储的是一个int类型。
强制类型转换的含义:就是开始的类型的解析进行强制更改。
函数名的含义:
C语言中,函数就是一段代码的封装!函数名的实质就是这一段代码的首地址。所以说函数名的本质也是一个内存地址。
4.2 用指针来间接访问内存
关于类型(不管是普通的变量类型int float等,还是指针类型int * float*等),只要记住:
第一:类型只是对后面数字或者符号(代表的是内存地址)所表征的内存的一种长度规定和解析方法规定而已。
int a; //int a;时编译器会自动给a分配一个内存地址,譬如说0x12345678
(int *)a;
(float *)a;
第二:譬如int a 和int *p 其实没有任何区别,a和p都代表一个内存地址(譬如0x20000000),但是这个内存地址(0x20000000)的长度和解析方法不同,a是int类型所以a的长度是4字节,解析方法是按照int的规定来的;p是int*类型,所以长度是4字节,解析方法是int *的规定来的(0x20000000)开头的连续4个字节中存储1个地址,这个地址所代表的内存单元中存放的是一个int 类型的数。
4.3 指针类型的含义
C语言中的指针,全名叫指针变量,指针变量其实跟普通变量没有任何区别。
4.4 用数组来管理内存
数组管理内存和变量其实没有本质区别,知识符号的解析方法不同。(普通变量、数组、指针变量其实都没有本质差别,都是对内存地址的解析,知识解析方法不一样)。
int a; //int类型a //编译器分配4字节长度给a,并且把首地址和符号a绑定起来。
int b[10]; //数组变量b //编译器分配40个字节长度给b,并且把首元素的首地址和符号b绑定起来。
数组中第一个元素(a[0])就称为首元素;每一个元素类型都是int,所以长度都是4,其中第一个字节就成为首地址;那么首元素a[0]的首地址就称为首元素首地址。
4.5 用变量来访问内存和用指针来访问内存的区别
这里用一个最简单的表述就是用变量来访问内存其实就是把一个变量名a规定了长度和空间大小,用指针来访问是画出以某个首地址作为开始多长截止就是一个规定类型。这句话有点儿绕口,形象点儿比喻买包装好的一个一个大小相等的盒子里面装的蛋糕,这就是用变量来访问;用指针访问就是知道这个蛋糕大小,自己去拿着一个标准去称,称出来的也就是整个一样的蛋糕。
一个是买成品,大小不能变了;一个是按照成品的标准,去丈量一个新的成品。
第五部分 内存管理之结构体
内存管理的高级话题就是研究这些数据结构。
5.1 数据结构这门学问的意义
数据结构就是研究数据如何组织(在内存中排布),如何加工的学问题。
5.2 最简单的数据结构:数组
为什么要有数组?
因为程序中有好多个类型相同、意义相关的变量需要管理,这时候如果用单独的变量来做,程序看起来比较乱,用数组来管理会更高管理。
譬如:
int ages[20]; //20个同学的年龄放在一个数字里面,20个同学的年龄都是int类型的(类型相关),而且20个同学的年龄都是相关的(意义相关)。
5.3 数组的优势和缺陷
优势:数组比较简单,访问用下标,可以随机访问。
缺陷:1.数组中所有元素类型必须相同;2.数组大小必须定义时给出,而且一旦确定不能再改(数组不具有伸缩性)。
(Python当中类似的有一个变量叫列表,但是不必元素类型都相同。)
5.4 结构体隆重登场
结构体发明出来就是为了解决数组的第一个缺陷(数组中所有元素类型必须相同)
譬如一个结构体(我们要管理3个学生的年龄,int类型),怎么办?
第一种解法:用数组
int ages[3];
第二种解法:用结构体
struct ages
{
int age1;
int age2;
int age3;
};
struct ages age;
分析总结:在这个示例中,数组要比结构体好。三十不能得出结论说数组就比结构体好,在包中元素类型不同时就只能用结构体而不能用数组了。
再比如定义这个结构体:
struct people
{
int age; //人的年龄
char name[20]; //人的姓名
int height; //人的身高
}
分析总结:因为people的各个元素类型不完全相同,所以必须用结构体,没法用数组。
另外:数组为什么用下标来访问,因为每一个变量之间的间隔是4个字节,a[0]的地址+8就是a[2]是多少。结构体里面每个元素的长短是不一样的,所以要用“.”来访问,只有这样编译器才能找到每一个元素的地址。所以说数组和结构体的差异在内存的寻址的方式不同。
5.5 题外话:结构体内嵌指针实现面向对象
面向过程与面向对象:
总的来说:C语言是面向过程的,但是C语言写出来的Linux系统是面向对象的。非面向对象的语言,不一定不能实现面向对象的代码。知识说用面向对象的语言来实现面向对象要更加简单一些、直观一些、无脑一些。用C++、Java等面向对象的语言来实现面向对象简单一些,因为语言本身帮我们做了很多事情;但是用C来实现面向对象很麻烦,看起来也不容易理解,这就是为什么大多数人学过C语言却看不懂Linux内核代码的原因。
怎么实现呢?就用结构体内嵌指针实现,譬如下面一段代码:
struct a
{
int age; //普通变量
void (*pFunc)(void); //函数指针,指向void func(void)这类的函数
}
使用这样的结构体就可以实现面向对象。这样包含了函数指针的结构体,就类似与面向对象里面的class(类),结构体中的变量类似于class中的成员变量,结构体中的函数指针类似于class中的成员方法。
第六部分 内存管理之栈(stack)
栈是栈,堆是堆,没有堆栈这么一说。
6.1 什么是栈
栈是一种数据结构,c语言中使用栈来保存局部变量。栈是被发明出来管理内存的。底指针不动,顶指针随着数据的删减(也叫弹栈)进行上下进行移动。
6.2 栈管理内存的特点(小内存、自动化)
先进后出 FILO first in last out ==> 栈
先进先出 FIFO first in first out ==> 队列
如图所示:
栈的特点是入口即出口,只有一个口,另一个口是堵死的,所以陷进去的必须后出来。这很像是青岛的栈桥。
队列的特点是入口和出口都有,必须从入口进去,从出口出来,所以先进去的必须先出来,否则就堵住后面的,这个比喻很像银行的排队。
6.3 栈的应用举例:局部变量
C语言中的局部变量是用栈来实现的。
我们在C中定义一个局部变量时:
int a;
编译器会在栈中分配一个段空间(4个字节)给这个局部变量用(分配栈顶指针会移动给出空间,给局部变量a用的意思就是将这4个字节的栈内存的内存地址和我们定义的局部变量名a给关联起来),对应栈的操作是入栈。
注意:这里的栈指针的移动和内存分配是自动的(栈自己完成,不用我们写代码去操作)。
然后等我们函数退出的时候,局部变量要“灭亡”,对应栈的操作是弹栈(出栈)。出站时也就是栈顶指针移动将栈空间中与a管理的那4个字节空间释放。这个动作也是自动的,也不用人写代码干预。
栈的优点:栈管理内存,好处是方便,分配和最后回收都不用程序员操心,C语言会自动完成。
分析一个细节:C语言中,定义局部变量时如果未初始化,则值是随机的,为什么?
定义局部变量,其实就是在栈中通过移动栈指针来给程序提供一个内存空间和这个局部变量名绑定,因为这段内存空间在栈上,而栈内存是反复使用的(弹栈知识栈指针移动了,栈内存是“脏的”,上次用完没有清零的),所以说使用栈来实现栈的局部变量时如果不显示初始化,值就是脏的,如果你显示初始化。
C语言是通过一个手段来实现局部变量的初始化的。譬如下面这段代码:
int a =15; //局部变量定义时初始化
C语言编译器会自动把这行转成
int a; //局部变量定义
a = 15; //普通的赋值语句
6.4 栈的不足之处(预定栈大小不太灵活,怕溢出)
首先,栈是由大小的。所以栈内存大小不好设置。如果太小怕溢出,太大怕浪费内存。(这个缺点有点儿像数组)
其次,栈的溢出危害很大,一定要避免。所以我们在C语言中定义局部变量时不能定义太多或者太大(譬如不能定义局部变量时这样:int a[10000])(再譬如使用递归来解决问题时一定要注意递归收敛)
第七部分 内存管理之堆(heap)
栈是栈,堆是堆,没有堆栈这么一说。
7.1 什么是堆
堆是一种内存管理方式。内存管理对操作系统来说是一件非常复杂的事情,因为首先内存容量很大,其次内存需求在时间和大小块上没有规律(操作系统上运行着十几、几十、上百、上千的进程随时都会申请或者释放内存,申请或释放的内存块大小随意)。
7.2 堆管理内存的特点(大块内存、手工分配、使用、释放)
如图所示,系统开始加载内存的时候,堆内存暂时没有使用,供谁需求谁去拿取使用。
堆这种内存管理方式特点就是自由(随时申请、释放、大小块随意),堆内存是操作系统划归给堆管理器(操作系统中的一段代码,属于操作系统的内存管理单元(操作系统最主要的两部分就是内存管理和用户进行))来管理的。然后项使用者(用户进程)提供API(malloc 和 free)来使用堆内存。
我们什么时候使用堆内存?
需要内容容量比较大,需要反复使用及释放时,很多数据结构(譬如链表)的实现都要使用堆内存。
总结:
特点一:容量不限(常规使用的需求容量都能满足)。
特点二:申请及其释放都需要手工进行,手工进行的含义就是需要程序员写代码明确进行申请malloc及其释放free,如果程序员申请内存并使用后未释放,这段内存就丢失了(在堆管理器的记录中,这段内存仍然属于你的这个进程,但进程自己又以为这段内存已经不能用了,再用的时候又会去申请新的内存块,这就叫“吃内存”),成为内存泄漏。在C/C++语言中,内存泄漏是最严重的程序bug,这也是别人认为Java/C# 等语言比C/C++优秀的地方。
7.3 C语言操作堆内存接口(malloc、free)
堆内存释放时最简单,直接调用free释放即可。原型==>为:void free(void *ptr);
其他常用的接口原型如下:
malloc 原型==>为:void *malloc(size_t size); //size 的单位是字节
calloc 原型==>为:void *calloc(size_t nmemb, size_t size); //nmemb是单位,需要nmemb个单元,每个单元size个字节
realloc 原型==>为:void *realloc(void *ptr, size_t size); //改变原来申请的空间大小的
譬如要申请10个int元素的内存:
malloc(40);
malloc(10*sizeof(int)); //如果迁移到32位平台上int自动变化成32了,比前面那个直接定义好
calloc(10,4);
calloc(10,sizeof(int)); //同理比上面那个好
int a[10];
realloc(a,20) //数组定义时必须同时给出数组元素个数(数组大小),而且一旦定义在无法更改,堆内存申请时必须给定大小,然后一旦申请完成大小不变,如果要变只能通过realloc接口。
另外,在Java等高级语言中,有一些语法技巧可以更改数组大小,但其实这只是一种障眼法。它的工作原理是:先重新创建一个新的数组大小为要更改后的数组,然后将原数组的所有元素复制进新的数组,然后释放掉原数组,最后返回新的数组给用户。realloc的实现原理类似于上面说的Java中的可变大小的数组的方法。
7.4 堆的优势和劣势(管理大块内存、灵活、容易内存泄漏)
优势:灵活。
劣势:需要程序员去处理各种细节,说以容易出错,严重依赖于程序员的水平。
第八部分:复杂数据结构(简单介绍)
8.1 链表、哈希表、二叉树、图等
链表:
链表最重要的,链表在Liunx内核中使用非常多,驱动、应用编写很多时候都需要使用链表,所以对链表必须掌握。所以对链表必须掌握,掌握到:会自己定义结构体来实现链表、会写链表的节点插入(前插、后插)、节点删除、节点查找、节点遍历等。(至于像逆序这些很少用,掌握前面那几个这个也不难)。
如图所示:
哈希表:
哈希表不是很常用,一般不需要自己写实现,而直接使用别人实现的哈希表比较多,对我们来说最重要的是要明白哈希表的原理、从而知道哈希表的特点,从而知道什么时候该用哈希表,当看到别人用了哈希表的时候明白别人为什么要用哈希表、合适不合适?有没有更好的选择?
如图所示:
哈希表的映射(map)其实和字典的意义是一样的,通过key去更改键值,在Python的几种数据中有一个叫Dict(字典)数据类型,就是和哈希表的映射是一样的。
二叉树、图等:
对于这些复杂数据结构,不要太当回事儿。这些复杂数据结构用到的概率很小(在嵌入式开发中),其实这些数据结构被发明出来就是为了解决特定问题的,你不处理特定问题根本用不到这些,没必要去研究。
8.2 为什么需要更复杂的数据结构
因为现实中的实际问题是多种多样的。问题的复杂度不同,所以需要解决问题的算法和数据结构也不同。所以当处理什么复杂度的问题,就取研究针对性解决的数据结构和算法;当你没有遇到此类问题(或者你工作的领域根本跟这个就没关系)时就不要去管了
8.3 数据结构和算法的关系
数据结构的发明都是为了配合一定的算法;算法是为了处理具体问题,算法的实现依赖于相应的数据结构。当前我们说的算法和纯数学是不同的(算法是基于数学的,大学计算机系的研究生、博士生本科都是数学相关专业的),因为计算机算法要求以数学算法为指导,并且结合计算机本身的特点来改进和加工,最终实现一个在计算机上可以运行的算法(意思是用代码可以表示的算法)。
8.4 应该怎么学习(数据结构)这部分?
从上面表述应该明白以下事实:
第一:数据结构和算法是相辅相成的,要一起研究。
第二:数据结构和算法对嵌入式来说不全是重点,不要盲目跑去研究这个。
第三:一般在实际应用中,实现数据结构和算法的人和使用数据结构和算法的人是分开的。实际中有一部分人的工作就是研究数据结构和算法,并试图用代码来实现这些算法(表现为 库(import));其他做真正工作的人要做的就是理解、明白这些算法和数据结构的意义、优劣、特征,然后在合适的时候选择合适的数据结构和算法来解决自己碰到的实现问题。
123
123
(持续..............................................)