C语言高级篇 - 5.内存

1、冯诺依曼结构和哈佛结构

     (1)冯诺依曼结构是:数据和代码放在一起。

     (2)哈佛结构是:数据和代码分开存在。

     (3)什么是代码:函数

     (4)什么是数据:全局变量、局部变量

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

2、动态内存DRAM和静态内存SRAM

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位的内存,但是好处是效率高。

        问题:实际编程时要以省内存为大还是要以运行效率为重?答案是不定的,看具体情况。很多年前内存很贵机器上内存都很少,那时候写代码以省内存为主。现在随着半导体技术的发展内存变得很便宜了,现在的机器都是高配,不在乎省一点内存,而效率和用户体验变成了关键。所以现在写程序大部分都是以效率为重。

4、结构体

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

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

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

        第二种解法:用结构体    

    struct ages

    {

        int  age1;

        int  age2;

        int  age3;

    };

    struct ages age;

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

    struct people

    {

        int age;            // 人的年龄

        char name[20];        // 人的姓名

        int height;            // 人的身高

    };

        因为people的各个元素类型不完全相同,所以必须用结构体,没法用数组。

5、题外话:结构体内嵌指针实现面向对象

        (1)面向过程与面向对象。

            总的来说:C语言是面向过程的,但是C语言写出的linux系统是面向对象的。

        (2)非面向对象的语言,不一定不能实现面向对象的代码。只是说用面向对象的语言来实现面向对象要更加简单一些、直观一些、无脑一些。

          用C++、Java等面向对象的语言来实现面向对象简单一些,因为语言本身帮我们做了很多事情;但是用C来实现面向对象很麻烦,看起来也不容易理解,这就是为什么大多数人学过C语言却看不懂linux内核代码的原因。

struct s

{

    int age;                    // 普通变量

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

};

        使用这样的结构体就可以实现面向对象。

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

6、内存管理之栈(stack)

        (1)什么是栈

        栈是一种数据结构,C语言中使用栈来保存局部变量。栈是被发明出来管理内存的。

        (2)栈管理内存的特点(小内存、自动化)

              先进后出 FILO    first in last out        栈

              先进先出 FIFO   first in first out      队列

        栈的特点是入口即出口,只有一个口,另一个口是堵死的。所以先进去的必须后出来。

队列的特点是入口和出口都有,必须从入口进去,从出口出来,所以先进去的必须先出来,否则就堵住后面的。

        (3)栈的应用举例:局部变量

         C语言中的局部变量是用栈来实现的。

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

        注意:这里栈指针的移动和内存分配是自动的(栈自己完成,不用我们写代码去操作)。

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

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

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

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

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

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

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

int a;            // 局部变量定义

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

        (5)栈的约束(预定栈大小不灵活,怕溢出)

        首先,栈是有大小的。所以栈内存大小不好设置。如果太小怕溢出,太大怕浪费内存。(这个缺点有点像数组)

        其次,栈的溢出危害很大,一定要避免。所以我们在C语言中定义局部变量时不能定义太多或者太大(譬如不能定义局部变量时 int a[10000]; 使用递归来解决问题时一定要注意递归收敛)

7、内存管理之堆

        (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中的可变大小的数组的方式。

(4)、堆的优势和劣势(管理大块内存、灵活、容易内存泄漏)

    优势:灵活;

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

8、复杂数据结构

        (1)、链表、哈希表、二叉树、图等

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

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

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

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

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

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

        数据结构的发明都是为了配合一定的算法;算法是为了处理具体问题,算法的实现依赖于相应的数据结构。

        当前我们说的算法和纯数学是不同的(算法是基于数学的,大学计算机系研究生博士生很多本科都是数学相关专业的),因为计算机算法要求以数学算法为指导,并且结合计算机本身的特点来改进,最终实现一个在计算机上可以运行的算法(意思就是用代码可以表示的算法)。

        (4)、应该怎样学习这部分?

从上面表述大家应该明白以下事实:

    1. 数据结构和算法是相辅相成的,要一起研究。

    2. 数据结构和算法对嵌入式来说不全是重点,不要盲目的跑去研究这个。

    3. 一般在实际应用中,实现数据结构和算法的人和使用数据结构和算法的人是分开的。实际中有一部分人的工作就是研究数据结构和算法,并且试图用代码来实现这些算法(表现为库);其他做真正工作的人要做的就是理解、明白这些算法和数据结构的意义、优劣、特征,然后在合适的时候选择合适的数据结构和算法来解决自己碰到的实际问题。

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

猜你喜欢

转载自blog.csdn.net/poi_carefree/article/details/82764252