C语言梦开始的地方19:动态内存管理

C语言梦开始的地方19:动态内存管理

在了解动态内存管理的知识之前我们先看一下这个:
C/C++中程序内存区域划分:
在这里插入图片描述

为什么要有动态内存分配

我们之前掌握的内存开辟方法:

int value = 20;
int arr[20] = {0};

但是这种开辟空间的方式有两个特点:

  • 空间开辟大小是固定的。
  • 数组在申明的时候,必须指定数组的长度,数组空间一旦确定了大小不能调整

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。

C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了。

  • 申请内存:malloccallocrealloc
  • 释放内存: free

malloc 和 free

malloc

C语言提供了一个动态内存开辟的函数:

void malloc(size_t size);
  • 这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
    1. 如果开辟成功,则返回一个指向开辟好空间的指针。
    2. 如果开辟失败,则返回一个NULL 指针,因此malloc的返回值一定要做检查。
    3. 返回值的类型是void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
    4. 如果参数size 为0,malloc的行为是标准是未定义的,取决于编译器。

free

C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:

void free(void* ptr);
  • free函数用来释放动态开辟的内存。
    1. 如果参数ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
    2. 如果参数ptr 是NULL指针,则函数什么事都不做。
  • mallocfree都声明在stdlib.h 头文件中。

malloc 和 free搭配使用:

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

int main()
{
    int num = 0;
    scanf("%d",&num);
    int *ptr = NULL;

    ptr = (int* ) malloc(num * sizeof(int)); // 申请num个int大小的内存空间

    if(ptr == NULL)
    {
        perror("malloc");
        return -1;
    }

    for(int i = 0; i < num;++i)
    {
        *(ptr + 1) = 0;
    }

    free(ptr);
    ptr = NULL; // 都free了,这一步是否还有必要

    return 0;
}

最后代码都释放了ptr申请的内存空间,还有必要再将ptr置为NULL吗?

有必要,这一步是为了防止释放后对指针的二次使用进行的冗余操作。因为释放内存后原指针就成了野指针,再将其置空是为了避免使用野指针。

calloc 和 realloc

calloc

C语言还提供了一个函数叫calloccalloc 函数也用来动态内存分配。原型如下:

void* calloc (size_1 t num, size_t size);
  • 函数的功能是为num 个大小为size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
  • 与函数malloc 的区别只在于calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
#include <stdio.h>
#include <stdlib.h>

int main()
{
    int num = 0;
    scanf("%d",&num);

    int *ptr = NULL;

    ptr = (int*)calloc(num,sizeof(int));

    if(ptr == NULL)
    {
        perror("calloc");
        return -1;
    }

    for(int i = 0; i < num;++i)
    {
        printf("%d ",*(ptr + i));
    }
    printf("\n");

    free(ptr);
    ptr = NULL;
    
    return 0;
}

realloc

  • relloc函数的出现让动态内存管理更加灵活。
  • 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。那realloc 函数就可以做到对动态开辟内存大小的调整。

函数原型如下:

void* realloc (void* ptr, size_t size);
  • ptr 是要调整的内存地址
  • size 调整之后新大小
  • 返回值为调整之后的内存起始位置。
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
  • realloc在调整内存空间的是存在两种情况:
    • 情况1:原有空间之后有足够大的空间
    • 情况2:原有空间之后没有足够大的空间

在这里插入图片描述
情况1
当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
情况2
当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。

所以为了避免后面没有足够多的空间时可以这样处理:

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

int main()
{
    int *ptr = NULL;
    ptr = (int *)malloc(100 * sizeof(int));
    if(ptr == NULL)
    {
        perror("malloc");
        return -1;
    }

    // 如果直接赋值,若失败则丢失原内存
    // ptr = (int*)realloc(ptr, 1000); 

    // 正确写法:
    int *temp = NULL;
    temp = (int* )realloc(ptr,1000 * sizeof(int));

    if(temp != NULL)
    {
        ptr = temp;
    }
    else{
        free(temp);
        temp = NULL;
        perror("realloc");
        return -2;
    }

    free(ptr);
    ptr = NULL;

    return 0;
}

上述就是C语言提供的开辟内存函数和释放内存函数的常见使用方法。

常见的动态内存的错误

对 NULL 指针解引用

void test()
{
    int *p = (int *)malloc(INT_MAX / 4); // 极可能失败
    *p = 10;                             // 若 p==NULL,程序崩溃
    free(p);
}

所以每次申请内存时都要进行判断,这样不仅能防止上述错误出现并且在后期调试时能快速定位到错误。

对动态开辟空间的越界访问

void test()
{
    int i = 0;
    int *p = (int *)malloc(10 * sizeof(int));
    if (NULL == p)
    {
        exit(EXIT_FAILURE);
    }
    for (i = 0; i <= 10; i++)
    {
        *(p + i) = i; // 当i是10的时候越界访问
    }
    free(p);
}

释放非动态内存

void test()
{
    int a = 10;
    int *p = &a;
    free(p);
}

只释放部分内存

void test()
{
    int *p = (int *)malloc(100);
    p++; 
    free(p); 
}

p++ 指向了第二个元素,再进行free释放时指向的就不是起始地址了

重复释放同一块内存

void test()
{
    int *p = (int *)malloc(100);
    free(p);
    free(p); // 重复释放
}

这里和free后指针置NULL是一个道理,freeptr就成了野指针,对野指针进行操作就会发生未定义行为,所以每次free后最好置NULL

内存泄漏(忘记释放)

void test()
{
    int *p = (int *)malloc(100);
    if (NULL != p)
    {
        *p = 20;
    }
}
int main()
{
    test();
    while (1);

    return 0;
}

忘记释放不再使用的动态开辟的空间会造成内存泄漏。
切记:动态开辟的空间一定要释放,并且正确释放。

动态内存经典题目

题目1:

void GetMemory(char *p)
{
    p = (char *)malloc(100);
}
void Test(void)
{
    char *str = NULL;
    GetMemory(str);
    strcpy(str, "hello world");
    printf(str);
}

结果:程序崩溃。
原因:C 函数参数是值传递,p 是 str 的副本,修改 p 不会改变 str。所以这里应该传二级指针或者返回指针。

题目2:

char* GetMemory() {
    char p[] = "hello world";  
    return p;                  
}
void Test() {
    char* str = GetMemory();
    printf(str);  
}

结果:打印乱码或崩溃(悬垂指针)。
原因:p开辟在栈上生命周期随着return p; 就被编译器收回了,此时接收就不知道是什么了。

题目3:

void GetMemory(char** p, int num) {
    *p = (char*)malloc(num);
}
void Test() {
    char* str = NULL;
    GetMemory(&str, 100);
    strcpy(str, "hello");
    printf(str);  // 打印 "hello"
}

结果:内存泄漏。
原因:未释放申请的内存

题目4:

void Test() {
    char* str = (char*)malloc(100);
    strcpy(str, "hello");
    free(str);                 
    if (str != NULL) {         
        strcpy(str, "world");  
        printf(str);
    }
}

结果:未定义行为,可能崩溃也可能正常打印。
原因freestr 成为野指针。free 不会修改 str 的值,所以 if(str != NULL) 恒真,但访问已释放内存是不安全的。解决方法就是free后置NULL

柔性数组

在我们了解数组章节时讲到过一个知识点就是 柔性数组柔性数组是C99推出的新语法。
C99 允许结构体的最后一个成员是未知大小的数组,这就叫柔性数组成员。
例如:

struct st_type {
    int n;
    int a[0]; // 柔性数组成员
};

有些编译器会报错无法编译可以改成:

struct st_type {
    int n;
    int a[]; 
};

柔性数组的特点

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

简单的理解就是sizeof(st_type);时柔性数组不参与计算,使用malloc时额外分配空间给数组使用。

示例:

typedef struct st_type
{
    int i;
    int a[0]; // 柔性数组成员
} type_a;

int main()
{
    printf("%d\n", sizeof(type_a)); // 输出的是4
    return 0;
}

柔性数组的使用

那么在动态内存管理中有什么用呢?我们看下面这个示例:

typedef struct st_type
{
    int i;
    int a[0]; // 柔性数组成员
} type_a;

int main()
{
    int i = 0;
    type_a *p = (type_a *)malloc(sizeof(type_a) + 100 * sizeof(int));
    // 业务处理
    p->i = 100;
    for (i = 0; i < 100; i++)
    {
        p->a[i] = i;
    }
    free(p);
    return 0;
}

那么上述代码的优势在哪里呢?我们再看一下这个代码:

typedef struct st_type
{
    int i;
    int *p_a;
} type_a;

int main()
{
    type_a *p = (type_a *)malloc(sizeof(type_a));
    p->i = 100;
    p->p_a = (int *)malloc(p->i * sizeof(int));
    // 业务处理
    for (int i = 0; i < 100; i++)
    {
        p->p_a[i] = i;
    }
    // 释放空间
    free(p->p_a);
    p->p_a = NULL;
    free(p);
    p = NULL;
    return 0;
}

我们对比两个代码,我们会发现代码1申请内存更方便并且释放时也只需要释放一次。
代码2内存申请了2次这样就可能导致内存不连续,而代码1申请了1次那么代码1的内存一定是连续的,这样在后续搜索时性能代码1会更优。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值