malloc、free、calloc 与 realloc


到目前为止,变量创建后会配置内存空间,这类资源是配置在内存的栈区(Stack),生命周期局限于函数运行期间,也就是函数执行过后,配置的空间就会自动清除。

若要将函数执行结果返回,不能直接返回这类被自动配置空间的地址,因为函数执行过后,该空间就会释出,函数调用者后续若透过地址取用这些资源,会发生不可预期的结果,例如,不能直接将局部创建的变量地址或数组地址返回。

然而程序运行后,资源之间的互用关系错综复杂,有些资源「无法预期」被使用的生命周期,因为无法预期,也就有赖于开发者自行管理内存资源,也就是开发者得自行在需要的时候配置内存,这些内存会被配置在堆区(Heap),不会自动清除,开发者得在不使用资源时自行释放内存。

要自行配置内存,C 可以使用malloc,它定义在 stdlib.h,举例来说,可以在程序中以动态方式配置一个int类型大小的内存,例如:

int *p = malloc(sizeof(int));

在这段程序中,malloc会配置一个int需要的空间,并返回该空间的地址,可以使用指针p来存储地址,就 C11 规范来说,malloc只配置空间但不初始化空间的值,若要在配置完成后默认为类型的空值,可以使用calloc

int *p = calloc(1, sizeof(int));

若要释放内存,可以使用free函数,以下使用一个简单的程序来示范动态配置的使用:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    int *p = malloc(100); 

    printf("空间地址:%p\n", p);
    printf("存储的值:%d\n", *p);

    *p = 200; 

    printf("空间地址:%p\n", p);
    printf("存储的值:%d\n", *p);

    free(p);

    return 0; 
}

执行结果:

空间地址:006F0D60
存储的值:7274688
空间地址:006F0D60
存储的值:200

动态配置的空间,在程序结束前不会自动归还,必须使用free释放空间,若大量动态配置而没有适当使用free的话,由于空间一直没有归还,最后将导致整个内存空间用尽。

如果想配置连续个指定类型的空间,可以如下:

int *p = malloc(sizeof(int) * 1000);

这段代码动态配置了 1000 个int大小的空间,并返回空间的第一个地址,配置后的空间数据是未知的,,可以使用calloc来定义空间配置,每个int空间会被始为 0,例如:

int *p = calloc(1000, sizeof(int));

配置的空间长度必须自行存储下来,因为没有任何方式,可以从p得知到底配置的长度是多少,配置得来的空间,在不使用时同样是使用free释放,方法如下:

free(p);

下面这个程序是个动态配置空间,并模拟为数组来操作的简单示范:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int size = 0;

    printf("输入长度:");
    scanf("%d", &size);

    int *arr = malloc(size * sizeof(int));

    printf("指定元素:\n");
    for(int i = 0; i < size; i++) {
        printf("arr[%d] = ", i);
        scanf("%d" , arr + i);
    }

    printf("显示元素:\n");
    for(int i = 0; i < size; i++) {
        printf("arr[%d] = %d\n", i, *(arr+i));
    }

    free(arr);

    return 0;
}

执行结果:

输入长度:3
指定元素:
arr[0] = 10
arr[1] = 20
arr[2] = 30
显示元素:
arr[0] = 10
arr[1] = 20
arr[2] = 30

若要动态配置连续空间,并当成二维数组来操作,就记得二维(或多维)数组,就是以数组的数组来实现,二维数组就是多段一维数组,如果你的二维数组有两段一维数组,那就是如下:

int **arr = calloc(2, sizeof(int*));

现在arr[0]arr[1]可以分别存储动态配置int*空间的地址,若每段要模拟的一维数组的长度是 3,可以如下动态配置,并将模拟的一维数组每个元素初始化设为 0 :

for(int i = 0; i < 2; i++) {
    arr[i] = calloc(3, sizeof(int));
}

来看一下简单的范例:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int **arr = calloc(2, sizeof(int*));
    for(int i = 0; i < 2; i++) {
        arr[i] = calloc(3, sizeof(int));
    }

    for(int i = 0; i < 2; i++) {
        for(int j = 0; j < 3; j++) {
            printf("%d ", arr[i][j]);
        }
        putchar('\n');
    }

    for(int i = 0; i < 2; i++) {
        free(arr[i]);
    }
    free(arr); 

    return 0;
}

记得最后要释放配置的空间时,也要如以上范例逐一释放,执行结果如下:

0 0 0
0 0 0

既然可以动态配置,那每段模拟的一维数组长度当然可以不一样啰!

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int **arr = calloc(2, sizeof(int*));
    arr[0] = calloc(3, sizeof(int));
    arr[1] = calloc(5, sizeof(int));

    for(int j = 0; j < 3; j++) {
        printf("%d ", arr[0][j]);
    }
    putchar('\n');

    for(int j = 0; j < 5; j++) {
        printf("%d ", arr[1][j]);
    }
    putchar('\n');    

    for(int i = 0; i < 2; i++) {
        free(arr[i]);
    }
    free(arr); 

    return 0;
}

执行结果:

0 0 0
0 0 0 0 0

如果要改变已配置的内存大小,可以使用realloc,例如:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int size = 0;

    printf("数组长度:");
    scanf("%d", &size);
    int *arr1 = calloc(size, sizeof(int));

    printf("指定元素:\n");
    for(int i = 0; i < size; i++) {
        printf("arr1[%d] = ", i);
        scanf("%d" , arr1 + i);
    }

    printf("显示元素:\n");
    for(int i = 0; i < size; i++) {
        printf("arr1[%d] = %d\n", i, *(arr1 + i));
    }

    int *arr2 = realloc(arr1, sizeof(int) * size * 2);
    printf("显示元素:\n");
    for(int i = 0; i < size * 2; i++) {
        printf("arr2[%d] = %d\n", i, *(arr2 + i));
    }

    printf("arr1 地址:%p\n", arr1);
    printf("arr2 地址:%p\n", arr2);

    free(arr2);

    return 0;
}

执行结果:

数组长度:3
指定元素:
arr1[0] = 10
arr1[1] = 20
arr1[2] = 30
显示元素:
arr1[0] = 10
arr1[1] = 20
arr1[2] = 30
显示元素:
arr2[0] = 10
arr2[1] = 20
arr2[2] = 30
arr2[3] = 0
arr2[4] = 1409286485
arr2[5] = 51325
arr1 地址:00650D60
arr2 地址:00650D60

要注意的是,上例中,重新配置后的地址并不保证相同,realloc会复制数据来改变内存的大小,若原地址有足够的空间,使用原地址调整内存的大小,若空间不足,会重新寻找足够的空间来进行配置,在这个情况下,realloc前旧地址的空间会被释放掉,也就是说,必须使用realloc返回的新地址,而不该使用旧地址,若realloc失败会返回空指针(null),因此最好对地址进行检查。

对于动态配置的内存,若有个指针是唯一指向资源地址,可以使用restrict修饰,例如:

int *restrict p = calloc(1, sizeof(int));

restrict修饰的指针,表示由开发者指示编译器,这个资源只由该指针访问,如此一来,编译器就有机会进行最佳化,唯一性是由开发者掌握,编译器不会检查被restrict修饰的指针,指向的资源是否被其他指针指向。

restrict对代码阅读上,也具有提醒开发者的作用,表示不该有其他指针存储相同资源的地址,在函数签署上,也可提示多个资源的地址必须是独立的,例如strcpy的签署,声明了destsrc必须是不同的:

char *strcpy( char *restrict dest, const char *restrict src );




展开阅读全文