第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 

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


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 
#include 

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指针。

你可能感兴趣的:(第11章:动态内存分配)