内容整理于朱有鹏老师的课程。
1、冯诺依曼结构和哈佛结构
- 冯诺依曼结构是数据和代码放在一起,哈佛结构是数据和代码分开存在。
- 什么是代码:函数。什么是数据:全局变量、局部变量。
- 在S5PV210中运行的linux系统上,运行应用程序时,所有的应用程序的代码和数据都在DRAM,所以这种结构就是冯诺依曼结构。
- 在单片机中,程序代码烧写到Flash(NorFlash)中,然后程序在Flash中原地运行,程序中所涉及到的数据(全局变量、局部变量)放在RAM(SRAM)中。这种就叫哈佛结构。
2、动态内存DRAM和静态内存SRAM
- DRAM是动态内存,SRAM是静态内存。
3、为什么需要内存
- 内存是用来存储可变数据的,数据在程序中表现为全局变量、局部变量等(在gcc中,其实常量也是存储在内存中的)(大部分单片机中,常量是存储在flash中的,也就是在代码段),对我们写程序来说非常重要,对程序运行更是本质相关。所以内存对程序来说几乎是本质需求。
4、如何管理内存
(1)有无操作系统
- 有操作系统:操作系统掌握所有的硬件内存,因为内存很大,所以操作系统把内存分成1个1个的页面(其实就是一块,一般是4KB),然后以页面为单位来管理。页面内用更细小的方式来以字节为单位管理。操作系统内存管理的原理非常麻烦、非常复杂、非常不人性化。操作系统给我们提供了内存管理的一些接口,我们只需要用API即可管理内存。譬如在C语言中使用malloc free这些接口来管理内存。
- 无操作系统时:裸机程序中,程序需要直接操作内存,编程者需要自己计算内存的使用和安排。
- 不同的语言提供了不同的操作内存的接口。
- 譬如汇编根本没有任何内存管理,内存管理全靠程序员自己,汇编中操作内存时直接使用内存地址(譬如0xd0020010),非常麻烦;
- 譬如C语言中编译器管理直接内存地址,我们都是通过编译器提供的变量名等来访问内存的,操作系统下如果需要大块内存,可以通过API(malloc free)来访问系统内存。
- 譬如C++语言对内存的使用进一步封装。我们可以用new来创建对象(其实就是为对象分配内存),然后使用完了用delete来删除对象(其实就是释放内存)。所以C++语言对内存的管理比C要高级一些,容易一些。但是C++中内存的管理还是靠程序员自己来做。如果程序员new了一个对象,但是用完了忘记delete就会造成这个对象占用的内存不能释放,这就是内存泄漏。
- Java/C#等语言不直接操作内存,而是通过虚拟机来操作内存。这样虚拟机作为我们程序员的代理,来帮我们处理内存的释放工作。如果程序申请了内存,使用完成后忘记释放,则虚拟机会释放掉这些内存。
5、位、字节、半字、字和内存位宽
(1)什么是内存?
- 从硬件角度:内存是电脑的一个配件(内存条)。根据不同的硬件实现原理又分成SRAM和DRAM(DRAM又有好多代,譬如最早的SDRAM,后来的DDR1……LPDDR)。
- 从逻辑角度:内存可以随机访问(随机访问的意思是只要给一个地址,就可以访问这个内存地址),并且可以读写。内存是用来存放变量的(因为有了内存,所以C语言中才能定义变量,C语言中的一个变量对应内存中的一个单元)。
- 逻辑上来说,内存可以有无限大(因为数学上编号永远可以增加,无尽头)。但是实际的内存大小是有限制的,譬如32位的系统(32位系统指的是32位数据线,但是一般地址线也是32位,这个地址线32位决定了内存地址只能有32位二进制,所以逻辑上的大小为2的32次方)内存限制就为4G。
- 在所有的计算机、所有的机器中(不管是32位系统还是16位系统还是以后的64位系统),位永远都是1bit,字节永远都是8bit。
- 这些单位具体有多少位是依赖于平台的。实际工作中在每种平台上先去搞清楚这个平台的定义(字是多少位,半字永远是字的一半,双字永远是字的2倍大小)。
- 在linux+ARM这个软硬件平台上(我们嵌入式核心课的所有课程中),字是32位的。
- 从硬件角度讲:硬件内存的实现本身是有宽度的,即有些内存条是8位的,有些是16位的。内存芯片之间可以并联,通过并联后即使8位的内存芯片也可以做出来16位或32位的硬件内存。
- 从逻辑角度讲:内存位宽在逻辑上是任意的,甚至逻辑上存在内存位宽是24位的内存(但是实际上这种硬件是买不到的,也没有实际意义)。
6、内存编址和寻址、内存对齐
(1)内存编址是以字节为单位的
(2)内存和数据类型的关系
- C语言中的基本数据类型有:char short int long float double ;
- int 整形(整数类型,这个整就体现在它和CPU本身的数据位宽是一样的)譬如32位的CPU,整形就是32位,int就是32位。
- 数据类型和内存的关系就在于:数据类型是用来定义变量的,而这些变量需要存储、运算在内存中。所以数据类型必须和内存相匹配才能获得最好的性能,否则可能不工作或者效率低下。在32位系统中定义变量最好用int,因为这样效率高。原因就在于32位的系统本身配合内存等也是32位,这样的硬件配置天生适合定义32位的int类型变量,效率最高。也能定义8位的char类型变量或者16位的short类型变量,但是实际上访问效率不高。在很多32位环境下,我们实际定义bool类型变量(实际只需要1个bit就够了)都是用int来实现bool的。也就是说我们定义一个bool b1;时,编译器实际帮我们分配了32位的内存来存储这个bool变量b1。编译器这么做实际上浪费了31位的内存,但是好处是效率高。
(3)内存对齐
- 在C中int a;定义一个int类型变量,在内存中就必须分配4个字节来存储这个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硬件上就是合适的,效率就高。对齐访问很配合硬件,所以效率很高;非对齐访问因为和硬件本身不搭配,所以效率不高。(因为兼容性的问题,一般硬件也都提供非对齐访问,但是效率要低很多。)
7、C语言如何操作内存
(1)C语言对内存地址的封装
(用变量名来访问内存、数据类型的含义、函数名的含义)
-------------------------------------------------------------------------------------
譬如在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 = a + 4;编译器会先把a原来的值读出来,然后给这个值加4,再把加之后的和写入a里面去。
C语言中数据类型的本质含义是:表示一个内存格子的长度和解析方法。
数据类型决定长度的含义:我们一个内存地址(0x30000000),本来这个地址只代表1个字节的长度,但是实际上我们可以通过给他一个类型(int),让他有了长度(4),这样这个代表内存地址的数字(0x30000000)就能表示从这个数字(0x30000000)开头的连续的n(4)个字节的内存格子了(0x30000000 + 0x30000001 + 0x30000002 + 0x30000003)。
数据类型决定解析方法的含义:譬如我有一个内存地址(0x30000000),我们可以通过给这个内存地址不同的类型来指定这个内存单元格子中二进制数的解析方法。譬如我 (int)0x30000000,含义就是(0x30000000 + 0x30000001 + 0x30000002 + 0x30000003)这4个字节连起来共同存储的是一个int型数据;那么我(float)0x30000000,含义就是(0x30000000 + 0x30000001 + 0x30000002 + 0x30000003)这4个字节连起来共同存储的是一个float型数据;
之前讲过一个很重要的概念:内存单元格子的编址单位是字节。
(int *)0;
(float *)0;
(short)0;
(char)0;
int a; // int a;时编译器会自动给a分配一个内存地址,譬如说是0x12345678
(int *)a; // 等价于(int *)0x12345678
(float *)a;
C语言中,函数就是一段代码的封装。函数名的实质就是这一段代码的首地址。所以说函数名的本质也是一个内存地址。
------------------------------------------------------------------------------------------------------
(2)用指针来间接访问内存
- 类型只是对后面数字或者符号(代表的是内存地址)所表征的内存的一种长度规定和解析方法规定而已。
- C语言中的指针,全名叫指针变量,指针变量其实很普通变量没有任何区别。譬如int a和int *p其实没有任何区别,a和p都代表一个内存地址(譬如是0x20000000),但是这个内存地址(0x20000000)的长度和解析方法不同。a是int型所以a的长度是4字节,解析方法是按照int的规定来的;p是int *类型,所以长度是4字节,解析方法是int *的规定来的(0x20000000开头的连续4字节中存储了1个地址,这个地址所代表的内存单元中存放的是一个int类型的数)。
(3)用数组来管理内存
- 数组管理内存和变量其实没有本质区别,只是符号的解析方法不同。(普通变量、数组、指针变量其实都没有本质差别,都是对内存地址的解析,只是解析方法不一样)。
- int a; // 编译器分配4字节长度给a,并且把首地址和符号a绑定起来。
- int b[10]; // 编译器分配40个字节长度给b,并且把首元素首地址和符号b绑定起来。数组中第一个元素(b[0])就称为首元素;每一个元素类型都是int,所以长度都是4,其中第一个字节的地址就称为首地址;首元素b[0]的首地址就称为首元素首地址。
8、内存管理之结构体
(1)数组的优势和缺陷
- 优势:数组比较简单,访问用下标,可以随机访问。
- 缺陷:1 数组中所有元素类型必须相同;2 数组大小必须定义时给出,而且一旦确定不能再改。
- 结构体发明出来就是为了解决数组的第一个缺陷:数组中所有元素类型必须相同
- C语言是面向过程的,但是C语言写出的linux系统是面向对象的。
- 非面向对象的语言,不一定不能实现面向对象的代码。只是说用面向对象的语言来实现面向对象要更加简单一些、直观一些。用C++、Java等面向对象的语言来实现面向对象简单一些,因为语言本身帮我们做了很多事情
struct s
{int age; // 普通变量void (*pFunc)(void); // 函数指针,指向 void func(void)这类的函数
};//使用这样的结构体就可以实现面向对象。
//这样包含了函数指针的结构体就类似于面向对象中的class
///结构体中的变量类似于class中的成员变量,结构体中的函数指针类似于class中的成员方法。
9、内存管理之栈(stack)
(1)什么是栈
- 栈是一种数据结构,C语言中使用栈来保存局部变量。栈是被发明出来管理内存的。
(2)栈管理内存的特点(小内存、自动化)
- 先进后出 FILO:first in last out,栈;
- 先进先出 FIFO:first in first out,队列;
- C语言中的局部变量是用栈来实现的。
- 我们在C中定义一个局部变量时(int a),编译器会在栈中分配一段空间(4字节)给这个局部变量用(分配时栈顶指针会移动给出空间,给局部变量a用的意思就是,将这4字节的栈内存的内存地址和我们定义的局部变量名a给关联起来),对应栈的操作是入栈。函数退出的时候,局部变量要灭亡,对应栈的操作是弹栈(出栈)。出栈时也是栈顶指针移动将栈空间中与a关联的那4个字节空间释放。
- 注意这里栈指针的移动和内存分配是自动的(栈自己完成,不用我们写代码去操作)。
(4)栈的优点
- 好处是方便,分配和最后回收都不用程序员操心,C语言自动完成。
- C语言中,定义局部变量时如果未初始化,则值是随机的,为什么?定义局部变量,其实就是在栈中通过移动栈指针来给程序提供一个内存空间和这个局部变量名绑定。因为这段内存空间在栈上,而栈内存是反复使用的(脏的,上次用完没清零的),所以说使用栈来实现的局部变量定义时如果不显式初始化,值就是脏的。
- 如果你显式初始化怎么样?C语言是通过一个小手段来实现局部变量的初始化的,即:
- int a = 15;// 局部变量定义时初始化
- C语言编译器会自动把这行转成:i
- nt a;// 局部变量定义
- a = 15;// 普通的赋值语句
- 栈是有大小的。所以栈内存大小不好设置。如果太小怕溢出,太大怕浪费内存。(这个缺点有点像数组);
- 栈的溢出危害很大,一定要避免。
- 我们在C语言中定义局部变量时不能定义太多或者太大(譬如不能定义局部变量时 int a[10000]; 使用递归来解决问题时一定要注意递归收敛)
10、内存管理之堆
(1)什么是堆- 堆(heap)是一种内存管理方式。
- 内存管理对操作系统来说是一件非常复杂的事情,因为首先内存容量很大,其次内存需求在时间和大小块上没有规律(操作系统上运行着的几十、几百、几千个进程随时都会申请或者释放内存,申请或者释放的内存块大小随意)。
- 堆这种内存管理方式特点就是自由(随时申请、释放;大小块随意)。
- 堆内存是操作系统划归给堆管理器(操作系统中的一段代码,属于操作系统的内存管理单元)来管理的,然后向使用者(用户进程)提供API(malloc和free)来使用堆内存。
- 什么时候使用堆内存?需要内存容量比较大时,需要反复使用及释放时,很多数据结构(譬如链表)的实现都要使用堆内存。
(2)堆管理内存的特点
- 特点一:容量不限(常规使用的需求容量都能满足)。
- 特点二:申请及释放都需要手工进行,手工进行的含义就是需要程序员写代码明确进行申请malloc及释放free。如果程序员申请内存并使用后未释放,这段内存就丢失了(在堆管理器的记录中,这段内存仍然属于你这个进程,但是进程自己又以为这段内存已经不用了,再用的时候又会去申请新的内存块,这就叫吃内存),称为内存泄漏。在C/C++语言中,内存泄漏是最严重的程序bug,这也是别人认为Java/C#等语言比C/C++优秀的地方。
(3)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中的可变大小的数组的方式。
- 管理大块内存、灵活、容易内存泄漏。
11、复杂数据结构
(1)链表、哈希表、二叉树、图等
- 链表在linux内核中使用非常多,驱动、应用编写很多时候都需要使用链表。所以对链表必须掌握,掌握到:会自己定义结构体来实现链表、会写链表的节点插入(前插、后插)、节点删除、节点查找、节点遍历等。(至于像逆序这些很少用,掌握了前面那几个这个也不难)。
- 哈希表不是很常用,一般不需要自己写实现,而直接使用别人实现的哈希表比较多。对我们来说最重要的是要明白哈希表的原理、从而知道哈希表的特点,从而知道什么时候该用哈希表,当看到别人用了哈希表的时候要明白别人为什么要用哈希表、合适不合适?有没有更好的选择?
- 二叉树、图等。对于这些复杂数据结构,不要太当回事。这些复杂数据结构用到的概率很小(在嵌入式开发中),其实这些数据结构被发明出来就是为了解决特定问题的,你不处理特定问题根本用不到这些,没必要去研究。
- 在实际应用中,实现数据结构和算法的人和使用数据结构和算法的人是分开的。实际中有一部分人的工作就是研究数据结构和算法,并且试图用代码来实现这些算法(表现为库);其他做真正工作的人要做的就是理解、明白这些算法和数据结构的意义、优劣、特征,然后在合适的时候选择合适的数据结构和算法来解决自己碰到的实际问题。