C语言高级专题(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;
		c = a + b;
		printf("c = %d.\n", c);
		return c;
	}			// 这个函数又重结果又重过程

【2】什么是内存

  • 内存实际上是一堆存储颗粒的集合,每一个最小的内存单位可以储存0和1的数字状态。
  • 内存是可以随机访问、并且可以读写;内存在编程中天然是用来存放变量的(就是因为有了内存,所以C语言才能定义变量,C语言中的一个变量实际就对应内存中的一个单元)。
  • 内存是用来存储可变数据的,数据在程序中表现为全局变量、局部变量等(在gcc中,其实常量也是存储在内存中的,大部分单片机中,常量是存储在flash中的,也就是在代码段),对我们写程序来说非常重要,对程序运行更是本质相关。

【3】内存的逻辑编程模型

  • 从逻辑角度来讲,内存实际上是由很多个内存单元格组成的,每个单元格有一个固定的地址叫内存地址,这个内存地址和这个内存单元格唯一对应且永久绑定。
  • 以大楼来类比内存是最合适的。逻辑上的内存就好象是一栋很大的大楼,内存的单元格就好象大楼中的一个个小房间。每个内存单元格的地址就好象每个小房间的房间号。内存中存储的内容就好象住在房间中的人一样。
  • 逻辑上来说,内存可以有无限大(因为数学上编号永远可以增加,无尽头)。但是现实中实际的内存大小是有限制的,譬如32位的系统内存限制就为4G。(32位系统指的是32位数据线,但是一般地址线也是32位,这个地址线32位决定了内存地址只能有32位二进制,所以逻辑上的大小为2的32次方)

【4】内存的基本单位

  • 内存单元的大小单位有4个:位(1bit) 字节(8bit) 半字(一般是16bit) 字(一般是32bit)
  • 在所有的计算机、所有的机器中(不管是32位系统还是16位系统还是以后的64位系统),位永远都是1bit,字节永远都是8bit。
  • 建议大家对字、半字、双字这些概念不要详细区分,只要知道这些单位具体有多少位是依赖于平台的。实际工作中在每种平台上先去搞清楚这个平台的定义(字是多少位,半字永远是字的一半,双字永远是字的2倍大小)。

一,内存编址和寻址、内存对齐


【1】内存编址方法

  • 内存在逻辑上就是一个一个的格子,这些格子可以用来装东西(里面装的东西就是内存中存储的数),每个格子有一个编号,这个编号就是内存地址,这个内存地址(一个数字)和这个格子的空间(实质是一个空间)是一一对应且永久绑定的。这就是内存的编址方法。
  • 在程序运行时,CPU实际只关心内存地址,因为硬件设计保证了按照这个地址就一定能找到这个格子,所以说内存单元的2个概念:地址和空间是内存单元的两个方面。

【2】内存编址是以字节为单位的

  • 我随便给一个数字(譬如说0x12345678),然后说这个数字是一个内存地址,然后问你这个内存地址对应的空间多大?这个大小是固定式,就是一个字节(8bit)
  • 如果把内存比喻位一栋大楼,那么这个楼里面的一个一个房间就是一个一个内存格子,这个格子的大小是固定的8bit,就好象这个大楼里面所有的房间户型是一样的。

【3】内存和数据类型的关系

  • C语言中的基本数据类型有:char short int long float double

  • int 整形(整数类型,这个整就体现在它和CPU本身的数据位宽是一样的)譬如32位的CPU,整形就是32位,int就是32位;64位的CPU,int就是64位.

  • 数据类型和内存的关系就在于:数据类型是用来定义变量的,而这些变量需要存储、运算在内存中。所以数据类型必须和内存相匹配才能获得最好的性能,否则可能不工作或者效率低下。

  • 在32位系统中定义变量最好用int,因为这样效率高。原因就在于32位的系统本身配合内存等也是32位,这样的硬件配置天生适合定义32位的int类型变量,效率最高。也能定义8位的char类型变量或者16位的short类型变量,但是实际上访问效率不高。

  • 在很多32位环境下,我们实际定义bool类型变量(实际只需要1个bit就够了)都是用int来实现bool的。也就是说我们定义一个bool b1;时,编译器实际帮我们分配了32位的内存来存储这个bool变量b1。编译器这么做实际上浪费了31位的内存,但是好处是效率高。

【4】内存对齐

  • 我们在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硬件上就是合适的,效率就高。
  • 对齐访问很配合硬件,所以效率很高;非对齐访问因为和硬件本身不搭配,所以效率不高。(因为兼容性的问题,一般硬件也都提供非对齐访问,但是效率要低很多。)

二,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里面去

【2】C语言中数据类型的本质含义是:表示一个内存格子的长度和解析方法。

  • 数据类型决定长度的含义:我们一个内存地址(比如0x30000000),本来这个地址只代表1个字节的长度,但是实际上我们可以通过给他一个类型(int),让他有了长度(4),这样这个代表内存地址的数字(0x30000000)就能表示从这个数字(0x30000000)开头的连续的n(4)个字节的内存格子了(0x30000000 + 0x30000001 + 0x30000002 + 0x30000003)。

  • 数据类型决定解析方法的含义:譬如我有一个内存地址(0x30000000),我们可以通过给这个内存地址不同的类型来指定这个内存单元格子中二进制数的解析方法。譬如我 (int)0x30000000,含义就是(0x30000000 + 0x30000001 + 0x30000002 + 0x30000003)这4个字节连起来共同存储的是一个int型数据;那么我(char)0x30000000,含义就是0x30000000 这个字节存储的是一个char型数据;

【3】用指针来间接访问内存

  • C语言中的指针,全名叫指针变量,指针变量其实很普通变量没有任何区别。譬如int a和int *p其实没有任何区别,a和p都代表一个内存地址(譬如是0x20000000),但是这个内存地址(0x20000000)的长度和解析方法不同。
  • a是int型所以a的长度是4字节,解析方法是按照int的规定来的;p是int *类型,所以长度是4字节,解析方法是int *的规定来的(0x20000000开头的连续4字节中存储了1个地址,这个地址所代表的内存单元中存放的是一个int类型的数)。
  • 总之,指针就是一个存储地址的一个普通变量

三,内存的相关概念


【1】两种结构:冯诺依曼结构和哈佛结构

  • 冯诺依曼结构是:数据和代码放在一起。(代码就是多个函数体,数据就是全局变量、局部变量)
  • 哈佛结构是:数据和代码分开存在。
  • 比如在S5PV210中运行的linux系统上,运行应用程序时:这时候所有的应用程序的代码和数据都在DRAM(动态内存),所以这种结构就是冯诺依曼结构;
  • 在单片机中,我们把程序代码烧写到Flash(NorFlash)中,然后程序在Flash中原地运行,程序中所涉及到的数据(全局变量、局部变量)不能放在Flash中,必须放在RAM(SRAM静态内存)中。 这种就叫哈佛结构。

【2】动态内存和静态内存

  • 静态内存是指在程序开始运行时由编译器分配的内存,它的分配是在程序开始编译时完成的,不占用CPU资源。程序中的各种变量,在编译时系统已经为其分配了所需的内存空间,当该变量在作用域内使用完毕时,系统会自动释放所占用的内存空间,都无须程序员自行考虑。比如定义数组…

  • 动态内存程序员自己确定使用内存空间大小,或者空间太大,栈上无法分配时,会采用动态内存分配,常用malloc申请,用完用free释放。

  • 区别
    a) 静态内存分配在编译时完成,不占用CPU资源; 动态内存分配在运行时,分配与释放都占用CPU资源。
    b) 静态内存在栈(stack)上分配; 动态内存在堆(heap)上分配。
    c) 动态内存分配需要指针和引用类型支持,静态不需要。
    d) 静态内存分配是按计划分配,由编译器负责; 动态内存分配是按需分配,由程序员负责。

【3】有OS时动态分配内存

操作系统掌握所有的硬件内存,因为内存很大,所以操作系统把内存分成1个1个的页面(其实就是一块,一般是4KB),然后以页面为单位来管理。页面内用更细小的方式来以字节为单位管理。

操作系统给我们提供了内存管理的一些接口,譬如在C语言中使用mallocfree这些接口来管理内存。

  • malloc的认知与使用:
  • malloc函数用于分配内存单元, 函数返回值的类型为vod *
  • 函数的调用形式为: malloc (size)其中参数size的类型为 unsigned int,表示需要分配的内存的字节数, 如果不能确定数据类型所占字节数,可以调用sizeof运算符来求:如 p=(int*)malloc(sizeof(int))
  • 如果系统有足够的 内存可供分配, 函数返回一个指向有size个字节的存储区首地址, 该首地址的基类型为void 类型 : 若没有足够的内存单元可供分配, 函数返回空值NULL
	//例 malloc函数应用举例
	int *p;
	float *q;
	p=(int*)malloc(4);
    q=(float*)malloc(4);
  • calloc的认知与使用:

  • 通常情况下,malloc函数用于分配单个数据类型的储存单元,但是有时候根据需求可能需要分配多个同一类型的连续的存储空间。标准C提供了calloc用于此次功能的实现,calloc的返回值类型是基类型的为void 的指针。

  • 其调用格式为 calloc (n,size); 其中参数n,size的类型都是unsigned int。calloc函数用来给n个同类型的数据项分配连续的存储空间,其中每个数据长度为size个字节。

  • 若分配成功,函数返回存储空间的首地址,否则返回空值。

  • 由calloc函数分配的存储单元,系统自动置初值0.

//例2    calloc的使用
    int *pint;
	pint=(int*) calloc (10,sizeof(int));
  • 注意:使用malloc/calloc分配动态存储单元必须用函数free释放free(p);
  • 其调用形式: free (p); 这里的指针P必须指向动态分配函数(如malloc函数)分配的地址,此函数没有返回值。

【4】没有OS时自己管理内存

  • 在没有操作系统(其实就是裸机程序)中,程序需要直接操作内存,编程者需要自己计算内存的使用和安排。如果编程者不小心把内存用错了,错误结果需要自己承担。

四,C语言中用各种数据结构来管内存

【1】用数组来管理内存

  • 数组管理内存和变量其实没有本质区别,只是符号的解析方法不同。
  • 普通变量、数组、指针变量其实都没有本质差别,都是对内存地址的解析,只是解析方法不一样。
  • 为什么要有数组?因为程序中有好多个类型相同、意义相关的变量需要管理,这时候如果用单独的变量来做程序看起来比较乱,用数组来管理会更好管理。譬如 int ages[20];
	int a;		     // 编译器分配4字节长度给a,并且把首地址和符号a绑定起来。
	int b[10];		// 编译器分配40个字节长度给b,并且把首元素首地址和符号b绑定起来。
  • 数组中第一个元素(b[0])就称为首元素;每一个元素类型都是int,所以长度都是4,其中第一个字节的地址就称为首地址;首元素b[0]的首地址就称为首元素首地址。
  • 数组定义时必须同时给出数组元素个数(数组大小),而且一旦定义再无法更改。在Java等高级语言中,有一些语法技巧可以更改数组大小,但其实这只是一种障眼法。它的工作原理是:先重新创建一个新的数组大小为要更改后的数组,然后将原数组的所有元素复制进新的数组,然后释放掉原数组,最后返回新的数组给用户;

【2】内存管理之结构体
【2.0】先分析数组的优势和缺陷

  • 优势:数组比较简单,访问用下标,可以随机访问。

  • 缺陷1:数组中所有元素类型必须相同;

  • 缺陷2 :数组大小必须定义时给出,而且一旦确定不能再改。

  • 结构体发明出来就是为了解决数组的第一个缺陷:数组中所有元素类型必须相同

	我们要管理3个学生的年龄(int类型),怎么办?
	第一种解法:用数组		int ages[3];
	第二种解法:用结构体	
	struct ages
	{
		int    age1;
		int	   age2;
		int    age3;
	};
	struct ages age;    //实例化一个结构体变量age
  • 分析总结:在这个示例中,数组要比结构体好。但是不能得出结论说数组就比结构体好,在包中元素类型不同时就只能用结构体而不能用数组了。
	struct people
	{
		int age;			// 人的年龄
		char name[20];		// 人的姓名
		int height;			// 人的身高
	};
	
	因为people的各个元素类型不完全相同,所以必须用结构体,没法用数组。

【2.1】结构体内嵌指针实现面向对象

  • 总的来说:C语言是面向过程的,但是C语言写出的linux系统是面向对象的
  • 结构体内嵌指针实现面向对象
  • 这样包含了函数指针的结构体就类似于面向对象中的class,结构体中的变量类似于class中的成员变量,结构体中的函数指针类似于class中的成员方法。
	struct s
	{
		int age;			// 普通变量
		void (*pFunc)(void);		// 函数指针,指向 void func(void)这类的函数
	};

【3】内存管理之栈(stack)

  • 栈是一种数据结构,C语言中使用栈来保存局部变量
  • 栈管理内存的特点(小内存、自动化)
先进后出 FILO   first in last out		栈       (类似一个只有一个开口的瓶子)
先进先出 FIFO   first in first out  	    队列     (类似一段水管)
  • 栈的特点是入口即出口,只有一个口,另一个口是堵死的。所以先进去的必须后出来。
  • 队列的特点是入口和出口都有,必须从入口进去,从出口出来,所以先进去的必须先出来,否则就堵住后面的。

栈的应用举例:局部变量

  • C语言中的局部变量是用栈来实现的。我们在C中定义一个局部变量时(int a),编译器会在栈中分配一段空间(4字节)给这个局部变量用(分配时栈顶指针会移动给出空间,给局部变量a用的意思就是,将这4字节的栈内存的内存地址和我们定义的局部变量名a给关联起来),对应栈的操作是入栈。

  • 注意:这里栈指针的移动和内存分配是自动的(栈自己完成,不用我们写代码去操作)。然后等我们函数退出的时候,局部变量要灭亡。对应栈的操作是弹栈(出栈)。出栈时也是栈顶指针移动将栈空间中与a关联的那4个字节空间释放。这个动作也是自动的,也不用人写代码干预。

分析一个细节:C语言中,定义局部变量时如果未初始化,则值是随机的,为什么?

  • 定义局部变量,其实就是在栈中通过移动栈指针来给程序提供一个内存空间和这个局部变量名绑定。因为这段内存空间在栈上,而栈内存是反复使用的,所以说使用栈来实现的局部变量定义时如果不显式初始化,值就可能是上次别个程序用过的值。

  • C语言是通过一个小手段来实现局部变量的初始化的。

    int a = 15;		// 局部变量定义时初始化
	                   //   C语言编译器会自动把这行转成:下面两句
	int a;			// 局部变量定义
	a = 15;			// 普通的赋值语句

栈的优点:栈管理内存,好处是方便,分配和最后回收都不用程序员操心,C语言自动完成。
栈的约束(预定栈大小不灵活,怕溢出)

  • 首先,栈是有大小的。所以栈内存大小不好设置。如果太小怕溢出,太大怕浪费内存。(这个缺点有点像数组)
  • 其次,栈的溢出危害很大,一定要避免。所以我们在C语言中定义局部变量时不能定义太多或者太大(譬如不能定义局部变量时
    int a[10000]; 使用递归来解决问题时一定要注意递归收敛)

【4】内存管理之堆

  • 堆(heap)是一种内存管理方式。内存管理对操作系统来说是一件非常复杂的事情,因为首先内存容量很大,其次内存需求在时间和大小块上没有规律(操作系统上运行着的几十、几百、几千个进程随时都会申请或者释放内存,申请或者释放的内存块大小随意)。

  • 堆这种内存管理方式特点就是自由(随时申请、释放;大小块随意)。堆内存是操作系统划归给堆管理器(操作系统中的一段代码,属于操作系统的内存管理单元)来管理的,然后向使用者提供API(malloc和free)来使用堆内存。

  • 需要内存容量比较大时,需要反复使用及释放时,很多数据结构(譬如链表)的实现都要使用堆内存。

堆管理内存的特点(大块内存、手工分配&使用&释放)

  • 特点一:容量不限(常规使用的需求容量都能满足)。
  • 特点二:申请及释放都需要手工进行,手工进行的含义就是需要程序员写代码明确进行申请malloc及释放free。如果程序员申请内存并使用后未释放,这段内存就丢失了(在堆管理器的记录中,这段内存仍然属于你这个进程,但是进程自己又以为这段内存已经不用了,再用的时候又会去申请新的内存块,这就叫吃内存),称为内存泄漏

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);		// 改变原来申请的空间的大小的

	譬如要申请10int元素的内存:
	malloc(40);			    malloc(10*sizeof(int));
	calloc(10, 4);			calloc(10, sizeof(int));

【5】复杂数据结构链表、哈希表、二叉树、图等

  • 链表是最重要的,链表在linux内核中使用非常多,驱动、应用编写很多时候都需要使用链表。所以对链表必须掌握,掌握到:会自己定义结构体来实现链表、会写链表的节点插入(前插、后插)、节点删除、节点查找、节点遍历等。(至于像逆序这些很少用,掌握了前面那几个这个也不难)。

  • 哈希表不是很常用,一般不需要自己写实现,而直接使用别人实现的哈希表比较多。对我们来说最重要的是要明白哈希表的原理、从而知道哈希表的特点,从而知道什么时候该用哈希表,当看到别人用了哈希表的时候要明白别人为什么要用哈希表、合适不合适?有没有更好的选择?

  • 二叉树、图等。对于这些复杂数据结构,不要太当回事。这些复杂数据结构用到的概率很小(在嵌入式开发中),其实这些数据结构被发明出来就是为了解决特定问题的,你不处理特定问题根本用不到这些,没必要去研究。

(1)为什么需要更复杂的数据结构

  • 因为现实中的实际问题是多种多样的,问题的复杂度不同,所以需要解决问题的算法和数据结构也不同。
  • 所以当你处理什么复杂度的问题,就去研究针对性解决的数据结构和算法;当你没有遇到此类问题(或者你工作的领域根本跟这个就没关系)时就不要去管了。

(2)数据结构和算法的关系

  • 数据结构的发明都是为了配合一定的算法;算法是为了处理具体问题,算法的实现依赖于相应的数据结构。
  • 当前我们说的算法和纯数学是不同的,因为计算机算法要求以数学算法为指导,并且结合计算机本身的特点来改进,最终实现一个在计算机上可以运行的算法(意思就是用代码可以表示的算法)。
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值