• C语言探索之旅 | 第二部分第八课:动态分配


    作者 谢恩铭,公众号「程序员联盟」(微信号:coderhub)。
    转载请注明出处。
    原文:https://www.jianshu.com/p/bbce8f04faf1

    《C语言探索之旅》全系列

    内容简介


    1. 前言
    2. 变量的大小
    3. 内存的动态分配
    4. 动态分配一个数组
    5. 总结
    6. 第二部分第九课预告

    1. 前言


    上一课是 C语言探索之旅 | 第二部分第七课:文件读写

    经历了第二部分的一些难点课程,我们终于来到了这一课,一个听起来有点酷酷的名字:动态分配

    “万水千山总是情,分配也由系统定”。

    到目前为止,我们创建的变量都是系统的编译器为我们自动构建的,这是简单的方式。

    其实还有一种更偏手动的创建变量的方式,我们称为“动态分配”(Dynamic Allocation)。dynamic 表示“动态的”,allocation 表示“分配”。

    动态分配的一个主要好处就是可以在内存中“预置”一定空间大小,在编译时还不知道到底会用多少。

    使用这个技术,我们可以创建大小可变的数组。到目前为止我们所创建的数组都是大小固定不可变的。而学完这一课后我们就会创建所谓“动态数组”了。

    学习这一章需要对指针有一定了解,如果指针的概念你还没掌握好,可以回去复习 C语言探索之旅 | 第二部分第二课:进击的指针,C语言的王牌! 那一课。

    我们知道当我们创建一个变量时,在内存中要为其分配一定大小的空间。例如:

    int number = 2;
    

    当程序运行到这一行代码时,会发生几件事情:

    1. 应用程序询问操作系统(Operating System,简称 OS。例如Windows,Linux,macOS,Android,iOS,等)是否可以使用一小块内存空间。

    2. 操作系统回复我们的程序,告诉它可以将这个变量存储在内存中哪个地方(给出分配的内存地址)。

    3. 当函数结束后,你的变量会自动从内存中被删除。你的程序对操作系统说:“我已经不需要内存中的这块地址了,谢谢!” (当然,实际上你的程序不可能对操作系统说一声“谢谢”,但是确实是操作系统在掌管一切,包括内存,所以对它还是客气一点比较好...)。

    可以看到,以上的过程都是自动的。当我们创建一个变量,操作系统就会自动被程序这样调用。

    那么什么是手动的方式呢?说实在的,没人喜欢把事情复杂化,如果自动方式可行,何必要大费周章来使用什么手动方式呢?但是要知道,很多时候我们是不得不使用手动方式。

    这一课中,我们将会:

    1. 探究内存的机制(是的,虽然以前的课研究过,但是还是要继续深入),了解不同变量类型所占用的内存大小。

    2. 接着,探究这一课的主题,来学习如何向操作系统动态请求内存。也就是所谓的“动态内存分配”。

    3. 最后,通过学习如何创建一个在编译时还不知道其大小(只有在程序运行时才知道)的数组来了解动态内存分配的好处。

    准备好了吗?Let's Go !

    2. 变量的大小


    根据我们所要创建的变量的类型(char,int,double,等等),其所占的内存空间大小是不一样的。

    事实上,为了存储一个大小在 -128 至 127 之间的数(char 类型),只需要占用一个字节(8 个二进制位)的内存空间,是很小的。

    然而,一个 int 类型的变量就要占据 4 个字节了;一个 double 类型要占据 8 个字节。

    问题是:并不总是这样。

    什么意思呢?

    因为类型所占内存的大小还与操作系统有关系。不同的操作系统可能就不一样,32 位和 64 位的操作系统的类型大小一般会有区别。

    这一节中我们的目的是学习如何获知变量所占用的内存大小。

    有一个很简单的方法:使用 sizeof()

    虽然看着有点像函数,但其实 sizeof 不是一个函数,而是一个 C语言的关键字,也算是一个运算符吧。

    我们只需要在 sizeof 的括号里填入想要检测的变量类型,sizeof 就会返回所占用的字节数了。

    例如,我们要检测 int 类型的大小,就可以这样写:

    sizeof(int)
    

    在编译时,sizeof(int) 就会被替换为 int 类型所占用的字节数了。

    在我的电脑上,sizeof(int) 是 4,也就是说 int 类型在我的电脑的内存中占据 4 个字节。在你的电脑上,也许是 4,但也可能是其他的值。

    我们用一个例子来测试一下吧:

    // octet 是英语“字节”的意思,和 byte 类似
    printf("char : %d octets
    ", sizeof(char));
    printf("int : %d octets
    ", sizeof(int));
    printf("long : %d octets
    ", sizeof(long));
    printf("double : %d octets
    ", sizeof(double));
    

    在我的电脑(64 位)运行,输出:

    char : 1 octets
    int : 4 octets
    long : 8 octets
    double : 8 octets
    

    我们并没有测试所有已知的变量类型,你也可以课后自己去测试一下其他的类型,例如:short,float。

    曾几何时,当电脑的内存很小的年代,有这么多不同大小的变量类型可供选择是一件很好的事,因为我们可以选“够用的最小的”那种变量类型,以节约内存。

    现在,电脑的内存一般都很大,“有钱任性”么。所以我们在编程时也没必要太“拘谨”。不过在嵌入式领域,内存大小一般是有限的,我们就得斟酌着使用变量类型了。

    既然 sizeof 这么好用,我们可不可以用它来显示我们自定义的变量类型的大小呢?例如 struct,enum,union。

    是可以的。写一个程序测试一下:

    #include <stdio.h>
    
    typedef struct Coordinate
    {
        int x;
        int y;
    } Coordinate;
    
    int main(int argc, char *argv[])
    {
        printf("Coordinate 结构体的大小是 : %d 个字节
    ", sizeof(Coordinate));
    
        return 0;
    }
    

    运行输出:

    Coordinate 结构体的大小是 : 8 个字节
    

    对于内存的全新视角


    之前,我们在绘制内存图示时,还是比较不精准的。现在,我们知道了每个变量所占用的大小,我们的内存图示就可以变得更加精准了。

    假如我定义一个 int 类型的变量:

    int age = 17;
    

    我们用 sizeof 测试后得知 int 的大小为 4。假设我们的变量 age 被分配到的内存地址起始是 1700,那么我们的内存图示就如下所示:

    我们看到,我们的 int 型变量 age 在内存中占用 4 个字节,起始地址是 1700(它的内存地址),一直到 1703。

    如果我们对一个 char 型变量(大小是一个字节)同样赋值:

    char number = 17;
    

    那么,其内存图示是这样的:

    假如是一个 int 型的数组:

    int age[100];
    

    用 sizeof() 测试一下,就可以知道在内存中 age 数组占用 400 个字节。4 * 100 = 400。

    即使这个数组没有赋初值,但是在内存中仍然占据 400 个字节的空间。变量一声明,在内存中就为它分配一定大小的内存了。

    那么,如果我们创建一个类型是 Coordinate 的数组呢?

    Coordinate coordinate[100];
    

    其大小就是 8 * 100 = 800 个字节了。

    3. 内存的动态分配


    好了,现在我们就进入这一课的关键部分了,重提一次这一课的目的:学会如何手动申请内存空间。

    我们需要引入 stdlib.h 这个标准库头文件,因为接下来要使用的函数是定义在这个库里面。

    这两个函数是什么呢?就是:

    • malloc:是 Memory Allocation 的缩写,表示“内存分配”。询问操作系统能否预支一块内存空间来使用。

    • free:表示“解放,释放,自由的”。意味着“释放那块内存空间”。告诉操作系统我们不再需要这块已经分配的空间了,这块内存空间会被释放,另一个程序就可以使用这块空间了。

    当我们手动分配内存时,须要按照以下三步顺序来:

    1. 调用 malloc 函数来申请内存空间。

    2. 检测 malloc 函数的返回值,以得知操作系统是否成功为我们的程序分配了这块内存空间。

    3. 一旦使用完这块内存,不再需要时,必须用 free 函数来释放占用的内存,不然可能会造成内存泄漏。

    以上三个步骤是不是让我们回忆起关于上一课“文件读写”的内容了?

    这三个步骤和文件指针的操作有点类似,也是先申请内存,检测是否成功,用完释放。

    malloc 函数:申请内存


    malloc 分配的内存是在堆上,一般的局部变量(自动分配的)大多是在栈上。

    关于堆和栈的区别,还有内存的其他区域,如静态区等,大家可以自己延伸阅读。

    之前“字符串”那一课里已经给出过一张图表了。再来回顾一下吧:

    名称 内容
    代码段 可执行代码、字符串常量
    数据段 已初始化全局变量、已初始化全局静态变量、局部静态变量、常量数据
    BSS段 未初始化全局变量,未初始化全局静态变量
    局部变量、函数参数
    动态内存分配

    给出 malloc 函数的原型,你会发现有点滑稽:

    void* malloc(size_t numOctetsToAllocate);
    

    可以看到,malloc 函数有一个参数 numOctetsToAllocate,就是需要申请的内存空间大小(用字节数表示),这里的 size_t(之前的课程有提到过)其实和 int 是类似的,就是一个 define 宏定义,实际上很多时候就是 int。

    对于我们目前的演示程序,可以将 sizeof(int) 置于 malloc 的括号中,表示要申请 int 类型的大小的空间。

    真正引起我们兴趣的是 malloc 函数的返回值:

    void*
    

    如果你还记得我们在函数那章所说的,void 表示“空”,我们用 void 来表示函数没有返回值。

    所以说,这里我们的函数 malloc 会返回一个指向 void 的指针,一个指向“空”(void 表示“虚无,空”)的指针,有什么意义呢?malloc 函数的作者不会搞错了吧?

    不要担心,这么做肯定是有理由的。

    难道有人敢质疑老爷子 Dennis Ritchie(C语言的作者)的智商?
    来人呐,拖出去... 罚写 100 个 C语言小游戏。

    事实上,这个函数返回一个指针,指向操作系统分配的内存的首地址。

    如果操作系统在 1700 这个地址为你开辟了一块内存的话,那么函数就会返回一个包含 1700 这个值的指针。

    但是,问题是:malloc 函数并不知道你要创建的变量是什么类型的。

    实际上,你只给它传递了一个参数: 在内存中你需要申请的字节数。

    如果你申请 4 个字节,那么有可能是 int 类型,也有可能是 long 类型。

    正因为 malloc 不知道自己应该返回什么变量类型(它也无所谓,只要分配了一块内存就可以了),所以它会返回 void* 这个类型。这是一个可以表示任意指针类型的指针。

    void* 与其他类型的指针之间可以通过强制转换来相互转换。例如:

    int *i = (int *)p;  // p 是一个 void* 类型的指针
    
    void *v = (void *)c;  // c 是一个 char* 类型的指针
    

    实践


    如果我实际来用 malloc 函数分配一个 int 型指针:

    int *memoryAllocated = NULL;  // 创建一个 int 型指针
    
    memoryAllocated = malloc(sizeof(int));  // malloc 函数将分配的地址赋值给我们的指针 memoryAllocated
    

    经过上面的两行代码,我们的 int 型指针 memoryAllocated 就包含了操作系统分配的那块内存地址的首地址值。

    假如我们用之前我们的图示来举例,这个值就是 1700。

    检测指针


    既然上面我们用两行代码使得 memoryAllocated 这个指针包含了分配到的地址的首地址值,那么我们就可以通过检测 memoryAllocated 的值来判断申请内存是否成功了:

    1. 如果为 NULL,则说明 malloc 调用没有成功。

    2. 否则,就说明成功了。

    一般来说内存分配不会失败,但是也有极端情况:

    1. 你的内存(堆内存)已经不够了。

    2. 你申请的内存值大得离谱(比如你申请 64 GB 的内存空间,那我想大多数电脑都是不可能分配成功的)。

    希望大家每次用 malloc 函数时都要做指针的检测,万一真的出现返回值为 NULL 的情况,那我们需要立即停止程序,因为没有足够的内存,也不可能进行下面的操作了。

    为了中断程序的运行,我们来使用一个新的函数:

    exit()
    

    exit 函数定义在 stdlib.h 中,调用此函数会使程序立即停止。

    这个函数也只有一个参数,就是返回值,这和 return 函数的参数是一样原理的。实例:

    int main(int argc, char *argv[])
    {
        int *memoryAllocated = NULL;
    
        memoryAllocated = malloc(sizeof(int));
    
        if (memoryAllocated == NULL)  // 如果分配内存失败
        {
            exit(0);  // 立即停止程序
        }
    
        // 如果指针不为 NULL,那么可以继续进行接下来的操作
    
        return 0;
    }
    

    另外一个问题:用 malloc 函数申请 0 字节内存会返回 NULL 指针吗?


    可以测试一下,也可以去查找关于 malloc 函数的说明文档。

    申请 0 字节内存,函数并不返回 NULL,而是返回一个正常的内存地址。
    但是你却无法使用这块大小为 0 的内存!

    这就好比尺子上的某个刻度,刻度本身并没有长度,只有某两个刻度一起才能量出长度。

    对于这一点一定要小心,因为这时候 if(NULL != p) 语句校验将不起作用。

    free函数:释放内存


    记得上一课我们使用 fclose 函数来关闭一个文件指针,也就是释放占用的内存。

    free 函数的原理和 fclose 是类似的,我们用它来释放一块我们不再需要的内存。原型:

    void free(void* pointer);
    

    free 函数只有一个目的:释放 pointer 指针所指向的那块内存。

    实例程序:

    int main(int argc, char *argv[])
    {
        int* memoryAllocated = NULL;
    
        memoryAllocated = malloc(sizeof(int));
    
        if (memoryAllocated == NULL)  // 如果分配内存失败
        {
            exit(0);  // 立即停止程序
        }
    
        // 此处添加使用这块内存的代码
    
        free(memoryAllocated);  // 我们不再需要这块内存了,释放之
    
        return 0;
    }
    

    综合上面的三个步骤,我们来写一个完整的例子:

    #include <stdio.h>
    #include <stdlib.h>
    
    int main(int argc, char *argv[])
    {
        int* memoryAllocated = NULL;
    
        memoryAllocated = malloc(sizeof(int));  // 分配内存
    
        if (memoryAllocated == NULL)  // 检测是否分配成功
        {
            exit(0);  // 不成功,结束程序
        }
    
        // 使用这块内存
        printf("您几岁了 ? ");
    
        scanf("%d", memoryAllocated);
    
        printf("您已经 %d 岁了
    ", *memoryAllocated);
    
        free(memoryAllocated);  // 释放这块内存
    
        return 0;
    }
    

    运行输出:

    您几岁了 ? 32
    您已经 32 岁了
    

    以上就是我们用动态分配的方式来创建了一个 int 型变量,使用它,释放它所占用的内存。

    但是,我们也完全可以用以前的方式来实现,如下:

    int main(int argc, char *argv[])
    {
        int myAge = 0; // 分配内存 (自动)
    
        // 使用这块内存
        printf("您几岁了 ? ");
    
        scanf("%d", &myAge);
    
        printf("你已经 %d 岁了
    ", myAge);
    
        return 0;
    }  // 释放内存 (在函数结束后自动释放)
    

    在这个简单使用场景下,两种方式(手动和自动)都是能完成任务的。

    总结说来,创建一个变量(说到底也就是分配一块内存空间)有两种方式:自动和手动。

    • 自动:我们熟知并且一直使用到现在的方式。

    • 手动(动态):这一课我们学习的内容。

    你可能会说:“我发现动态分配内存的方式既复杂又没什么用嘛!”

    复杂么?还行吧,确实相对自动的方式要考虑比较多的因素。

    没有用么?绝不!

    因为很多时候我们不得不使用手动的方式来分配内存。

    接下来我们就来看一下手动方式的必要性。

    4. 动态分配一个数组


    暂时我们只是用手动方式来创建了一个简单的变量。

    然而,一般说来,我们的动态分配可不是这样“大材小用”的。

    如果只是创建一个简单的变量,我们用自动的方式就够了。

    那你会问:“啥时候须要用动态分配啊?”

    问得好。动态分配最常被用来创建在运行时才知道大小的变量,例如动态数组。

    假设我们要存储一个用户的朋友的年龄列表,按照我们以前的方式(自动方式),我们可以创建一个 int 型的数组:

    int ageFriends[18];
    

    很简单对吗?那问题不就解决了?

    但是以上方式有两个缺陷:

    1. 你怎么知道这个用户只有 18 个朋友呢?可能他有更多朋友呢。

    2. 你说:“那好,我就创建一个数组:

    int ageFriends[10000];
    

    足够储存 1 万个朋友的年龄。”

    但是问题是:可能我们使用到的只是这个大数组的很小一部分,岂不是浪费内存嘛。

    最恰当的方式是询问用户他有多少朋友,然后创建对应大小的数组。

    而这样,我们的数组大小就只有在运行时才能知道了。

    Voila,这就是动态分配的优势了:

    1. 可以在运行时才确定申请的内存空间大小。

    2. 不多不少刚刚好,要多少就申请多少,不怕不够或过多。

    所以借着动态分配,我们就可以在运行时询问用户他到底有多少朋友。

    如果他说有 20 个,那我们就申请 20 个 int 型的空间;如果他说有 50 个,那就申请 50 个。经济又环保。

    我们之前说过,C语言中禁止用变量名来作为数组大小,例如不能这样:

    int ageFriends[numFriends];  // numFriends 是一个变量
    

    尽管有的 C编译器可能允许这样的声明,但是我们不推荐。

    我们来看看用动态分配的方式如何实现这个程序:

    #include <stdio.h>
    #include <stdlib.h>
    
    int main(int argc, char *argv[])
    {
        int numFriends = 0, i = 0;
    
        int *ageFriends= NULL;  // 这个指针用来指示朋友年龄的数组
    
        // 询问用户有多少个朋友
        printf("请问您有多少朋友 ? ");
    
        scanf("%d", &numFriends);
    
        if (numFriends > 0)  // 至少得有一个朋友吧,不然也太惨了 :P
        {
            ageFriends = malloc(numFriends * sizeof(int));  // 为数组分配内存
            if (ageFriends== NULL)  // 检测分配是否成功
            {
                exit(0); // 分配不成功,退出程序
            }
    
            // 逐个询问朋友年龄
            for (i = 0 ; i < numFriends; i++)  {
                printf("第%d位朋友的年龄是 ? ", i + 1);
                scanf("%d", &ageFriends[i]);
            }
    
            // 逐个输出朋友的年龄
            printf("
    
    您的朋友的年龄如下 :
    ");
            for (i = 0 ; i < numFriends; i++) {
                printf("%d 岁
    ", ageFriends[i]);
            }
    
            // 释放 malloc 分配的内存空间,因为我们不再需要了
            free(ageFriends);
        }
    
        return 0;
    }
    

    运行输出:

    请问您有多少朋友 ? 7
    第1位朋友的年龄是 ? 25
    第2位朋友的年龄是 ? 21
    第3位朋友的年龄是 ? 27
    第4位朋友的年龄是 ? 18
    第5位朋友的年龄是 ? 14
    第6位朋友的年龄是 ? 32
    第7位朋友的年龄是 ? 30
    
    您的朋友的年龄如下 :
    25岁
    21岁
    27岁
    18岁
    14岁
    32岁
    30岁
    

    当然了,这个程序比较简单,但我向你保证以后的课程会使用动态分配来做更有趣的事。

    5. 总结


    1. 不同类型的变量在内存中所占的大小不尽相同。

    2. 借助 sizeof 这个关键字(也是运算符)可以知道一个类型所占的字节数。

    3. 动态分配就是在内存中手动地预留一块空间给一个变量或者数组。

    4. 动态分配的常用函数是 malloc(当然,还有 calloc,realloc,可以查阅使用方法,和 malloc 是类似的),但是在不需要这块内存之后,千万不要忘了使用 free 函数来释放。而且,malloc 和 free 要一一对应,不能一个 malloc 对应两个 free,会出错;或者两个 malloc 对应一个 free,会内存泄露!

    5. 动态分配使得我们可以创建动态数组,就是它的大小在运行时才能确定。

    6. 第二部分第九课预告


    今天的课就到这里,一起加油吧!

    下一课: C语言探索之旅 | 第二部分第九课: 实战"悬挂小人"游戏


    我是 谢恩铭,公众号「程序员联盟」(微信号:coderhub)运营者,慕课网精英讲师 Oscar 老师,终生学习者。
    热爱生活,喜欢游泳,略懂烹饪。
    人生格言:「向着标杆直跑」

  • 相关阅读:
    遍历mac系统访达文件时候要注意了
    python r+ 是追加写
    Linux如何给一个python被挂起进程传递命令?
    nohup后台挂起
    树莓派建站全过程
    wechat robot
    廖雪峰笔记(转网络专题)
    【转】 前端笔记之JavaScript(十一)event&amp;BOM&amp;鼠标/盒子位置&amp;拖拽/滚轮
    【转】 前端笔记之JavaScript(十)深入JavaScript节点&amp;DOM&amp;事件
    【转】 前端笔记之JavaScript(九)定时器&amp;JSON&amp;同步异步/回调函数&amp;函数节流&amp;call/apply
  • 原文地址:https://www.cnblogs.com/frogoscar/p/13150944.html
Copyright © 2020-2023  润新知