0%

动态内存管理

文章时效性提示

本文发布于 504 天前,部分信息可能已经改变,请注意甄别。

为什么存在动态内存分配

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

动态内存函数介绍

malloc

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#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);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#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函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#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)的解引用操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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;
}
  • 对开辟的空间进行越界访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#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
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main()
{
    int a = 10;
    int* p = &a;
    *p = 20;
    free(p);
    p = NULL;
    return 0;
}
  • 使用free释放一块动态开辟内存的一部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#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;
}
  • 对同一块动态内存多次释放
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#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置成了空指针,后面的释放就没有意义了,程序可以正常运行。

  • 动态开辟的内存忘记释放(内存泄露)
1
2
3
4
5
6
7
8
9
10
11
12
#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修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直至程序结束才销毁。

柔性数组

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

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
struct S
{
    int n;
    int arr[];//未知大小的
    //int arr[0];//未知大小的 - 和上面是一样的,只是写法不同
};
int main()
{
    struct S s;
}

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

柔性数组的大小

1
2
3
4
5
6
7
8
9
10
struct S
{
    int n;
    int arr[];//未知大小的
};
int main()
{
    struct S s;
    printf("%d\n",sizeof(s));
}

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

柔性数组的使用

柔性数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

优点:

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