第11章:动态内存分配

  1. 为什么使用动态内存分配
  2. malloc和free
  3. calloc和realloc
  4. 使用动态分配的内存
  5. 常见的动态内存错误
  6. 内存分配实例

数组的元素存储于内存中连续的位置上。当一个数组被声明时,它所需要的内存在编译时就被分配。

#1. 为什么使用动态内存分配

当你声明数组时,你必须用一个编译时常量指定数组的长度。但是,数组的长度常常在运行时才知道,这是由于它所需的内存空间取决于数据。

#2. malloc和free

C函数库提供了两个函数,mallocfree,分别用来执行动态内存分配和释放。这些函数维护一个可用内存池。当一个程序另外需要一些内存时,它就调用malloc函数,malloc函数从内存池中提取一个块合适的内存,并向程序返回一个指向这块内存的指针。这块内存此时并没有以任何方式进行初始化。如果对这块内存进行初始化非常重要,你要么自己动手对它进行初始化,要么使用calloc函数。当一块以前分配的内存不再使用时,程序调用free函数把它归还给内存池供以后之需。

这两个函数的原型如下所示,它们都在头文件stdlib.h中声明。

void *malloc(size_t size);

void free(void *pointer);

malloc的参数就是需要分配的内存字节(字符)数。如果内存池中的可用内存可以满足这个需求,malloc就返回一个指向被分配的内存块起始位置的指针。

malloc所分配的是一块连续内存。例如,如果请求它分配100个字节的内存,那么它实际分配的内存就是100个连续的字节,并不会分开位于两块或多块不同的内存。同时,malloc实际分配的内存有可能比你请求的稍微多一点。

如果内存池是空的,或者它的可用内存无法满足你的请求,在这种情况下,malloc函数向操作系统请求,要求得到更多的内存,并在这块新内存上执行分配任务。如果操作系统无法向malloc提供更多的内存,malloc就返回一个NULL指针。因此,对每个malloc返回的指针进行检查,确保它并非NULL是非常重要的。

free的参数必须要么是NULL,要么是一个先前从malloc、calloc或realloc返回的值。向free传递一个NULL参数不会产生任何效果。

#3. calloc和realloc

另外有两个内存分配函数,callocrealloc。它们的原型如下所示:

void *calloc(size_t num_elements);

void realloc(void *ptr,size_t new_size);

calloc也用于分配内存。malloc和calloc之间的主要区别是后者在返回指向内存的指针之前把它初始化为0。calloc和malloc之间另一个较小的区别是它们请求内存数量的方式不同。calloc的参数包括所需元素的数量和每个元素的字节数。根据这些值能计算出总共需要分配的内存。

realloc函数用于修改一个原先已经分配的内存块的大小。使用这个函数使一块内存扩大或缩小。如果它用于扩大一个内存块,那么这块内存原先的内容依然保留,新增加的内存添加到原先内存块的后面,新内存并未以任何方法进行初始化。如果它用于缩小一个内存块,该内存块尾部部分内存便被拿掉,剩余部分内存的原先内容依然保留。

如果原先的内存块无法改变大小,realloc将分配另一块正确大小的内存,并把原先那块内存的内容复制到新的块上。因此,在使用realloc之后,你就不能再使用指向旧内存的指针,而是应该改用realloc所返回的指针。

如果realloc函数的第一个参数是NULL,那么它的行为就和malloc一模一样。

#4. 使用动态分配的内存

这里有一个例子,它用malloc分配一块内存。

int *pi;
pi = malloc(25 * sizeof(int));
if(pi == NULL) {
    printf("Out of memory!\n");
    exit(1);
}

符号NULL定义于stdio.h,它实际上是字面值常量0。它在这里起着视觉提醒器的作用,提醒我们进行测试的值是一个指针而不是整数。

如果内存分配成功,那么我们就拥有一个指向100个字节的指针。在整型为4个字节的机器上,这块内存被当作25个整型元素的数组,因为pi是一个指向整型的指针。

#5. 常见的动态内存错误

在使用动态内存分配的程序中,常常会出现许多错误。这些错误包括对NULL指针进行解引用操作、对分配的内存进行操作时越过边界、释放并非动态分配的内存、试图释放一块动态分配的内存的一部分以及一块动态内存被释放之后被继续使用。

==动态内存分配最常见的错误就是忘记检查所请求的内存是否成功分配。==

动态内存分配的第二大错误来源是操作内存时超出内存的边界。例如,如果你得到一个25个整型的数组,进行下标引用操作时如果下标值小于0或大于24将引起两种类型的问题。

  1. 第1种问题显而易见:被访问的内存可能保存了其他变量的值。对它进行修改将破坏那个变量,修改那个变量将破坏你存储在那里的值,这种类型的bug非常难以发现。
  2. 第2种问题不是那么明显:在malloc和free的有些实现中,它们以链表的形式维护可用的内存池。对分配的内存之外的区域进行访问可能破坏这个链表,这有可能产生异常,从而终止程序。
# include <stdlib.h>

#define malloc 不要直接调用malloc!
#define MALLOC(num,type)(type*)alloc((num) * sizeof(type))
extern void *alloc(size_t size);
错误检查分配器:接口(alloc.h)
/*
** 不易发生错误的内存分配器的实现
*/
#include <stdio.h>
#include "alloc.h"
#undef malloc

void *alloc(size_t size) {
    void *new_mem;
    /*
    **请求所需的内存,并检查确实分配成功
    */
    new_mem = malloc(size);
    
    if(new_mem == NULL) {
        printf("Out of memory!\n")
        exit(1);
    }
    
    return new_mem;
}
错误检查分配器:实现(alloc.c)
/*
**一个使用很少引起错误的内存分配器的程序
*/
#include "alloc.h"

void function() {
    int *new_memory;
    /*
    **获得一串整型数的空间
    */
    new_memory = MALLOC(25,int);
}
使用错误检查分配器

当一个使用动态内存分配的程序失败时,人们很容易把问题的责任推给malloc和free函数。但它们实际上很少是罪魁祸首。事实上,问题几乎总是出现在你自己的程序中,而且常常是由于访问了分配内存以外的区域而引起的。

当你使用free时,可能出现各种不同的错误。传递给free的指针必须是一个从malloc、calloc或realloc函数返回的指针。传递给free函数一个指针,让它释放一块并非动态分配的内存可能导致程序立即终止或在晚些时候终止。试图释放一块动态分配内存的一部分也有可能引起类似的问题,像下面这样:

/*
**Get 10 integers
*/
pi = malloc(10 * sizeof(int));
/*
**Free only the last 5 integers; keep the first 5
*/
free(pi + 5);

释放一块内存的一部分是不允许的。动态分配的内存必须整块一起释放。但是,realloc函数可以缩小一块动态分配的内存,有效的释放它尾部的部分内存。

内存泄漏

当动态分配的内存不再需要使用时,它应该被释放,这样它以后可以被重现分配使用。分配内存但在使用完毕后不释放将引起内存泄漏。在那些所有执行程序共享一个通用内存池的操作系统中,内存泄漏将一点点地榨干可用内存,最终使其一无所有。要摆脱这个困境,只有重启系统。

其他操作系统能够记住每个程序当前拥有的内存段,这样当一个程序终止时,所有分配给它但未被释放的内存都归还给内存池。但即使在这类系统中,内存泄漏仍然是一个严重的问题,因为一个持续分配却一点不释放内存的程序最终将耗尽可用的内存。此时,这个有缺陷的程序无法继续执行下去,它的失败有可能导致当前已经完成的工作统统丢失。

#6. 内存分配实例

动态内存分配一个常见的用途就是为那些长度在运行时才知的数组分配内存空间。

排序一列整型值
#include <stdio.h>
#include <stdlib.h>


int compare_integers(void const *a, void const *b) {
    register int const *pa = (int*)a;
    register int const *pb = (int*)b;

    return *pa > *pb ? 1 : *pa < *pb ? -1 : 0;
}

int main() {
    int *array;
    int n_values = 0;
    int i = 0;

    printf("How many values are there?");

    if (scanf_s("%d", &n_values) != 1 || n_values <= 0) {
        printf("IIlegal number of values.\n");
        exit(EXIT_FAILURE);
    }

    /*
    **读取这些值
    */
    for (int i = 0; i < n_values; i++) {
        printf("? ");
        if (scanf_s("%d", array + i) != 1) {
            printf("Error reading value #%d\n", i);
            free(array);
            exit(EXIT_FAILURE);
        }
    }

    qsort(array, n_values, sizeof(int), compare_integers);

    /*
    **打印这些值
    */
    for (int i = 0; i < n_values; i++) {
        printf("%d\n", array[i]);
    }

    /*
    **释放内存并退出
    */
    free(array);

    system("pause");

    return EXIT_SUCCESS;
}
复制字符串
#include <stdio.h>
#include <stdlib.h>

char* strdup(char const *string) {
    char *new_string;

    /*
    **请求足够长度的内存,用于存储字符串和它的结尾NUL字节
    */
    new_string = malloc(strlen(string) + 1);

    /*
    **如果我们得到内存,就复制字符串
    */
    if (new_string != NULL) {
        strcpy(new_string, string);
    }

    return new_string;
}

输入被读入到缓冲区,每次读取一行。此时可以确定字符串的长度,然后就分配内存用于存储字符串。最后,字符串被复制到新内存。这样缓冲区又可以用于读取下一个输入行。

strdup函数返回一个字符串的拷贝,该拷贝存储于一块动态分配的内存中。函数首先试图获取足够的内存来存储这个拷贝,内存的容量应该比字符串的长度多一个字节,以便存储字符串结尾的NUL字节。如果内存分配成功,字符串就被复制到这块新内存。最后,函数返回一个指向这块内存的指针。注意,如果处于某些原因导致内存分配失败,new_string的值将为NULL。在这种情况下,函数将返回一个NULL指针。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容

  • 逻辑上的分区 栈区 堆区 静态区 常量区 代码区 代码区,常量区,静态区,堆区,栈区这个排列顺序按照地址由小到大排...
    MathCat阅读 614评论 1 0
  • 1. malloc函数 其中,形参n为要求分配的字节数。 如果函数执行成功,malloc返回获得内存空间的首地...
    云之君兮鹏阅读 1,880评论 4 10
  • 很多新学C语言的童鞋在用到动态内存分配的时候,对选择哪种分配函数及其有何区别搞不清楚,那么下文就认真的讲讲它们的种...
    Leon_Geo阅读 1,007评论 0 2
  • 《c和指针》阅读笔记 前言:为什么要使用动态内存分配?函数的局部变量会进行回收,相比于函数的局部变量,有什么好处呢...
    qinxing阅读 1,236评论 0 2
  • 一般当需要根据程序运行中的状态生成数组或结构之类的变量时,我们可以使用动态内存分配
    Dafanzi阅读 92评论 0 0