关于C语⾔中内存的理解
今天上朱⽼师的课⼜学到了很多新东西
⽼师对内存的讲解令我茅塞顿开。
加油冲冲冲!
在这⾥发个笔记记⼀下⾃⼰的学到的东西。
关于内存这个⼤话题
1.程序运⾏为什么需要内存?
1.1 计算机程序运⾏的⽬的
程序运⾏⽬的是为了去运⾏数据。
计算机程序= 代码+ 数据(经过运⾏)=结果,程序运⾏是为了得到⼀定的结果。计算机就是⽤来计算的,所有的计算机程序其实都是在做计算。计算就是在计算数据。所以计算机程序中很重要的部分就是数据。
从宏观上来理解,代码就是动作,就是加⼯数据的动作;数据就是数字,就是被代码加⼯的东西。
那么可以得出结论,程序运⾏的⽬的不外乎两个:结果、过程
⽤函数来类⽐:函数的形参就是待加⼯的数据(函数内还需要⼀些临时数据,就是局部变量)。函数本体就是代码,函数的返回值就是结果,
函数体的执⾏过程就是过程。
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;
printf("c = %d \n",c);
return c;
} //这个函数⼜重结果⼜重过程
1.2 计算机程序运⾏过程
计算机程序的运⾏过程,其实就是程序中很多个函数相继运⾏的过程。程序是由很多个函数组成的,程序的本质就是函数,函数的本质是加⼯数据的动作。1.3 冯诺依曼结构和哈佛结构
冯诺依曼结构:数据和代码放在⼀起
哈佛结构:数据和代码分开放
什么是代码?函数
什么是数据?全局变量、局部变量
在S5PV210中运⾏的linux系统上,运⾏应⽤程序时,这时候所有应⽤程序的代码和数据都在DRAM,所以这种结构就是冯诺依曼结构;在单⽚机中,我们把程序代码烧写到Flash(Norflash)中,然后程序在Flash中原地运⾏,程序中所涉及到的数据(全局变量、局部变量)不能放在Flash(只读)中,必须放在RAM(S RAM)中。这种结构就叫哈佛结构。
1.4 动态内存DRAM和静态内存SRAM
DRAM 只能将数据保存很短的时间。为了保持数据,DRAM使⽤电容存储,所以必须隔⼀段时间refresh⼀次,如果存储单元没有被刷新,存储的信息就会丢失(需要初始化)
c语言编译器怎么用?SRAM 不需要刷新电路就可以保存它内部存储的数据(不需要初始化)
1.5 为什么需要内存?
内存是⽤来存储可变数据的,数据在程序中表现为全局变量、局部变量等(在gcc中,常量也是存储在内存中的;⼤部分单⽚机中常量是存储在Flash中,也就是在代码段),对我们写程序来说⾮常重要,对程序运⾏更是本质相关。
所以内存对程序来说⼏乎是本质需求,越简单的程序需要越少的内存,越庞⼤复杂的程序需要更多的内存。内存管理是我们写程序时很重要的话题,很多编程的关键其实就是为了内存,譬如说数据结构(数据结构就是研究如何数据如何组织的)和算法(算法是为了更优秀更有效的来加⼯数据)。
1.6 如何管理内存(⽆OS时,有OS时)
有OS时:操作系统掌握所有的硬件内存,内存很⼤,所以操作系统把内存分成⼀个⼀个的页⾯(其实就是⼀块,⼀般是4kb),然后以页⾯为单位来管理。页⾯内⽤更细⼩的⽅式来以字节为单位来管理,⾮常复杂。对于使⽤操作系统的⼈来说,不需要了解这些细节。操作系统给我们提供了内存管理的⼀些接⼝,我们只需要⽤API即可管理内存。譬如在C语⾔使⽤malloc free这些接⼝来管理内存。
⽆OS时:裸机程序中程序需要直接操作内存,编程者需要⾃⼰计算内存的使⽤和安排。如果不⼩⼼把内
存⽤错了,错误结果⾃⼰承担。
再从语⾔⾓度来讲:
汇编:根本没有任何内存管理,全靠程序员⾃⼰,汇编中操作内存直接使⽤内存地址(譬如0xd0020010),⾮常⿇烦;
C语⾔:编译器帮我们管理直接内存地址,通过编译器提供的变量名等来访问内存,如需⼤量内存,可以通过API(malloc free)来访问系统内存。
c++:c++对内存的使⽤进⼀步封装。我们可以⽤new来创建对象(其实就是为对象分布内存),然后使⽤完了delete删除对象(其实就是已释放内存),但是c+ +中的内存管理还是需要程序员⾃⼰来做。如果new了忘记delete,就会造成这个对象占⽤的内存不能释放,这就是内存泄漏、
Java/c#等语⾔:这些语⾔不直接操作内存,⽽是通过虚拟机来操作内存。这样虚拟机为我们程序员的代理,来帮我们处理内存的释放⼯作。如果申请了内存忘记释放虚拟机机会帮助释放这些内存。但这个虚拟机回收内存是需要付出⼀定代价的。
所以说语⾔没有好坏,只有适⽤与不适应。当我们程序对性能⾮常在乎的时候(譬如操作系统内核)就会⽤c/c++语⾔;当我们对开发程序的速度⾮常在乎的时候,就会⽤Java/c#等语⾔。
2. 位、字节、半字、字的概念和内存位宽
2.1 什么是内存?(硬件和逻辑两个⾓度)
从硬件⾓度:内存实际上是电脑的⼀个配件(⼀般叫内存条)。根据不同的硬件实现原理还可以把内存分成SRAM和DRAM(DRAM⼜有好多代,譬如最早的S DRAM,后来的DDR1、、LPDDR)
从逻辑⾓度:内存是可以随机访问(随机访问的意思是只要给⼀个地址,就可以访问这个内存地址)、并且可以读写(逻辑上也可以限制其为只读或者只写);内存在编程中天然是⽤来存放变量的(也可以理解为就是有了内存,C语⾔才能定义变量,C语⾔中的变量实际就对应内存中的⼀个单元)。
2.2 内存的逻辑抽象图(内存的编程模型)
从逻辑⾓度来讲,内存实际上是由⽆限多个内存单元格组成的,每个单元格都有⼀个固定的地址叫内存地址,这个内存地址和这个内存单元格唯⼀对应且永久绑定。
逻辑上来说,内存可以由⽆限⼤(因为数学上编号⽆尽头),但是现实中实际内存的⼤⼩是有限制的。譬如32位系统(32位系统指的是32位数据线,但是⼀般地址线也是32位,这个地址线32位决定了内存地址只能有32位⼆进制,所以逻辑上的⼤⼩为2的32次⽅)内存限制就为4G,实际上32位系统可⽤的内存是⼩于等于4G的
2.3 位和字节
内存单元的⼤⼩单位有4个:位(1 bit)字节(8 bit )半字(⼀般是16 bit)字(⼀般是32 bit)
2.4 字和半字
由于历史原因定义⽐较混乱。单位具体有多少位依赖与平台。有些⽂档中会⽤到字这些概念。
linux+ARM上字是32位的
2.5 内存位宽(硬件和逻辑两个⾓度)
从硬件⾓度上来讲:硬件内存的实现本⾝是有宽度的,也就是说有些内存条就是8位的,⽽有些就是16位的。那么需要强调的是内存芯⽚之间可以是并联的,通过并联后即使8位的内存芯⽚也可以做出来16或32位的硬件内存。
从逻辑⾓度讲:内存位宽在逻辑上是任意的,甚⾄逻辑上存在内存位宽是24位的内存(实际上这种硬件买不到,也没有实际意义)。逻辑上来讲不管内存位宽是多少我们直接操作即可。但是因为你的操作不是纯逻辑⽽是需要硬件去执⾏的,所以不能为所欲为,实际操作很多都是受限于硬件的特性的。譬如24位的内存逻辑上和32位的内存没有任何区别,但实际硬件都是32位的,都按照32位的特性和限制来⼲活。
3. 内存编址和寻址、内存对齐
3.1 内存编址⽅法
内存在逻辑上就是⼀个⼀个的格⼦,这些格⼦可以⽤来装东西(装的是内存中存储的数),每个格⼦有⼀个编号,即内存地址,这个内存地址(⼀个数字)和格⼦的空间(实质是⼀个空间)是⼀⼀对应且永久绑定的。这就是内存的编址⽅法。
程序运⾏时,计算机中CPU实际只认识内存地址,不关⼼地址所代表的空间在哪⾥,怎么分布这些实体问题。因为硬件设计保证了按照这个地址就⼀定能到这个格⼦。
所以说内存单元的2个概念:地址和空间是内存单元的两个⽅⾯。
3.2 关键:内存编址是以字节为单位的
随便给个数字(譬如说7),这个数字是⼀个内存地址,问内存地址所对应的空间有多⼤?⼤⼩是固定的,也就是⼀个字节(8 bit)
把内存⽐作⼀栋⼤楼,每个内存格⼦就是每个房间,格⼦⼤⼩是固定的8bit,就好⽐所有房间户型⾯积⼀样。
3.3 内存和数据类型的关系
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.4 内存对齐
在C中int a;定义⼀个int型变量,在内存中就必须分配4个字节来存储a。有两种不同内存分配思路和策略:
第⼀种:0 1 2 3 对齐访问
第⼆种:1 2 3 4 或者 2 3 4 5 或者 3 4 5 6 ⾮对齐访问
内存的对齐访问不是逻辑的问题,是硬件的问题。从硬件⾓度来说,32位的内存它0 1 2 3四个单元本⾝逻辑上就有相关性,这4个字节组合起来当作⼀个int硬件上就是合适的,效率就⾼。
对齐访问很配合硬件,所以效率很⾼;⾮对齐访问因为和硬件本⾝不搭配,所以效率不⾼。(因为兼容性的问题,⼀般硬件也提供⾮对齐访问)
3.5 从内存编址看数组的意义,两者有天然契合性。
4.C语⾔如何操作内存
4.1 C语⾔对内存地址的封装(⽤变量名来访问内存、数据类型的含义、函数名的含义)
譬如在C语⾔中 int a;a = 5;a += 4; //a == 9;
结合内存来解析C语⾔中语句的本质:
int a; //编译器帮我们申请了⼀个int类型的内存格⼦(长度是4字节,地址是确定的,但是只有编译器知道),并且把符号a和这个格⼦绑定。
a = 5; // 编译器发现我们要给a赋值,就会把这个值5丢到符号a绑定的那个内存格⼦中
a =+ 4; // 编译器发现我们要给a加值,会把a原来的值读出来,然后给这个值加4,再把加之后的和写⼊a⾥⾯去
C语⾔数据类型的本质含义是:表⽰⼀个内存格⼦的长度和解析⽅法。
数据类型决定长度的含义:我们⼀个内存地址(0x300000000),本来这个地址只代表⼀个字节的长度,但是实际上我们可以通过给它⼀个类型(int),让它有了长度(4),这样这个代表内存地质的数
(0x30000000)就能表⽰从这个数字(0x30000000)开头的连续的n(4)个字节的内存格⼦(0x300000000+0x 30000001+0x30000002+0x30000003)。
数据类型决定解析⽅法的含义:譬如我们有个地址(0x300000000),我们可以通过给这个内存地址不同的类型来指定这个内存单元格⼦中⼆进制数的解析⽅法。譬如我(int)0x30000000,含义就是(0x300000000+0x30000001+0x30000002+0x30000003)这4个字节连起来共同存储的是⼀个int型数据;那么我们(float)0x30000000,含义就是(0x300000000+0x30000001+0x30000002+0x30000003)4个字节连起来共同存储的是⼀个float型数据;
之前讲过⼀个很重要的概念:内存单元格⼦的编址单位是字节。
(int *)0;(强制类型转换转换的是类型。0代表的是个内存地址。)0这个地址⾥存的是⼀个指针,这个指针指向的是⼀个int型的数
(float *)0; 0这个地址⾥存的是⼀个指针,这个指针指向的是⼀个float型的数
(short)0; 0是个变量,这个变量⾥存的是short类型的数
(char)0; 0是个变量,这个变量⾥存的是是char类型的数
C语⾔中函数就是⼀段代码的封装。函数名的实质就是这段代码的⾸地址,所以我们说函数名的实质也是⼀个内存地址。
4.2 ⽤指针来间接访问内存
关于变量类型(不管是普通变量类型int float 等,还是指针类型int * float * 等),只要记住:类型只是对后⾯数字或符号(代表的是内存地址)所表征的内存的⼀种长度规定和解析⽅法规定⽽已。
C语⾔中的指针,全名叫指针变量,其实跟普通变量没有任何区别。譬如int a和 int *p其实没有任何区别,a和 p都代表⼀个内存地址,但是这个内存地址的长度和解析⽅法不同。a是int型所以长度是4字节,解析⽅法按照int的规定来的;p是int *类型的,所以长度是4字节,解析⽅法是int *的规定来的。
4.3 指针类型的含义
4.4 ⽤数组来管理内存
数组管理内存和变量其实没有本质区别,只是符号的解析⽅法不同。(普通变量、数组、指针变量其实都没有本质差别,都是对内存地址的解析,只是解析⽅法不⼀样)。
int a; // 编译器分配4字节长度给a,并把⾸地址和符号a绑起来
int b[10]; //编译器分配40个字节长度给b,并把⾸元素⾸地址和符号b绑起来
数组第⼀个元素(b[0])就称为⾸元素;每⼀个元素类型都是int,长度都是4,其中第⼀个字节的地址就是⾸地址。⾸元素b[0]的⾸地址就称为⾸元素⾸地址。
5.内存管理之结构体
5.1 数据结构这门学问的意义
研究数据如何组织(在内存中排布),如何加⼯。
5.2 最简单的数据结构:数组
为什么要有数组?
因为程序中有多个类型相同、意义相关的变量需要管理,这时候如果⽤单独的变量来做程序看起来⽐较乱,⽤数组管理会更好。
譬如int ages[20];
5.3 数组的优势和缺陷
优势:数组⽐较简单,访问⽤下标,可以随机访问
缺陷:1.数组中所有元素类型必须相同
2.数组⼤⼩必须定义时给出,⽽且⼀旦确定不能更改
5.4 结构体隆重登场
结构体发明出来就是为了解决数组的第⼀个缺陷
譬如我们要管理3个学⽣的年龄(int型)
第⼀种解法:⽤数组 int ages[3];
第⼆种解法:⽤结构体
struct ages
{
int age1;
int age2;
int age3;
};
第⼀种⽐第⼆种好
struct people
{
int age;
char name;
int height;
}
因为people的各个元素不完全相同,所以必须⽤结构体。
5.5 题外话:结构体内嵌指针实现⾯向对象
总的来说C语⾔是⾯向过程的,但是C语⾔写出的linux系统是⾯向对象的。
⾮⾯向对象的语⾔,不⼀定不能实现⾯向对象的代码,只是很难理解。像linux内核代码。
struct s
{
int age; //普通变量
void (*pFunc)(void); //函数指针,指向void func(void)这类的函数
};
使⽤这样的结构体就可以实现⾯向对象。这样包含了函数指针的结构体及类似于⾯向对象中的class,结构体中的变量类似于class中的成员变量,结构体中的函数指针类似于class中的成员⽅法。
6. 内存管理之栈
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语⾔通过⼀个⼩⼿段来实现局部变量的初始化
int a = 15; //局部变量定义时初始化
C语⾔编译器会⾃动把这⾏转成:
int a; //局部变量定义
a = 15; //普通的赋值语句
6.4 栈的约束(预定栈⼤⼩灵活,怕溢出)
栈是有⼤⼩的,⼤⼩不好设置。太⼩怕溢出,太⼤怕浪费内存。(这个缺点有点像数组)
栈溢出危害很⼤⼀定要避免。(譬如不能定义局部变量时 int a[10000];使⽤递归来解决问题时⼀定要注意递归收敛)
7. 内存管理之堆
7.1 什么是堆?
堆(heap)是⼀种内存管理⽅式。内存管理对操作系统来说是⼀件⾮常复杂的事情。因为⾸先内存容量很⼤,其次内存需求在时间和⼤⼩块上没有规律。
这种内存管理⽅式的特点就是⾃由(随时申请,释放;⼤⼩块随意)。堆内存是系统规划给堆管理器(操作系统中的⼀段代码,属于操作系统内存管理单元)来管理的,然后向使⽤者(⽤户进程)提供API(malloc和free)来使⽤堆内存。
内存容量⽐较⼤,需要反复使⽤及释放时,很多数据结构(譬如链表)的实现都要使⽤堆内存。
7.2 堆内存管理的特点(⼤块内存、⼿⼯分配&使⽤&释放)
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论