C语言内存朱有鹏老师_第1页
C语言内存朱有鹏老师_第2页
C语言内存朱有鹏老师_第3页
C语言内存朱有鹏老师_第4页
C语言内存朱有鹏老师_第5页
已阅读5页,还剩11页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、4.C语言专题精讲篇- 4.1.内存这个大话题第一部分、章节目录4.1.1.程序运行为什么需要内存14.1.2.程序运行为什么需要内存24.1.3.位、字节、半字、字的概念和内存位宽4.1.4.内存编址和寻址、内存对齐4.1.5.C语言如何操作内存4.1.6.内存管理之结构体4.1.7、内存管理之栈4.1.8、内存管理之堆4.1.9、复杂数据结构第二部分、章节介绍4.1.1.程序运行为什么需要内存1本节从本质上分析了计算机程序在计算机中是如何运行的,通过冯诺依曼结构和哈佛结构的对比,让大家对代码和数据之分有了清楚的认识。这些认识有助于你对程序运行过程的分析,从而保证将来写出优秀的程序代码。4.

2、1.2.程序运行为什么需要内存2本节从本质上分析了计算机程序在计算机中是如何运行的,通过冯诺依曼结构和哈佛结构的对比,让大家对代码和数据之分有了清楚的认识。这些认识有助于你对程序运行过程的分析,从而保证将来写出优秀的程序代码。4.1.3.位、字节、半字、字的概念和内存位宽本节从逻辑上阐述内存的编程模型和逻辑认识,并且解释了内存单元的几个单位:位、字节、半字、字。通过本节学习希望大家从逻辑上对内存有一个认知,先建立起来大的框架性概念。4.1.4.内存编址和寻址、内存对齐本节重点讲述内存单元格和其地址的对应关系,同时讲了内存对齐的意义和重要性,试图带领大家对内存从逻辑和现实两个角度深入理解,以为后

3、面的深入分析C语言特性打下基础。4.1.5.C语言如何操作内存本节主要讲C语言语法中对内存的使用,包括:变量定义、指针、数组等C语言基本语法,讲述这些语法和内存之间的内在联系,试图引导大家从内存的角度来理解这些语法特性。4.1.6.内存管理之结构体本节首先讲述数据结构的概念和意义,然后从数组讲起,使用数组的缺陷引出结构体,目的在于让大家明白结构体这种简单数据结构的内在,最后讲了通过结构体内嵌指针来实现面向对象,这是linux内核中常见的一种语法技巧。4.1.7.内存管理之栈4.1.8.内存管理之堆4.1.9、复杂数据结构第三部分、随堂记录4.1.1.程序运行为什么需要内存4.1.1.1、计算机

4、程序运行的目的计算机为什么需要编程?编程已经编了很多年,已经写了很多程序,为什么还需要另外写程序?计算机有这个新的程序到底为了什么?程序的目的是为了去运行,程序运行是为了得到一定的结果。计算机就是用来计算的,所有的计算机程序其实都是在做计算。计算就是在计算数据。所以计算机程序中很重要的部分就是数据。计算机程序 = 代码 + 数据计算机程序运行完得到一个结果,就是说代码 + 数据 (经过运行后) = 结果从宏观上来理解,代码就是动作,就是加工数据的动作;数据就是数字,就是被代码所加工的东西。那么可以得出结论:程序运行的目的不外乎2个:结果、过程用函数来类比:函数的形参就是待加工的数据(函数内还需

5、要一些临时数据,就是局部变量),函数本体就是代码,函数的返回值就是结果,函数体的执行过程就是过程。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;/ 这个函数又重结果又重过程4.1.

6、1.2、计算机程序运行过程计算机程序的运行过程,其实就是程序中很多个函数相继运行的过程。程序是由很多个函数组成的,程序的本质就是函数,函数的本质是加工数据的动作。4.1.1.3、冯诺依曼结构和哈佛结构冯诺依曼结构是:数据和代码放在一起。哈佛结构是:数据和代码分开存在。什么是代码:函数什么是数据:全局变量、局部变量在S5PV210中运行的linux系统上,运行应用程序时:这时候所有的应用程序的代码和数据都在DRAM,所以这种结构就是冯诺依曼结构;在单片机中,我们把程序代码烧写到Flash(NorFlash)中,然后程序在Flash中原地运行,程序中所涉及到的数据(全局变量、局部变量)不能放在Fl

7、ash中,必须放在RAM(SRAM)中。这种就叫哈佛结构。4.1.1.4、动态内存DRAM和静态内存SRAMDRAM是动态内存,SRAM是静态内存。详细细节自己baidu4.1.1.5、总结:为什么需要内存呢内存是用来存储可变数据的,数据在程序中表现为全局变量、局部变量等(在gcc中,其实常量也是存储在内存中的)(大部分单片机中,常量是存储在flash中的,也就是在代码段),对我们写程序来说非常重要,对程序运行更是本质相关。所以内存对程序来说几乎是本质需求。越简单的程序需要越少的内存,而越庞大越复杂的程序需要更多的内存。内存管理是我们写程序时很重要的话题。我们以前学过的了解过的很多编程的关键其

8、实都是为了内存,譬如说数据结构(数据结构是研究数据如何组织的,数据是放在内存中的)和算法(算法是为了用更优秀更有效的方法来加工数据,既然跟数据有关就离不开内存)。4.1.1.6、深入思考:如何管理内存(无OS时,有OS时)对于计算机来说,内存容量越大则可能性越大,所以大家都希望自己的电脑内存更大。我们写程序时如何管理内存就成了很大的问题。如果管理不善,可能会造成程序运行消耗过多的内存,这样迟早内存都被你这个程序吃光了,当没有内存可用时程序就会崩溃。所以内存对程序来说是一种资源,所以管理内存对程序来说是一个重要技术和话题。先从操作系统角度讲:操作系统掌握所有的硬件内存,因为内存很大,所以操作系统

9、把内存分成1个1个的页面(其实就是一块,一般是4KB),然后以页面为单位来管理。页面内用更细小的方式来以字节为单位管理。操作系统内存管理的原理非常麻烦、非常复杂、非常不人性化。那么对我们这些使用操作系统的人来说,其实不需要了解这些细节。操作系统给我们提供了内存管理的一些接口,我们只需要用API即可管理内存。譬如在C语言中使用malloc free这些接口来管理内存。没有操作系统时:在没有操作系统(其实就是裸机程序)中,程序需要直接操作内存,编程者需要自己计算内存的使用和安排。如果编程者不小心把内存用错了,错误结果需要自己承担。再从语言角度来讲:不同的语言提供了不同的操作内存的接口。譬如汇编:根

10、本没有任何内存管理,内存管理全靠程序员自己,汇编中操作内存时直接使用内存地址(譬如0xd0020010),非常麻烦;譬如C语言:C语言中编译器帮我们管理直接内存地址,我们都是通过编译器提供的变量名等来访问内存的,操作系统下如果需要大块内存,可以通过API(malloc free)来访问系统内存。裸机程序中需要大块的内存需要自己来定义数组等来解决。譬如C+语言:C+语言对内存的使用进一步封装。我们可以用new来创建对象(其实就是为对象分配内存),然后使用完了用delete来删除对象(其实就是释放内存)。所以C+语言对内存的管理比C要高级一些,容易一些。但是C+中内存的管理还是靠程序员自己来做。如

11、果程序员new了一个对象,但是用完了忘记delete就会造成这个对象占用的内存不能释放,这就是内存泄漏。Java/C#等语言:这些语言不直接操作内存,而是通过虚拟机来操作内存。这样虚拟机作为我们程序员的代理,来帮我们处理内存的释放工作。如果我的程序申请了内存,使用完成后忘记释放,则虚拟机会帮我释放掉这些内存。听起来似乎C# java等语言比C/C+有优势,但是其实他这个虚拟机回收内存是需要付出一定代价的,所以说语言没有好坏,只有适应不适应。当我们程序对性能非常在乎的时候(譬如操作系统内核)就会用C/C+语言;当我们对开发程序的速度非常在乎的时候,就会用Java/C#等语言。4.1.3.位、字节

12、、半字、字的概念和内存位宽4.1.3.1、什么是内存?(硬件和逻辑两个角度)从硬件角度:内存实际上是电脑的一个配件(一般叫内存条)。根据不同的硬件实现原理还可以把内存分成SRAM和DRAM(DRAM又有好多代,譬如最早的SDRAM,后来的DDR1、DDR2·····、LPDDR)从逻辑角度:内存是这样一种东西,它可以随机访问(随机访问的意思是只要给一个地址,就可以访问这个内存地址)、并且可以读写(当然了逻辑上也可以限制其为只读或者只写);内存在编程中天然是用来存放变量的(就是因为有了内存,所以C语言才能定义变量,C语言中的一个变量实际就对应内存

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

14、了内存地址只能有32位二进制,所以逻辑上的大小为2的32次方)内存限制就为4G。实际上32位的系统中可用的内存是小于等于4G的(譬如我32位CPU装32位windows,但实际电脑只有512M内存)4.1.3.3、位和字节内存单元的大小单位有4个:位(1bit) 字节(8bit) 半字(一般是16bit) 字(一般是32bit)在所有的计算机、所有的机器中(不管是32位系统还是16位系统还是以后的64位系统),位永远都是1bit,字节永远都是8bit。4.1.3.4、字和半字历史上曾经出现过16位系统、32位系统、64位系统三种,而且操作系统还有windows、linux、iOS等很多,所以很

15、多的概念在历史上曾经被混乱的定义过。建议大家对字、半字、双字这些概念不要详细区分,只要知道这些单位具体有多少位是依赖于平台的。实际工作中在每种平台上先去搞清楚这个平台的定义(字是多少位,半字永远是字的一半,双字永远是字的2倍大小)。编程时一般根本用不到字这个概念,那我们区分这个概念主要是因为有些文档中会用到这些概念,如果不加区别可能会造成你对程序的误解。在linux+ARM这个软硬件平台上(我们嵌入式核心课的所有课程中),字是32位的。4.1.3.5、内存位宽(硬件和逻辑两个角度)从硬件角度讲:硬件内存的实现本身是有宽度的,也就是说有些内存条就是8位的,而有些就是16位的。那么需要强调的是内存

16、芯片之间是可以并联的,通过并联后即使8位的内存芯片也可以做出来16位或32位的硬件内存。从逻辑角度讲:内存位宽在逻辑上是任意的,甚至逻辑上存在内存位宽是24位的内存(但是实际上这种硬件是买不到的,也没有实际意义)。从逻辑角度来讲不管内存位宽是多少,我就直接操作即可,对我的操作不构成影响。但是因为你的操作不是纯逻辑而是需要硬件去执行的,所以不能为所欲为,所以我们实际的很多操作都是受限于硬件的特性的。譬如24位的内存逻辑上和32位的内存没有任何区别,但实际硬件都是32位的,都要按照32位硬件的特性和限制来干活。4.1.4.内存编址和寻址、内存对齐4.1.4.1、内存编址方法内存在逻辑上就是一个一个

17、的格子,这些格子可以用来装东西(里面装的东西就是内存中存储的数),每个格子有一个编号,这个编号就是内存地址,这个内存地址(一个数字)和这个格子的空间(实质是一个空间)是一一对应且永久绑定的。这就是内存的编址方法。在程序运行时,计算机中CPU实际只认识内存地址,而不关心这个地址所代表的空间在哪里,怎么分布这些实体问题。因为硬件设计保证了按照这个地址就一定能找到这个格子,所以说内存单元的2个概念:地址和空间是内存单元的两个方面。4.1.4.2、关键:内存编址是以字节为单位的我随便给一个数字(譬如说7),然后说这个数字是一个内存地址,然后问你这个内存地址对应的空间多大?这个大小是固定式,就是一个字节

18、(8bit)。如果把内存比喻位一栋大楼,那么这个楼里面的一个一个房间就是一个一个内存格子,这个格子的大小是固定的8bit,就好象这个大楼里面所有的房间户型是一样的。4.1.4.3、内存和数据类型的关系C语言中的基本数据类型有:char short int long float double int 整形(整数类型,这个整就体现在它和CPU本身的数据位宽是一样的)譬如32位的CPU,整形就是32位,int就是32位。数据类型和内存的关系就在于:数据类型是用来定义变量的,而这些变量需要存储、运算在内存中。所以数据类型必须和内存相匹配才能获得最好的性能,否则可能不工作或者效率低下。在32位系统中定义

19、变量最好用int,因为这样效率高。原因就在于32位的系统本身配合内存等也是32位,这样的硬件配置天生适合定义32位的int类型变量,效率最高。也能定义8位的char类型变量或者16位的short类型变量,但是实际上访问效率不高。在很多32位环境下,我们实际定义bool类型变量(实际只需要1个bit就够了)都是用int来实现bool的。也就是说我们定义一个bool b1;时,编译器实际帮我们分配了32位的内存来存储这个bool变量b1。编译器这么做实际上浪费了31位的内存,但是好处是效率高。问题:实际编程时要以省内存为大还是要以运行效率为重?答案是不定的,看具体情况。很多年前内存很贵机器上内存都

20、很少,那时候写代码以省内存为主。现在随着半导体技术的发展内存变得很便宜了,现在的机器都是高配,不在乎省一点内存,而效率和用户体验变成了关键。所以现在写程序大部分都是以效率为重。4.1.4.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硬件上就是合适的,效率

21、就高。对齐访问很配合硬件,所以效率很高;非对齐访问因为和硬件本身不搭配,所以效率不高。(因为兼容性的问题,一般硬件也都提供非对齐访问,但是效率要低很多。)4.1.4.5、从内存编址看数组的意义4.1.5.C语言如何操作内存4.1.5.1、C语言对内存地址的封装(用变量名来访问内存、数据类型的含义、函数名的含义)譬如在C语言中 int a; a = 5; a += 4;/ a = 9;结合内存来解析C语言语句的本质:int a;/ 编译器帮我们申请了1个int类型的内存格子(长度是4字节,地址是确定的,但是只有编译器知道,我们是不知道的,也不需要知道。),并且把符号a和这个格子绑定。a = 5;

22、/ 编译器发现我们要给a赋值,就会把这个值5丢到符号a绑定的那个内存格子中。a += 4;/ 编译器发现我们要给a加值,a += 4 等效于 a = a + 4;编译器会先把a原来的值读出来,然后给这个值加4,再把加之后的和写入a里面去。C语言中数据类型的本质含义是:表示一个内存格子的长度和解析方法。数据类型决定长度的含义:我们一个内存地址(0x30000000),本来这个地址只代表1个字节的长度,但是实际上我们可以通过给他一个类型(int),让他有了长度(4),这样这个代表内存地址的数字(0x30000000)就能表示从这个数字(0x30000000)开头的连续的n(4)个字节的内存格子了(

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

24、3)这4个字节连起来共同存储的是一个float型数据;之前讲过一个很重要的概念:内存单元格子的编址单位是字节。(int *)0;(float *)0;(short)0;(char)0;int a;/ int a;时编译器会自动给a分配一个内存地址,譬如说是0x12345678(int *)a;/ 等价于(int *)0x12345678(float *)a;C语言中,函数就是一段代码的封装。函数名的实质就是这一段代码的首地址。所以说函数名的本质也是一个内存地址。4.1.5.2、用指针来间接访问内存关于类型(不管是普通变量类型int float等,还是指针类型int * float *等),只要

25、记住:类型只是对后面数字或者符号(代表的是内存地址)所表征的内存的一种长度规定和解析方法规定而已。C语言中的指针,全名叫指针变量,指针变量其实很普通变量没有任何区别。譬如int a和int *p其实没有任何区别,a和p都代表一个内存地址(譬如是0x20000000),但是这个内存地址(0x20000000)的长度和解析方法不同。a是int型所以a的长度是4字节,解析方法是按照int的规定来的;p是int *类型,所以长度是4字节,解析方法是int *的规定来的(0x20000000开头的连续4字节中存储了1个地址,这个地址所代表的内存单元中存放的是一个int类型的数)。4.1.5.3、指针类型

26、的含义4.1.5.4、用数组来管理内存数组管理内存和变量其实没有本质区别,只是符号的解析方法不同。(普通变量、数组、指针变量其实都没有本质差别,都是对内存地址的解析,只是解析方法不一样)。int a;/ 编译器分配4字节长度给a,并且把首地址和符号a绑定起来。int b10;/ 编译器分配40个字节长度给b,并且把首元素首地址和符号b绑定起来。数组中第一个元素(a0)就称为首元素;每一个元素类型都是int,所以长度都是4,其中第一个字节的地址就称为首地址;首元素a0的首地址就称为首元素首地址。4.1.6.内存管理之结构体4.1.6.1、数据结构这门学问的意义数据结构就是研究数据如何组织(在内存

27、中排布),如何加工的学问。4.1.6.2、最简单的数据结构:数组为什么要有数组?因为程序中有好多个类型相同、意义相关的变量需要管理,这时候如果用单独的变量来做程序看起来比较乱,用数组来管理会更好管理。譬如 int ages20;4.1.6.3、数组的优势和缺陷优势:数组比较简单,访问用下标,可以随机访问。缺陷:1 数组中所有元素类型必须相同;2 数组大小必须定义时给出,而且一旦确定不能再改。4.1.6.4、结构体隆重登场结构体发明出来就是为了解决数组的第一个缺陷:数组中所有元素类型必须相同我们要管理3个学生的年龄(int类型),怎么办?第一种解法:用数组int ages3;第二种解法:用结构体

28、struct agesint age1;intage2;int age3;struct ages age;分析总结:在这个示例中,数组要比结构体好。但是不能得出结论说数组就比结构体好,在包中元素类型不同时就只能用结构体而不能用数组了。struct peopleint age;/ 人的年龄char name20;/ 人的姓名int height;/ 人的身高;因为people的各个元素类型不完全相同,所以必须用结构体,没法用数组。4.1.6.5、题外话:结构体内嵌指针实现面向对象面向过程与面向对象。总的来说:C语言是面向过程的,但是C语言写出的linux系统是面向对象的。非面向对象的语言,不一定

29、不能实现面向对象的代码。只是说用面向对象的语言来实现面向对象要更加简单一些、直观一些、无脑一些。用C+、Java等面向对象的语言来实现面向对象简单一些,因为语言本身帮我们做了很多事情;但是用C来实现面向对象很麻烦,看起来也不容易理解,这就是为什么大多数人学过C语言却看不懂linux内核代码的原因。struct sint age;/ 普通变量void (*pFunc)(void);/ 函数指针,指向 void func(void)这类的函数;使用这样的结构体就可以实现面向对象。这样包含了函数指针的结构体就类似于面向对象中的class,结构体中的变量类似于class中的成员变量,结构体中的函数指针

30、类似于class中的成员方法。4.1.7、内存管理之栈(stack)4.1.7.1、什么是栈栈是一种数据结构,C语言中使用栈来保存局部变量。栈是被发明出来管理内存的。1.4.7.2、栈管理内存的特点(小内存、自动化)先进后出 FILOfirst in last out栈先进先出 FIFO first in first out 队列栈的特点是入口即出口,只有一个口,另一个口是堵死的。所以先进去的必须后出来。队列的特点是入口和出口都有,必须从入口进去,从出口出来,所以先进去的必须先出来,否则就堵住后面的。1.4.7.3、栈的应用举例:局部变量C语言中的局部变量是用栈来实现的。我们在C中定义一个局部

31、变量时(int a),编译器会在栈中分配一段空间(4字节)给这个局部变量用(分配时栈顶指针会移动给出空间,给局部变量a用的意思就是,将这4字节的栈内存的内存地址和我们定义的局部变量名a给关联起来),对应栈的操作是入栈。注意:这里栈指针的移动和内存分配是自动的(栈自己完成,不用我们写代码去操作)。然后等我们函数退出的时候,局部变量要灭亡。对应栈的操作是弹栈(出栈)。出栈时也是栈顶指针移动将栈空间中与a关联的那4个字节空间释放。这个动作也是自动的,也不用人写代码干预。栈的优点:栈管理内存,好处是方便,分配和最后回收都不用程序员操心,C语言自动完成。分析一个细节:C语言中,定义局部变量时如果未初始化

32、,则值是随机的,为什么?定义局部变量,其实就是在栈中通过移动栈指针来给程序提供一个内存空间和这个局部变量名绑定。因为这段内存空间在栈上,而栈内存是反复使用的(脏的,上次用完没清零的),所以说使用栈来实现的局部变量定义时如果不显式初始化,值就是脏的。如果你显式初始化怎么样?C语言是通过一个小手段来实现局部变量的初始化的。int a = 15;/ 局部变量定义时初始化C语言编译器会自动把这行转成:int a;/ 局部变量定义a = 15;/ 普通的赋值语句1.4.7.4、栈的约束(预定栈大小不灵活,怕溢出)首先,栈是有大小的。所以栈内存大小不好设置。如果太小怕溢出,太大怕浪费内存。(这个缺点有点像

33、数组)其次,栈的溢出危害很大,一定要避免。所以我们在C语言中定义局部变量时不能定义太多或者太大(譬如不能定义局部变量时 int a10000; 使用递归来解决问题时一定要注意递归收敛)4.1.8、内存管理之堆4.1.8.1、什么是堆堆(heap)是一种内存管理方式。内存管理对操作系统来说是一件非常复杂的事情,因为首先内存容量很大,其次内存需求在时间和大小块上没有规律(操作系统上运行着的几十、几百、几千个进程随时都会申请或者释放内存,申请或者释放的内存块大小随意)。堆这种内存管理方式特点就是自由(随时申请、释放;大小块随意)。堆内存是操作系统划归给堆管理器(操作系统中的一段代码,属于操作系统的内

34、存管理单元)来管理的,然后向使用者(用户进程)提供API(malloc和free)来使用堆内存。我们什么时候使用堆内存?需要内存容量比较大时,需要反复使用及释放时,很多数据结构(譬如链表)的实现都要使用堆内存。4.1.8.2、堆管理内存的特点(大块内存、手工分配&使用&释放)特点一:容量不限(常规使用的需求容量都能满足)。特点二:申请及释放都需要手工进行,手工进行的含义就是需要程序员写代码明确进行申请malloc及释放free。如果程序员申请内存并使用后未释放,这段内存就丢失了(在堆管理器的记录中,这段内存仍然属于你这个进程,但是进程自己又以为这段内存已经不用了,再用的时候又会

35、去申请新的内存块,这就叫吃内存),称为内存泄漏。在C/C+语言中,内存泄漏是最严重的程序bug,这也是别人认为Java/C#等语言比C/C+优秀的地方。4.1.8.3、C语言操作堆内存的接口(malloc free)堆内存释放时最简单,直接调用free释放即可。void free(void *ptr);堆内存申请时,有3个可选择的类似功能的函数:malloc, calloc, reallocvoid *malloc(size_t size);void *calloc(size_t nmemb, size_t size);/ nmemb个单元,每个单元size字节void *realloc(vo

36、id *ptr, size_t size);/ 改变原来申请的空间的大小的譬如要申请10个int元素的内存:malloc(40);malloc(10*sizeof(int);calloc(10, 4);calloc(10, sizeof(int);数组定义时必须同时给出数组元素个数(数组大小),而且一旦定义再无法更改。在Java等高级语言中,有一些语法技巧可以更改数组大小,但其实这只是一种障眼法。它的工作原理是:先重新创建一个新的数组大小为要更改后的数组,然后将原数组的所有元素复制进新的数组,然后释放掉原数组,最后返回新的数组给用户;堆内存申请时必须给定大小,然后一旦申请完成大小不变,如果要变

37、只能通过realloc接口。realloc的实现原理类似于上面说的Java中的可变大小的数组的方式。4.1.8.4、堆的优势和劣势(管理大块内存、灵活、容易内存泄漏)优势:灵活;劣势:需要程序员去处理各种细节,所以容易出错,严重依赖于程序员的水平。4.1.9、复杂数据结构4.1.9.1、链表、哈希表、二叉树、图等链表是最重要的,链表在linux内核中使用非常多,驱动、应用编写很多时候都需要使用链表。所以对链表必须掌握,掌握到:会自己定义结构体来实现链表、会写链表的节点插入(前插、后插)、节点删除、节点查找、节点遍历等。(至于像逆序这些很少用,掌握了前面那几个这个也不难)。哈希表不是很常用,一般

38、不需要自己写实现,而直接使用别人实现的哈希表比较多。对我们来说最重要的是要明白哈希表的原理、从而知道哈希表的特点,从而知道什么时候该用哈希表,当看到别人用了哈希表的时候要明白别人为什么要用哈希表、合适不合适?有没有更好的选择?二叉树、图等。对于这些复杂数据结构,不要太当回事。这些复杂数据结构用到的概率很小(在嵌入式开发中),其实这些数据结构被发明出来就是为了解决特定问题的,你不处理特定问题根本用不到这些,没必要去研究。4.1.9.2、为什么需要更复杂的数据结构因为现实中的实际问题是多种多样的,问题的复杂度不同,所以需要解决问题的算法和数据结构也不同。所以当你处理什么复杂度的问题,就去研究针对性

39、解决的数据结构和算法;当你没有遇到此类问题(或者你工作的领域根本跟这个就没关系)时就不要去管了。4.1.9.3、数据结构和算法的关系数据结构的发明都是为了配合一定的算法;算法是为了处理具体问题,算法的实现依赖于相应的数据结构。当前我们说的算法和纯数学是不同的(算法是基于数学的,大学计算机系研究生博士生很多本科都是数学相关专业的),因为计算机算法要求以数学算法为指导,并且结合计算机本身的特点来改进,最终实现一个在计算机上可以运行的算法(意思就是用代码可以表示的算法)。4.1.9.4、应该怎样学习这部分?从上面表述大家应该明白以下事实:1. 数据结构和算法是相辅相成的,要一起研究。2. 数据结构和

40、算法对嵌入式来说不全是重点,不要盲目的跑去研究这个。3. 一般在实际应用中,实现数据结构和算法的人和使用数据结构和算法的人是分开的。实际中有一部分人的工作就是研究数据结构和算法,并且试图用代码来实现这些算法(表现为库);其他做真正工作的人要做的就是理解、明白这些算法和数据结构的意义、优劣、特征,然后在合适的时候选择合适的数据结构和算法来解决自己碰到的实际问题。举个例子:linux内核在字符设备驱动管理时,使用了哈希表(hash table,散列表)。所以字符设备驱动的很多特点都和哈希表的特点有关。4.2.C语言位操作第一部分、章节目录4.2.1.常用位操作符4.2.2.位与位或位异或在操作寄存

41、器时的特殊作用4.2.3.如何用位运算构建特定二进制数4.2.4.位运算实战演练14.2.5.位运算实战演练24.2.6.技术升级:用宏定义来完成位运算第二部分、章节介绍4.2.1.常用位操作符本节讲解C语言中常用的位操作符,如位与、位或、位取反、位异或、左移右移等。目的在于让大家系统学习各种位操作的操作符及真值表。4.2.2.位与位或位异或在操作寄存器时的特殊作用本节首先讲解寄存器位设置时的特点和需求,然后重点讲解了位与、位或、位异或等操作及其在寄存器设置中的具体作用,并用实例进行演示和验证。4.2.3.如何用位运算构建特定二进制数本节主要讲解了左移、右移以及位取反符号,以及如何用这三个符号

42、构建特定的二进制数。这种方式是最常见的设置寄存器的方式,而不是像上节中直接给出二进制数的方式。4.2.4.位运算实战演练1本节用6个实际案例来演示如果用位运算符完成一定运算,这些示例都是流行的面试题目中位运算部分的,对大家掌握位运算有很大帮助。4.2.5.位运算实战演练2本节用2个实际案例来演示如果用位运算符完成一定运算,这些示例都是流行的面试题目中位运算部分的,对大家掌握位运算有很大帮助。4.2.6.技术升级:用宏定义来完成位运算本节讲述位运算中难度最大的部分,即用宏定义来描述位运算。在linux内核中有很多类似的宏,分布在各个角落,搞清楚这些宏的实现可以帮助我们提升C语言水平,同时为以后研

43、究linux内核源码扫清障碍。第三部分、随堂记录4.2.1.位操作符4.2.1.1、位与&(1)注意:位与符号是一个&,两个&&是逻辑与。(2)真值表:1&0=01&1=10&0=00&1=0(3)从真值表可以看出:位与操作的特点是,只有1和1位于结果为1,其余全是0.(4)位与和逻辑与的区别:位与时两个操作数是按照二进制位彼次对应位相与的,逻辑与是两个操作数作为整体来相与的。(举例:0xAA&0xF0=0xA0,0xAA && 0xF0=1)4.2.1.2、位或|(1)注意:位或符号是一个|,两个|是逻辑

44、或。(2)真值表:1|0=11|1=10|0=00|1=1(3)从真值表可以看出:位或操作的特点是:只有2个0相位或才能得到0,只要有1个1结果就一定是1.(4)位或和逻辑或的区别:位或时两个操作数是按照二进制位彼次对应位相与的,逻辑或是两个操作数作为整体来相或的。4.2.1.3、位取反(1)注意:C语言中位取反是,C语言中的逻辑取反是!(2)按位取反是将操作数的二进制位逐个按位取反(1变成0,0变成1);而逻辑取反是真(在C语言中只要不是0的任何数都是真)变成假(在C语言中只有0表示假)、假变成真。实验:任何非0的数被按逻辑取反再取反就会得到1; 任何非0的数倍按位取反再取反就会得到他自己;

45、4.2.1.4、位异或(1)位异或真值表:11=0 00=010=101=1(2)位异或的特点:2个数如果相等结果为0,不等结果为1。记忆方法:异或就是相异就或操作起来。位与、位或、位异或的特点总结:位与:(任何数,其实就是1或者0)与1位与无变化,与0位与变成0位或:(任何数,其实就是1或者0)与1位或变成1,与0位或无变化位异或:(任何数,其实就是1或者0)与1位异或会取反,与0位异或无变化4.2.1.5、左移位<< 与右移位>>C语言的移位要取决于数据类型。对于无符号数,左移时右侧补0(相当于逻辑移位)对于无符号数,右移时左侧补0(相当于逻辑移位)对于有符号数,左

46、移时右侧补0(叫算术移位,相当于逻辑移位)对于有符号数,右移时左侧补符号位(如果正数就补0,负数就补1,叫算术移位)嵌入式中研究的移位,以及使用的移位都是无符号数4.2.2.位与位或位异或在操作寄存器时的特殊作用4.2.2.1、寄存器操作的要求(特定位改变而不影响其他位)(1)ARM是内存与IO统一编址的,ARM中有很多内部外设,SoC中CPU通过向这些内部外设的寄存器写入一些特定的值来操控这个内部外设,进而操控硬件动作。所以可以说:读写寄存器就是操控硬件。(2)寄存器的特点是按位进行规划和使用。但是寄存器的读写却是整体32位一起进行的(也就是说你只想修改bit5bit7是不行的,必须整体32

47、bit全部写入)(3)寄存器操作要求就是:在设定特定位时不能影响其他位。(4)如何做到?答案是:读-改-写三部曲。读改写的操作理念,就是:当我想改变一个寄存器中某些特定位时,我不会直接去给他写,我会先读出寄存器整体原来的值,然后在这个基础上修改我想要修改的特定位,再将修改后的值整体写入寄存器。这样达到的效果是:在不影响其他位原来值的情况下,我关心的位的值已经被修改了。4.2.2.2、特定位清零用&(1)回顾上节讲的位与操作的特点:(任何数,其实就是1或者0)与1位与无变化,与0位与变成0(2)如果希望将一个寄存器的某些特定位变成0而不影响其他位,可以构造一个合适的1和0组成的数和这个寄

48、存器原来的值进行位与操作,就可以将特定位清零。(3)举例:假设原来32位寄存器中的值为:0xAAAAAAAA,我们希望将bit8bit15清零而其他位不变,可以将这个数与0xFFFF00FF进行位与即可。4.2.2.3、特定位置1用|(1)回顾上节讲的位或操作的特点:任何数,其实就是1或者0)与1位或变成1,与0位或无变化(2)操作手法和刚才讲的位与是类似的。我们要构造这样一个数:要置1的特定位为1,其他位为0,然后将这个数与原来的数进行位或即可。4.2.2.4、特定位取反用(1)回顾上节讲的位异或操作的特点:(任何数,其实就是1或者0)与1位异或会取反,与0位异或无变化(2)操作手法和刚才讲

49、的位与是类似的。我们要构造这样一个数:要取反的特定位为1,其他位为0,然后将这个数与原来的数进行位异或即可。4.2.3.如何用位运算构建特定二进制数4.2.3.1、寄存器位操作经常需要特定位给特定值(1)从上节可知,对寄存器特定位进行置1或者清0或者取反,关键性的难点在于要事先构建一个特别的数,这个数和原来的值进行位与、位或、位异或等操作,即可达到我们对寄存器操作的要求。(2)解法1:用工具软件或者计算器或者自己大脑计算,直接给出完整的32位特定数。优势:可以完成工作,难度也不大,操作起来也不是太麻烦。劣势:依赖工具,而且不直观,读程序的人不容易理解。评价:凑活能用,但是不好用,应该被更好用的

50、方法替代。(2)解法2:自己写代码用位操作符号(主要是移位和位取反)来构建这个特定的二进制数4.2.3.2、使用移位获取特定位为1的二进制数(1)最简单的就是用移位来获取一个特定位为1的二进制数。譬如我们需要一个bit3bit7为1(隐含意思就是其他位全部为0)的二进制数,可以这样:(0x1f<<3)(2)更难一点的要求:获取bit3bit7为1,同时bit23bit25为1,其余位为0的数:(0x1f<<3) | (7<<23)4.2.3.3、再结合位取反获取特定位为0的二进制数(1)这次我们要获取bit4bit10为0,其余位全部为1的数。怎么做?(2)

51、利用上面讲的方法就可以:(0xf<<0)|(0x1fffff<<11)但是问题是:连续为1的位数太多了,这个数字本身就很难构造,所以这种方法的优势损失掉了。(3)这种特定位(比较少)为0而其余位(大部分)为1的数,不适合用很多个连续1左移的方式来构造,适合左移加位取反的方式来构造。(2)思路是:先试图构造出这个数的位相反数,再取反得到这个数。(譬如本例中要构造的数bit4bit10为0其余位为1,那我们就先构造一个bit4bit10为1,其余位为0的数,然后对这个数按位取反即可)4.2.3.4、总结:位与、位或结合特定二进制数即可完成寄存器位操作需求(1)如果你要的这个

52、数比较少位为1,大部分位为0,则可以通过连续很多个1左移n位得到。(2)如果你想要的数是比较少位为0,大部分位为1,则可以通过先构建其位反数,然后再位取反来得到。(3)如果你想要的数中连续1(连续0)的部分不止1个,那么可以通过多段分别构造,然后再彼此位与即可。这时候因为参与位或运算的各个数为1的位是不重复的,所以这时候的位或其实相当于几个数的叠加。4.2.4.位运算实战演练1回顾:要置1用|,用清零用&,要取反用,和<< >>用来构建特定二进制数。4.2.4.1、给定一个整型数a,设置a的bit3,保证其他位不变。a = a | (1<<3)或者 a |= (

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论