C语言学习之 内存管理及数据结构操作

记笔记的过程,写出自己的问题,感想,边看边总结.

在看视频时,或者书籍学习时,有什么感想,疑问,可以停下来,记录好,或者有什么理解,什么启示,收获都可以记下来,笔记看不看不重要,重要的是记笔记的过程,眼过千遍不如手过一遍.

让学习以理解为主,而不是以记忆为主!

 

 

  1. 程序为什么需要内存

程序运行的目的是

的到一定结果,这个结果可以解决实际需求问题,新问题不断产生,程序也需要不断重新编写.得到不同结果.

 

计算机就是在计算数据那么 数据 的重要性不言而喻.

 

 

计算机程序 = 代码 + 数据

 

代码用来加工数据,改变数据得到我们想要的结果.

 

 

 

程序 运行的目的:    结果        过程(不在乎结果,主要是过程是否运行,凡是C语言函数返回值是Void的都不在乎结果)

 

函数:

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),返回值不重要。

 

 函数的返回问题

 

计算机程序运行其实就是很多函数的运行,程序的本质就是函数,函数的本质就是加工数据的动作。

 

 

 

冯诺依曼机构是数据和代码放在一起、

哈佛结构是数据和代码分开存放

 

什么是代码:  函数

什么是数据: 全局变量、局部变量。

 

S5PV210中运行的linux系统上,运行程序时,应用程序的代码和数据都在DRAM,所以这种机构就是冯诺依曼机构;在单片机中,程序代码放在FlsahNorflash)中,然后程序在Flash原地运行,程序中的数据(全局变量、局部变量)不能放在RAM,这种就叫哈佛结构

 

 

 DRAM 动态内存  ||  SRAM是静态内存

 

 

 

为什么需要内存?

内存用来存储可变数据,数据在程序中表现为全局变量、局部变量等(特殊的:gcc中,其实常量也是存储在内存中的)(大部分单片机中,常量是存储在flash中的,也就是代码段),  

 

 GCC编译

编程的关键几乎在于 内存管理     譬如  数据结构,数据如何组织、算法,为了更优秀的方法来加工数据,既然与数据有关就里离不开内存

 

那么如何管理内存?

 

问题在于 写程序时,如何对内存进行管理,尤其是在程序运行时,内存的消耗量,内存对程序来说是一种的资源,

 

操作系统掌握多有的硬件系统,因为内存很大,所以操作系统把内存分成一个个的页面(一块,4KB),然后以页面为单位管理。再细分到页里里面 以字节为单位管理。

操作系统管理内存的原理分厂复杂,但是我们不需要了解这些细节。操作系统给我们提供了内存管理的一些接口,我们只需要用API即可管理内存。

譬如C语言中使用 malloc free 这些接口 管理内存

 

 

没有操作系统时,其实就是裸机程序,程序需要操作内存,编程者需要自己计算内存使用和安排

 

从语言角度: 不同语言提供了不同的操作内存的接口。

譬如汇编 根本没有任何内存管理,内存管理全靠自己,汇编中操作内存时直接使用内存地址譬如0xd0020010),很麻烦;

譬如C语言中,通过APImalloc free)来访问系统内存

譬如C++,用new来创建对象(其实就是为对象分配内存),如果此对象用完后忘记Delete,就会造成这个对象的内存不能释放,这就是内存泄露。

 

 

 

 

  1. 位、字节、半字、字的概念和内存位宽

 

什么是内存?

从逻辑角度:内存可以随机访问(给一个地址,就可以访问这个内存地址)、并且可以读写(在裸机上可限制读写,但是一般都是可读写的);内存在编程中天然用来存放变量的。一个变量对应内存中的一个单元。

 

内存位宽

从硬件角度讲:硬件内存实现本身是有宽度(内存芯片的实际数据总线数)的,也就是说有些内存条是8位的,而有些就是16位的,那么需要强调的是内存芯片之间是可以并联的。

 

位和字节

 

内存单元的大小:

位(1bit

字节(8bit

半字(一般是16bit

字(一般是32bit

 

bit就是计算机中信息的最基础单元,

 

字和半字

 

历史上曾经出现过16位、32位、64位系统三种,

平台不一样,子和半字的bit定义不一样,这些单位就提有多少bit 是依赖平台的

 

 

 

 

 有没有8位系统

 

 

  1. 内存编址和寻址、内存对齐

内存编址方法:

 

内存地址(一个数字)指向一个空间,一对一且永久不变

 

在程序运行时,计算机中CPU实际只通过内存地址,不需要找到这个空间在哪里?  

这个设定由硬件设计保证。

 

关键:内存编址是以字节为单位的

 

随便给个内存地址,这个内存地址对应的空间的大小是固定的,就是一个字节(8bit

如果把内存比为一栋大楼,楼里的一个个房间就是一个个内存空间,这个空间是固定大小 8bit

 

内存和数据类型的关系:

 

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

 

int 整形   (整数类型,整个整体现在它和CPU本身的数据位宽是一样的)譬如32位的CPU,整形就是32位,int就是32位(4个字节)的

数据类型和内存的关系:

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

 

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

 

在很多32位环境下,我们实际定义bool类型变量(实际只需要一个bit就够了)都是用int来实现bool的。

 

譬如   定义  bool b1;时,编译器实际帮我们32的内存来存储这个bool变量b1,编译器这个做实际浪费了31位的内存,但是好处是效率高

 

问题:省内存和运行效率,二者需具体情况解决。

 

 

内存对齐

 

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 四个单元本身裸机就有相关性,这四个字节组合起来当做一个int 硬件上就是合适的,效率就高。

 

对齐访问很配合硬件,所以效率很高;非对齐访问因为硬件本身不搭配,所以效率不高。

 

 

 

从内存编址看数组的意义

 

  1. C语言如何操作内存

 

 

C语言对内存地址的封装

譬如C语言中   int a;   a = 5;  a += 4;       //  a == 9;

 

 

结合内存来解析C语言语句的本质:

int a;     // 编译器帮我们申请了 1 int 类型 的内存格子(长度是4 字节。地址只有编译器知道。我们是不知道的,也不需要知道。)并且把符号 a 和这个格子绑定。

 

a = 5; // 编译器会把 5 放到 a 这个格子

 

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

数据类型决定长度的含义:以一个内存地址(一个字节的长度单位(固定的))开始,长度是多少,一次往后面延生多少连续长度

数据类型决定解析方法: 通过内存地址不同的类型指定这个内存地址(包括整个长度)指向的内存单元格子中二进制数的解析方法。

 强制类型转换

 

 

int   *0;

 

C语言中,函数就是一段代码的封装。函数名的实质就是这一段代码的首地址。所以说函数名的本质也是一个内存地址。

 

用指针来间接访问内存

 

关于类型(不管是普通变量类型int float 等,还是 指针类型int * float * 等),只要记住:

类型只是对后面数字或者符号(代表的是内存地址)  所表征的内存的一种长度规定和解析方法而已。

 

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

 

 

用数组来管理内存

数组管理内存和变量其实没有本质区别,只是符号的解析方法不同。 (普通变量、数组、指针变量其实都没有本质差别,都是对内存地址的解析,只是解析方法不一样)

int a;        //编译器分配4字节长度,并且把首地址和符号a绑定起来

int b[10];  //编译器分配40字节长度,并且把首元素首地址和符号b 绑定起来

int *c;       //编译器分配4字节长度,并且把首地址和符号c绑定起来,而与c地址绑定的首地址开始,存放了一个地址,地址的内存单元存放的是一个int 类型的数

 

数组中第一个元素(a[0])就称为首元素;每一个元素类型都是int,所以长度都是4,其中第一个字节的地址

就称为首地址;首元素a[0]的首地址就称为首元素首地址。

 

 

 

  1. 内存管理之结构体
    1. 数据结构这门学问的意义
      1. 数据结构就是研究数据如何组织(在内存中排布),如何加工的学问
    2. 最简单的数据结构:数组
      1. 为什么要有数组?
        1. 因为程序中有好多个类型相同、意义相关的变量需要管理,这时候需要管理,这时候如果用单独的变量来做程序看起来比较乱,用数组来管理会更好管理。

譬如: int ages[20];

  1. 数组的优势和缺陷
    1. 优势:数组比较简单,访问用下标,可以随机访问。
    2. 缺陷:
      1. 数组所有元素类型必须相同
      2. 数组的大小必须在定义时给出,而且一旦确定不能再改
  2. 结构体隆重登场
    1. 结构体发明出来就是为了解决数组的第一个缺陷:数组中多有元素类型必须相同

譬如:我们要管理三个学生的年龄(int 类型),怎么办?

第一种解法:用数组   int age[3];

第二种解法:用结构体  

struct ages

{

int age1;

int age2;

int gae3;

};

struct ages age;

分析总结:在这个示例中,数组要比结构体好。但是不能得出结论说数组就比结构体好,在包中元素类型不同时就只能用结构体而不能用数组了;

struct people

{

int age;

char name[20];

int height;

};

struct people people;

  1. 题外话:结构体内嵌指针实现面向对象
    1. 总的来说:C语言是面向过程的,但是C语言写出的linux系统是面向对象的。
    1. 非面向对象的语言,不一定不能实现面向对象的代码。只是说用面向对象的语言来实现面向对象要更加简单一些、直观一些、无脑一些。
    2. C++java等面向对象的语言来实现面向对象简单一些,因为语言本身帮我们做了很多事情;但是用C来实现面向对象很麻烦,看起来也不容易理解,这就是为什么大多数人学过C语言却看不懂linux代码的原因。

struct s

{

int age;                                 //普通变量

void (*pFunc)(void);          //函数指针,指向void func(void)这类的函数

}

这样包含了函数指针的结构体就类似于面向对象的class,结构体中变量类似与class中的成员变量,结构体中的函数指针类似与class中的成员方法

  1. 内存管理之栈
    1. 什么是栈
      1. 栈是一种数据结构,C语言中使用栈来保存局部变量。
      2. 栈是被发明出来管理内存的
    2. 栈管理内存的特点
      1. 先进后出   FILO    first in last out   
      2. 先进先出   FIFO    first in first put    队列
      3. 栈的特点是入口即出口,只有一个口,另一个是堵死的,所以先进去的必须后出来。
    3. 栈的应用举例:局部变量
      1. 我们在C中定义一个局部变量时(int a,编译器会在栈中分配一段空间(4字节)给这个局部变量用(分配时栈顶指针会移动给出空间,给局部变量a用的意思就是,将这4字节的占的内存地址和我们定义的局部变量名a给关联起来 ),对应栈的操作是入栈。
      1. 注意这里栈指针的移动和内存分配是自动(栈自己完成,不用我们写代码去操作)。
      2. 然后等我们函数退出的时候,局部变量要灭亡。对应栈的操作时弹栈(出栈)。出栈时也是栈顶指针移动将栈空间中与a关联的那4个字节空间释放。这个动作也是自动的,也不用人写代码。

栈的优点:栈管理内存,好处是方便,分配和最后回收都不用程序员操心,C语言自动完成。

定义局部变量,其实就是在栈中通过移动栈指针来给程序提供一个内存空间和这个局部变量名绑定。因为这段内存空间在栈上,而栈内存是反复使用的(脏的,上次用完没清零的),所以说使用栈来实现的局部变量定义时如果不显示初始化,值就是脏的。如果你显式初始化怎么样?

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

int a = 15;//局部变量定义时初始化

//C语言编译器会自动把这行转为:

int a;                 //局部变量定义

a = 15;                    //普通的赋值语句

  1. 栈的约束(预定栈大小不灵活,怕溢出)
    1. 栈是有大小的,所以栈内存大小不好设置,如果太小怕溢出,太大怕浪费内存。 (这个缺点有点像数组)
    2. 其次,栈的溢出危害很大,一定要避免。所以我们在C语言中定义局部变量时不能定义太多或者太大(譬如不能定义局部变量时int a[10000];使用递归来解决问题时一定要注意递归收敛)      
  1. 内存管理之堆
    1. 什么是堆?
      1. 堆(heap)是一种内存管理方式。 内存管理对操作系统来说是一件非常复杂的事情
        1. 内存容量很大
        2. 内存需求在时间和大小块上没有规律(操作系统上运行着大量的进程随时都会申请或者释放内存,申请或者释放的内存块大小随意)
      2. 内存管理方式特点就是自由(随时申请、释放;大小块随意)。堆内存是操作系统划归给堆管理器(操作系统中的一段代码,属于操作系统中的一段代码,属于操作系统的内存管理单元)来管理的,然后向使用者(用户进程)提供APImalloc free)来使用堆内存
      3. 什么时候使用堆内存?    需要内存容量比较大时,需要反复使用及释放时;很多数据结构(譬如链表,二叉树)的实现都要使用堆内存。
    2. 堆内存的特点
      1. 容量不限,常规使用的需求容量都能满足
      2. 申请及释放都需要手工进行,需要程序员写代码明确进项申请malloc及释放free,如果程序员申请内存并使用后未释放,这段内存就丢失了(在对管理器的记录中,这段内存仍然属于你这个进程,但是进程自己又以为内存已经不用了,再用的时候又回去申请新的内存块,这就叫吃内存),称为内存泄露。在C/C++ 语言中,内存泄露是最严重的程序bug,这也是别人认为Java/C#优秀的地方。
    3. C语言操作堆内存的接口(malloc   free
      1. 堆内存释放时最简单,直接调用free释放即可。    void free(void *ptr)
      2. 堆内存申请时,有3个可选择的类似功能的函数:malloccalloc, realloc
        1. void *malloc(size_t size);
        2. void *calloc(size_t nmemb,size_t size);  //nmemb 个单元,每个单元size 个字节
        3. void *realloc (void *ptr, size_t size);  //改变原来申请的空间的大小的

譬如要申请10int 元素的内存:

malloc(40);                    malloc(10*sizeof(int));

calloc(10,4);                  calloc(10, sizeof(int));

  1. 数组定义时必须同时给出数组元素个数(数组大小),而且一旦定义再无法更改。

语法技巧可以更改数组大小,但其实这只是一种障眼法。他的工作原理是:先重新创建一个新的数组大小为要更改后的数组,然后将原数组的所有元素复制进新的数组,然后释放原数组,最后返回新的数组给用户;

堆内存申请时给定大小,然后一旦申请完成大小不变,如果要变只能通过realloc接口(原理与上述语法技巧一致)

  1. 优势与劣势(管理大块内存、灵活、容易内存泄露)

优势:灵活

劣势:需要程序员去处理各种细节,所以容易出错,严重依赖程序员的水水平。

  1. 复杂数据结构
    1. 链表、哈希表、二叉树、图等
      1. 链表是最简单且最重要的,链表在linux内核中使用非常多,驱动、应用编程很多时候都需要使用链表。所以对链表必须掌握,掌握到:  会自己定义结构体实现链表、会写链表的节点插入(前插、后插)、节点删除、节点查找、节点遍历等。
      2. 哈希表不是很常用,一般不需要自己学实现,而直接使用别人实现的哈希表比较多。对我们来说最重要的是要明白哈希表的原理、从而知道哈希表的特点,从而知道什么时候该用哈希表,当看到别人用了哈希表的时候要明白别人为什么要用哈希表。合不合适?有没有好的选择?
        1. 哈希表类似于数组,但是在索引节点时是利用某种映射方式的
      3. 二叉树、图等。在嵌入式开发中这些数据结构用的很少很少,且这些数据结构一般用来解决特定问题的。
    2. 为什么需要更复杂的数据结构?
      1. 因为现实中的实际问题是多种多样的,问题的复杂度不同,所以需要解决问题的算法和数据结构也不同。
      2. 所以在处理不同复杂度的问题,就去针对性解决的数据结构和算法
    3. 数据结构和算法的关系
      1. 数据结构的发明都是为了配合一定的算法
      2. 算法是为了处理具体问题,算法的实现依赖于相应的数据结构。
      3. 当前我们所的算法和纯数学是不同的(算法是基于数学的),因为计算机算法要求以数学算法为指导,并且结合计算机本身的特点来改进,最终实现一个在计算机上可以运行的算法(即代码可以表示的算法)。
    4. 应该怎样学习这部分?
      1. 数据结构和算法是相辅相成的,要一起研究。
      2. 数据结构和算法对嵌入式来说不全是重点,不要盲目去研究这个。
      3. 一般在实际应用中实现数据结构和算法的人和使用数据结构和算法的人是分开的。实际中有一部分人的工作就是研究数据结构和算法,并且试图用代码来实现这些算法(表现为库);其他真正做工作的人要做的就是理解明白这些算法和数据结构的意义、优劣、特征,然后在合适的时候选择合适的数据结构和算法来解决自己碰到的实际问题。

举个例子:linux内核在字符设备驱动管理时,使用了哈希表(hash table,散列表)。所以字符驱动的很多特点都和哈希表的特点有关。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(C语言)