程序运行为什么需要内存
计算机程序运行的目的
程序 = 代码 + 数据
代码就是函数,表示加工数据的动作
数据包括全局变量和局部变量,表示被加工的东西。
程序运行的目的要么重在数据结果(有返回值),要么重在过程(无返回值),要么既重视结果又重视过程。
计算机程序的运行过程
计算机程序的运行过程,其实就是程序中很多个函数相继运行的过程。程序是由很多个函数组成的,程序的本质就是函数,函数的本质就是加工数据的动作
哈佛结构和冯诺依曼结构
哈佛结构:哈佛结构就是将程序的代码和数据分开存放的一种结构,而他们存放的位置可以是相同的也可以是不同的(ROM&RAM或者RAM),总之只要是分成两个部分单独访问的结构都可以叫哈佛结构。(例如:51的程序运行时,代码放在ROM(NorFlash)中原地运行,而数据则存放在RAM中随代码动作而变动;而S5PV210程序运行时,代码和数据都在DRAM中运行,但是DRAM中又划分了代码段和数据段,二者互不干扰。)哈佛结构的特点就是代码和数据单独存放,使之不会互相干扰,进而当程序出BUG时,最多只会修改数据的值(因为代码部分是只读的,不可改写),而不会修改程序的执行顺序。因此,这种结构大量应用在嵌入式编程。
冯诺依曼结构:冯诺依曼结构是将代码和数据统一都放在RAM中,他们之间一般是按照程序的执行顺序依次存储。这样就会导致一个问题,如果程序出BUG,由于程序没有对代码段的读写限定,因此,它将拥有和数据一样的读写操作权限。于是就会很容易的死机,一旦代码执行出现一点改变就会出现非常严重的错误。但是,冯诺依曼结构的好处是可以充分利用有限的内存空间,并使CPU对程序的执行十分的方便,不用来回跑。
程序运行为什么需要内存?
对于S5PV210的程序来说,程序运行时要存放代码和数据,代码放在DRAM的只读权限代码段,数据放在DRAM的可读可写数据段,程序要跑,内存是必要条件 。
内存管理
从OS角度讲:OS掌握所有的硬件内存,因为内存很大,所以OS把内存分成1个1个的页面(其实就是分块,一般是4KB),然后以页面为单位来管理。页面内用更细小的方式来以字节为单位管理。(OS的内存管理原理复杂,对于我们使用OS的人来说,我们无需了解细节。OS为我们提供了内存管理的一些接口,我们只需要用相应的API即可管理内存。例如C语言中使用malloc、free这些接口来管理内存)。
在没有OS时,也就是裸机程序中程序需要直接操作内存,编程者需要自己计算内存的使用和安排。
从语言角度讲:不同语言提供了不同的操作内存的接口 汇编:根本没有内存管理,汇编中操作内存时直接使用内存地址(如0xd0020010) C:C语言编译器帮我们管理内存地址,我们都是通过编译器通过的变量名来访问内存的,OS下如果需要大块内存,可以通过API(mallos、free)来访问内存。裸机程序中需要大块内存需要自己定义数组等来解决。 C++:C++对内存的使用进一步封装。我们可以用new来创建对象(其实就是为对象分配内存),然后使用完了用delete来删除对象(其实就是释放内存)。所以C++比C更容易一些。但是C++中的内存管理还是要靠程序员自己来做,例如需要使用delete删除对象释放内存,如果忘记,就会造成内存不能释放,就是所谓的内存泄露。 JAVA/C#等:这些语言不直接操作内存,而是通过虚拟机来操作内存。这样虚拟机作为我们程序员的代理,来帮我们处理内存的释放工作。如果程序申请了内存,使用后忘记释放,那么虚拟机会帮我们释放。看起来,JAVA/C#比C/C++有优势,但是虚拟机回收内存的机制也是要付出一定的代价。
内存位宽
内存的逻辑抽象图
提到内存,脑中要有一张逻辑图。这张图是一行行大小相等的格子,对于32位内存来说,一行就是4个字节。CPU要访问一个int型数据,则首先取地址,这里的地址指的是int型数据单元的首地址,即4字节中的首字节的地址,然后就可以读取到这4个字节空间中所保存的数据。
内存位宽
从硬件角度:硬件的内存实现本身就是有宽度的,也就是内存条本身就有8位、16位等。需要注意的是,内存芯片之间可以并联,通过并联后8位内存芯片可以做出来16位、32位的硬件内存。
从逻辑角度:内存位宽在逻辑上是任意的,甚至逻辑上内存的位宽可以是24位,但没必要。从逻辑角度,不管内存位宽多少,直接操作即可。但因为所有的逻辑操作都是要硬件实现,所以还是要尊重硬件内存位宽。
内存编址和寻址、内存对齐
内存编址
在程序运行中,CPU实际只认识内存地址,而不关心这个地址所代表的空间在哪里、怎么分布等这些实体问题,因为硬件设计保证了这个地址就一定能找到这个格子,所以内存单元的2个概念:地址和空间是内存单元的两个方面。
关键:内存编址是以字节为单位的
内存和数据类型的关系
数据类型是用来定义变量的,而这些变量需要在内存中存储和运算。所以数据类型必须和内存相匹配才能获得最好的性能。
在32位系统中,定义变量最好使用32位的int,因为这样效率高。原因是32的数据类型配合32位的内存是可以实现32位CPU最好的性能。当定义8位的char时,CPU访问内存的效率其实是不高的。在很多情况下,我们定义的8位char变量,编译器会帮我们分配32位内存来存储这个char变量,也就是说浪费了24位的内存,但是效率高。
内存对齐
内存的对齐访问不是逻辑问题,是硬件问题。从硬件角度来说,32位的内存它0、1、2、3四个单元本身逻辑上就有相关性,这4个字节组合起来当做一个int,硬件上就是合适的,效率就高。
C语言如何操作内存
C语言对内存地址的封装
变量名即对内存地址的封装。指针即保存这个地址的变量。函数名实质就是一段代码的首地址。
C语言数据类型的本质含义:表示内存格子的个数(每个格子1个字节)和解析方法。
(1)决定内存格子的个数:如果给一个地址0x30000000,那么这个地址即一个格子。如果int定义它,这个地址就会扩展为4个格子。
(2)解析方法:(int)0x30000000含义就是从0x30000000开始的4个格子连起来共同存放的一个int型数据。(float)0x30000000含义就是从0x30000000开始的4个格子连起来共同存放的一个float型数据。
用指针来间接访问内存
关于数据类型(不管是普通变量类型int、float等,还是指针变量类型int *、float * 等),只要记住:
类型只是对后边数字或者符号(代表的都是内存地址)所表征的内存的一种长度规定和解析方法规定而已。
用数组来管理内存
数组管理内存和变量其实没有本质区别,只是符号的解析方法不用。(普通变量、数组、指针变量其实都没有本质差别,都是对内存地址的解析,只是解析方法不一样)。
int a; //编译器分配4个字节长度给a,并且把首地址和符号a绑定起来。
int b[10]; //编译器分配40个字节长度给b,并且把首元素的首地址和符号b绑定起来。
数组中第一个元素(b[0])就称为首元素;每一个元素都是类型都是int,所以长度都是4个字节,其中第一个字节的地址就称为首地址;首元素b[0]的首地址就称为首元素首地址。
内存管理之结构体
数据结构:是一门研究数据在内存中如何分布的学问。
最简单的数据结构:数组
数组的特点:类型相同、意义相关
数组的优势:数组比较简单,访问使用下标,可以随机访问(就是可以通过下标随机访问需要访问的元素)。
数组的缺陷:(1)数组中元素类型必须相同 (2)数组大小必须在定义时给出,而且一旦给出不能更改。
结构体隆重登场:
结构体发明出来就是为了解决数组的第(1)个缺陷。
结构体和数组的本质差异还是在于怎么找变量地址的问题。
题外话:结构体内嵌指针实现面向对象
C语言作为面向过程的语言,可以通过结构体内嵌指针实现面向对象的代码。
当然,面向对象的语言更为简单直观。
struct s
{
int age // 普通变量
void (*pFunc)(void); // 函数指针,指向 void func(void)这类的函数
};
使用这样的结构体就可以实现面向对象。
这样包含了函数指针的结构体就类似于面向对象中的class,结构体中的变量类似于class中的成员变量,结构体中的函数指针类似于class中的成员方法。
内存管理之栈
什么是栈(Stack)
栈是一种数据结构,C语言中使用栈来存放局部变量。
栈管理内存的特点(小内存、自动化)
先进后出 FILO(First In Last Out) 栈
先进先出 FIFO(First In First Out) 队列
栈的特点是入口即出口,只有一个口,另一个口是堵死的。所以先进去的必须后出来
队列的特点是入口和出口都有,必须从入口进,从出口出,所以先进去的必须先出来,否则就堵住后边的。
栈的应用举例:局部变量
C语言中的局部变量是用栈来实现的。
我们在C语言中定义一个局部变量时(int a),编译器会在栈中分配一段空间(4字节)给这个局部变量用(分配时栈顶指针会移动给出空间,给局部变量a用的意思就是,将这字节的栈内存的内存地址和我们定义的局部变量名a给关联起来),对应栈的操作是入栈。
注意:这里栈指针的移动和内存分配都是自动的。
然后等我们函数退出时,局部变量就会灭亡。对应栈的操作就是弹栈(出栈)。出栈时也是栈顶指针移动将栈空间中与a关联的那4个字节空间释放。这个动作也是自动的,不需要写代码干预。
栈的优点:入栈和出栈都由C语言自动完成。
分析一个细节:C语言中,定义局部变量时如果未初始化,则值是随机的,为什么?
定义局部变量,其实就是在栈中通过移动栈指针来给程序提供一个内存空间和这个局部变量名绑定。因为这段内存空间在栈上,而栈内存是反复使用的(脏的,上次用完没清零的),所以说使用栈来实现的局部变量定义时如果不显式初始化,值就是脏的(也就是随机值)。
栈的约束:预定栈大小不灵活,怕溢出
首先,栈是有大小的。所以栈内存大小不好设置。如果太小怕溢出,太大怕浪费内存。(这个缺点有点像数组)
其次,栈的溢出危害很大,一定要避免。所以我们在C语言中定义局部变量时不能定义太多或者太大(譬如不能定义局部变量时 int a[10000]; 使用递归来解决问题时一定要注意递归收敛)
内存管理之堆
什么是堆(heap)
内存管理对OS来说是一件非常复杂的事,因为首先内存容量大,其次内存需求在时间和大小块上没有规律(OS上运行着几十、几百、几千个进程随时都会申请或者释放内存,申请或者释放的内存块大小随意)。
堆这种内存管理方式特点就是自由(随时申请、释放;大小块随意)。堆内存是OS划归给堆管理器(OS中的一段代码,属于OS的内存管理单元)来管理的,然后向使用者(用户进程)提供API(malloc、free)来使用堆内存。
我们会在需要内存容量比较大,需要反复使用及释放时,会使用堆内存。很多数据结构(譬如链表)的实现都需要使用堆内存。
堆管理内存的特点(大块内存、手工分配&使用&释放)
特点1:容量不限(常规使用的需求容量都能满足)。
特点2:申请及释放都需要手工进行,手工进行的含义就是需要写代码明确申请malloc和释放free。如果申请内存并使用后未释放,这段内存就丢失了(在堆管理器的记录中,这段内存仍然属于你这个进程,但是进程自己又以为这段内存已经不用了,再用的时候又会去申请新的内存块,这就叫吃内存。),称为内存泄露。。。在C/C++语言中,内存泄露是最严重的程序bug,这也是Java/C#等语言比C/C++优秀的地方。
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中的可变大小的数组的方式。
堆的优势和劣势
(管理大块内存、灵活、容易内存泄露)
优势:灵活
劣势:需要人为处理各种细节,所以容易出错
复杂数据结构
链表、哈希表(散列表)、二叉树、图等
链表是最重要的,链表在Linux内核中使用非常多,驱动、应用编写很多时候都需要使用链表。所以对链表必须掌握。掌握到:会自己定义结构体来实现链表、会写链表的节点插入(前插、后插)、节点删除、节点查找、节点遍历等。(至于像逆序这些很少用)
哈希表不是很常用,一般不需要自己写实现,而直接使用别人实现的哈希表表较多。对我们来说,最重要的是明白哈希表的原理、从而知道哈希表的特点,从而知道什么时候该使用哈希表,当看到别人用了哈希表的时候要明白别人为什么要用哈希表、合适不合适?有没有更好的选择?
二叉树、图不用深究。
为什么需要复杂数据结构
因为现实中的实际问题是多种多样的,问题的复杂度不同,所以需要解决问题的算法和数据结构也不同。所以当你处理什么复杂度较高的问题,就去研究针对性解决的数据结构和算法。
数据结构和算法(可以用程序实现的代码)的关系
数据结构的发明都是为了配合一定的算法;算法是为了处理具体问题,算法的实现依赖于相应的数据结构。