动态内存管理

为什么存在动态内存分配

  • 之前开辟空间大小是固定的
  • 数组在创建时必须制定数组长度。
    动态内存分配,解决上面的问题。可以让内存分配更灵活。

动态内存函数介绍

malloc

动态内存开辟。需要引用stdlib.h头文件。

void* malloc(size_t size);
malloc会返回一个指针指向开辟好的空间,当申请空间失败时,会返回一个NULL。
注意⚠️:使用这个函数,一定要检查是否申请成功,因为它可能申请失败。

12345678910111213141516171819202122232425262728
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <errno.h>int main(){    //向内存申请10个整型空间    int* p = (int*)malloc(10*sizeof(int));    if (p==NULL) //检查是否申请成功    {        printf("%s\n",strerror(errno));    }    else    {        //开辟成功,可以使用空间        int i = 0;        for (i=0; i<10; i++)         {            *( p + i ) = i;        }        for (i=0; i<10; i++)         {            printf("%d ",*(p+i));        }    }    //当动态申请的空间不使用的时候,要还给操作系统。使用free函数。    return 0;}

打印的结果是0 1 2 3 4 5 6 7 8 9

把申请10个int型的空间修改为int* p = (int*)malloc(INTMAX_MAX);时,返回Cannot allocate memory没有足够的空间。

如果申请的size空间为0,这种行为是标准未定义的,取决于编译器。(不要这样写)

free

对动态开辟的空间进行回收和释放。
void free(void *memblock);

12345678910111213141516171819202122232425
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <errno.h>int main(){    //向内存申请10个整型空间    int* p = (int*)malloc(40);    if (p==NULL) {        printf("%s\n",strerror(errno));    }else    {        //开辟成功,可以使用空间        int i = 0;        for (i=0; i<10; i++) {            *( p + i ) = i;        }        for (i=0; i<10; i++) {            printf("%d ",*(p+i));        }    }    free (p);//释放申请的空间    p = NULL;//将p赋值为空指针    return 0;}

free (p);将空间释放。但是p的值是没有改变的。
使用free释放空间后最好运行:p = NULL;

free的空间必须是动态开辟出来的,如果不是动态开辟出来的,这种行为是标准未定义的。

当程序结束时,即使没写free函数,操作系统也会将空间回收♻️。

calloc

开辟空间,并把元素改成0。
void *calloc(size_t num,size_t size);
size_t num是元素个数,size_t size是每个元素的大小(字节)。
开辟空间,用指针返回,空间不足时,返回空指针NULL。

1234567891011121314151617181920212223
#include <stdio.h>#include <libc.h>#include <errno.h>  int main(){    int* p = calloc(10, sizeof(int));    if (p==NULL)     {        printf("%s\n",strerror(errno));    }    else    {        int i = 0;        for (i = 0; i<10; i++)         {            printf("%d ",*(p+i));        }    }    //释放空间,用free函数。    free(p);    p = NULL;    return 0;}

打印的结果是0 0 0 0 0 0 0 0 0 0

calloc和malloc的区别

希望开辟的空间内容为0,使用calloc;
希望开辟的空间不要初始化,使用malloc;

realloc

realloc让动态开辟的空间更灵活。
可以调整动态开辟空间的内存大小。

void *realloc(void *memblock,size_t size);
void *memblock是之前开辟好的动态空间,size_t size是新的大小(字节)。

返回一个指针指向新开辟的内存块,

realloc使用注意事项:

  • 如果p指向的空间之后有足够的内存空间,直接追加内存,后返回p
  • 如果p指向的空间之后没有足够的内存空间,则realloc会重新找一块新的内存区域,开辟一块新的满足需求的空间,并把原先内存中的拷贝至新内存空间,释放旧内存空间,最后返回新开辟的内存空间地址。
  • 使用新变量来接受realloc的返回值,因为它可能申请失败,使用新变量来避免之前保存的内容丢失。
  • 当动态申请的空间不使用的时候,要还给操作系统。使用free函数。
12345678910111213141516171819202122232425262728293031323334353637383940414243
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <errno.h>int main(){    int *p = (int*)malloc(20);    if ( p == NULL )    {        printf("%s\n",strerror(errno));    }    else    {        int i = 0;        for (i=0; i<5; i++) {            *(p+i) = i;        }    }    //上面是使用malloc开辟的空间,20字节    //20个字节不够了,想要40个字节,使用realloc调整    int* p2 =  realloc(p, 40);    if (p2 == NULL)    {        //追加失败        printf("%s\n",strerror(errno));    }    else    {        //追加成功        p = p2;        int i = 0;        for (i=5; i<10; i++) {            *(p+i) = i;        }        for (i=0; i<10; i++) {            printf("%d ",*(p+i));        }    }    //释放内存    free(p);    p = NULL;    return 0;}

常见的动态内存错误

  • 对空指针(NULL)的解引用操作
1234567891011121314151617
#include <stdio.h>#include <stdlib.h>#include <string.h>int main(){    int *p = (int*)malloc(40);    //万一malloc失败了,p会被赋值为NULL    //所以使用malloc返回后必需要进行判断!    int i = 0;    for (i=0; i<10; i++)    {        *(p+i) = i;    }    free(p);    p = NULL;    return 0;}
  • 对开辟的空间进行越界访问
12345678910111213141516171819202122
#include <stdio.h>#include <stdlib.h>#include <string.h>int main(){    int *p = (int*)malloc(5 * sizeof(int));    if (p==NULL) {        return 0;    }    else    {        int i = 0;        for (i=0; i<10; i++)//只开辟了5个整型的元素,越界访问        {            *(p+i) = i;        }    }    //释放    free(p);    p = NULL;    return 0;}
  • 对非动态开辟的空间使用free
12345678910
#include <stdio.h>int main(){    int a = 10;    int* p = &a;    *p = 20;    free(p);    p = NULL;    return 0;}
  • 使用free释放一块动态开辟内存的一部分
1234567891011121314151617181920
#include <stdio.h>#include <stdlib.h>#include <string.h>int main(){    int* p = (int*)malloc(40);    if (p == NULL)    {        return 0;    }    int i = 0;    for (i=0; i<10; i++)    {        *p++ = i;    }    //回收空间    free(p);//p改变了,经过了10次++,不再是原先的开辟的空间    p = NULL;    return 0;}
  • 对同一块动态内存多次释放
123456789101112131415161718
#include <stdio.h>#include <stdlib.h>#include <string.h>int main(){    int* p = (int*)malloc(40);    if (p==NULL)    {        return 0;    }    //使用    //释放    free(p);    //p = NULL;    //...    free(p);    return 0;}

为避免这种情况,谁开辟,谁回收。
但只要第一是释放之后,把p置成了空指针,后面的释放就没有意义了,程序可以正常运行。

  • 动态开辟的内存忘记释放(内存泄露)
123456789101112
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>int main(){    while (1)    {        malloc(1);        }    return 0;}

C/C++程序的内存开辟

C/C++程序内存分配有4个区域

  • 栈区(stack):在执行函数时,函数内局部变量的存储单元在栈上创建,函数结束时,被释放。栈区主要存放运行函数分配的局部变量、函数参数、返回数据、返回地址等。
  • 堆区(heap):一般由程序员分配释放,程序员不释放会由OS回收。分配方式类似链表
  • 数据段(静态区)(static):存放全局变量、静态数据。程序结束后由系统释放。
  • 代码段:存放函数体的二进制代码

普通的局部变量是在栈区分配空间的,栈区的特点就是创建的变量出了作用域就销毁了。
被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直至程序结束才销毁。

柔性数组

结构中最后一个元素允许是未知大小的数组,这就是柔性数组成员

1234567891011
#include <stdio.h>struct S{    int n;    int arr[];//未知大小的    //int arr[0];//未知大小的 - 和上面是一样的,只是写法不同};int main(){    struct S s;}

上面结构体S中的整型arr数组是未知大小的,就是柔性数组成员,柔性的意思就是:数组大小是可以调整的。

柔性数组的大小

12345678910
struct S{    int n;    int arr[];//未知大小的};int main(){    struct S s;    printf("%d\n",sizeof(s));}

打印的结果是4。
包含柔性数组成员的结构体计算大小时,不包含柔性数组成员的大小。

柔性数组的使用

柔性数组:

1234567891011121314151617181920212223242526272829303132
struct S//先创建一个包含柔性数组成员的结构体{    int n;    int arr[];//未知大小的};int main(){    struct S* ps = (struct S*)malloc(sizeof(struct S)+5*sizeof(int)) ;//开辟了24个字节,强制转换为struct S*型。其中有4字节空间给n,另外20字节给arr数组。    ps->n = 100;//令n=100    int i = 0;    for (i=0; i<5; i++) //给这个柔性数组中的5个成员赋值    {        ps->arr[i] = i;    }    struct S* ptr = realloc(ps, 44);//想开辟另外20字节的空间给这个柔性数组    if (ptr != NULL) //判断空间是否申请成功    {        ps = ptr;    }    for (i=5; i<10; i++) //给柔性数组中新开辟的5块arr数组赋值    {        ps->arr[i] = i;    }    for (i=0; i<10; i++) //打印这个arr柔性数组    {        printf("%d",ps->arr[i]);    }    //释放空间    free(ps);    ps = NULL;    return 0;}

使用指针维护的结构体数组数组(和柔性数组实现的功能一致):

1234567891011121314151617181920212223242526272829303132333435363738
struct S{    int n;    int *arr;//指针 - 指向一块动态开辟的空间};int main(){    struct S *ps = (struct S*)malloc(sizeof(struct S));//开辟一块空间,包括n和*arr指针的空间,ps指针指向,强转为(struct S*)型    ps->arr = malloc(5*sizeof(int));//给*arr这个指针指向的地址开辟5块int大小的空间    int i = 0;    for (i=0; i<5; i++) //给数组内的5个成员赋值    {        ps->arr[i] = i;    }    for (i=0; i<5; i++) //打印数组内的五个成员    {        printf("%d ",ps->arr[i]);    }    int *ptr = realloc(ps->arr, 10*sizeof(int));//调整arr数组的大小,先用ptr指针指向    if (ptr != NULL) //判断内存申请是否成功    {        ps->arr = ptr;    }    for (i=5; i<10; i++) //给新创建的五个数组成员赋值    {        ps->arr[i] = i;    }    for (i=0; i<10; i++) //打印所有数组成员    {        printf("%d ",ps->arr[i]);    }    //释放内存    free(ps->arr);//先释放arr数组的内存,避免找不到它    ps->arr = NULL;    free(ps);//后释放ps。    ps = NULL;    return 0;}

柔性数组的特点

  • 结构中的柔性数组成员前面必须至少有一个其他成员
  • sizeof返回的这种结构大小不包括柔性数组的内存
  • 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

优点:

  • 方便内存释放
  • 有利于访问速度
🔍 ×