【C语言进阶】详解C语言动态内存管理

前言:

今天这篇博客将为大家讲解如何通过开辟动态内存,从而写出更加优秀的的程序。同时今天的内容对于以后想要继续学习c++的同学来说也尤为重要。那就让我们进入正题吧。

一、动态内存概述:

  1. 什么是动态内存:

在C/C++语言中,所谓动态内存分配,就是指程序执行的过程中动态分配或者回收存储空间的分配方法。动态内存分配不像数组等静态内存分配法师那样需要先分配存储空间,而是由系统根据程序的需要及时分配,且分配的大小就是程序要求的大小。

2.动态内存分配的意义:

我们在之前的学习过程中,在使用各种变量与数组等功能时,都需要从内存中开辟一片空间用于存放我们的数据,我们之前常用的两种内存开辟方式:

int a=20;
//在内存栈空间上开辟4个字节的空间;
char arr[10]={10};
//在栈空间上开辟10个字节的连续空间;

但是我们发现这两种开辟方式都有一些共同点:

①空间开辟的大小是固定

②数组在申明的时候必须指定数组的长度,它所需要的内存将会在编译时分配;

换句话说这两种开辟方式开辟的是静态内存分配

但是我们在日常的代码编写何程序使用过程中,对于空间的需求往往不仅仅局限于上述情况。更多的时候我们需要的空间大小只有在程序运行的时候才能知道。如此数组等在编译时开辟空间的方式无法满足我们的需求。

例如:

①通讯录创建联系人的数据data[1000],实际有联系人10。这种造成大量空间浪费。

②统续录创建联系人的数据data[10],后期有联系人30,这种就时初期开辟空间不足。

为了解决这样的问题,于是我们就需要使用一种更好的内存分配方式进行处理,动态内存分配应运而生。

二、常用的动态内存函数:

  1. malloc和free:

mallochanshu (memory allocate,即内存分配)的作用是:向内存的堆区申请空间用于存储数据,free函数的作用为释放使用malloc函数向堆区申请的空间,并将空间归还给内存的堆区空间,通过配合使用这两个函数可以从堆区申请(释放)动态内存空间。

①malloc函数:

我们先来看看malloc函数:

我们可以看到,malloc函数的使用格式为:

void* malloc(size_t size);

从它的使用格式中我们可以知道,该函数向堆区申请了一块连续的空间,同时返回的是这块空间的指针。如果开辟成功则返回一个指向开辟好空间的指针。开辟失败,则返回一个NULL指针。所以我们在使用时一定要仔细检查malloc函数的返回值。返回值类型时void*,即malloc函数并不了解开辟空间的类型,至于空间的具体类型将在使用时由用户自己决定。如果参数size为0,则malloc函数的形式标准为定义,将会取决于编译器。

②free函数:

我们可以看到,free的使用格式时:

void free(void* ptr);

不同的是,于malloc函数恰好相反,free函数的作用为释放动态开辟的内存,同时没有返回值。如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义。如果参数ptr是NULL指针,则free函数将什么都不会做。

③malloc函数与free函数的使用:

关于两个函数的实际使用,我们直接来看下面的这段代码:

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

int main()
{
    int*ptr=NULL;
    //初始化指针;
    ptr=(int*)malloc(40);
    //使用malloc函数动态申请40字节空间
    //同时因为指针ptr类型为int*,而malloc函数的返回类型为void*,所以使用强制类型转换
    int *p=ptr;
    //在此定义指针p的原因是:在之后使用中,指针p的指向会发生改变    
    //若不对初始化指针指向进行保存,将无法释放改变前与改变后之间的空间

    if(p==NULL)
    //malloc函数在动态空间开辟失败时返回空指针
    //即此处为空指针,说明动态内存空间申请失败
    {
        perror("malloc");
        return 1;
    }

    //没有结束说明指针不为空,动态内存申请成功
    int i=0;
    for(i=0;i<10;i++)
    {
        *p++=i;
        //循环向动态内存空间中存入数据并且指针p向后;
    }

    //使用完后释放空间
    free(ptr);
    //指针指向发生变化,但是指针ptr扔指向初始指针;
    ptr=NULL;
    //动态内存归还后,重新将指针ptr置空
    //动态内存空间已经归还,若不将指针ptr进行置空,指针ptr将变为野指针,指向将不可控,这在程序运行中非常危险
    return 0;
}

具体使用方法在注释中已经注明了,在这里我们要对其中几个地方进行再次强调:

  1. 在对指针进行操作前一定要保存指向初始位置的原始指针;

  1. 在对指向动态内存空间的指针进行使用前,一定要进行非空判断。

  1. 在动态内存空间使用完毕并释放后,一定要将指针进行置空处理。

  1. 程序结束时若动态内存空间没有被释放,将会被操作系统自动回收

  1. 但若程序不结束且申请的动态内存空间持续不归还,动态内存将不会被回收,就会导致内存泄漏问题。

2.calloc函数:

使用格式为:

void* calloc (size_t num, size_t size);

更通俗的说,calloc函数的功能就是,为num个大小为size的元素开辟一块动态内存空间,并将空间内每个字节都初始化为0;

使用方式举例:

int main()
{
    int*ptr=NULL;
    //初始化指针;
    ptr=(int*)calloc(10,sizeof(int))
    //使用calloc函数动态申请10个int类型大小的空间
    //同时因为指针ptr类型为int*,而malloc函数的返回类型为void*,所以使用强制类型转换
    int *p=ptr;
    //在此定义指针p的原因是:在之后使用中,指针p的指向会发生改变    
    //若不对初始化指针指向进行保存,将无法释放改变前与改变后之间的空间

    if(p==NULL)
    //malloc函数在动态空间开辟失败时返回空指针
    //即此处为空指针,说明动态内存空间申请失败
    {
        perror("malloc");
        return 1;
    }
//没有结束说明指针不为空,动态内存申请成功
    int i=0;
    for(i=0;i<10;i++)
    {
        *p++=i;
        //循环向动态内存空间中存入数据并且指针p向后;
    }

    //使用完后释放空间
    free(ptr);
    //指针指向发生变化,但是指针ptr扔指向初始指针;
    ptr=NULL;
    //动态内存归还后,重新将指针ptr置空
    //动态内存空间已经归还,若不将指针ptr进行置空,指针ptr将变为野指针,指向将不可控,这在程序运行中非常危险
    return 0;
}

calloc函数与malloc函数区别在于calloc返回开辟地址时候会把申请空间内的每个字节初始化为0;其他基本都是相同的。

3.realloc

realloc函数(re-allocate,即重新分配)的作用是:重新分配从堆区申请来的动态内存空间大小。使用格式为:void* realloc (void* ptr, size_t size);

前面malloc、free与calloc三个函数的存在,都是为了向堆区申情或释放动态内存空间,但是倘若只是用前三个函数,我们可以发现,申请来的动态内存空间看起来和静态的内存空间似乎没有多大的区别。

于是,reallo函数就出现了。realloc函数的出现,使得动态内存管理更加灵活。例如有些时候我们觉得前面申请的空间太小不够用时,又或者我们觉得申请的空间太过大浪费时,我们都可以用realloc重新分配动态内存空间。可以说,realloc函数才让动态内存空间真正动起来的。

我们可以直接修改上面的例子,当我们在使用过程中发现我们申请的空间过小时,就可以通过realloc函数来进行扩容:

int main()
{
    int*ptr=NULL;
    //初始化指针;
    ptr=(int*)calloc(10,sizeof(int))
    //使用calloc函数动态申请10个int类型大小的空间
    //同时因为指针ptr类型为int*,而malloc函数的返回类型为void*,所以使用强制类型转换
    int *p=ptr;
    //在此定义指针p的原因是:在之后使用中,指针p的指向会发生改变    
    //若不对初始化指针指向进行保存,将无法释放改变前与改变后之间的空间

    if(p==NULL)
    //malloc函数在动态空间开辟失败时返回空指针
    //即此处为空指针,说明动态内存空间申请失败
    {
        perror("malloc");
        return 1;
    }

//没有结束说明指针不为空,动态内存申请成功
    int i=0;
    for(i=0;i<10;i++)
    {
        *p++=i;
        //循环向动态内存空间中存入数据并且指针p向后;
    }
//假设此时空间不够用:
    realloc(ptr,80);
    //将ptr指向的空间扩大到80字节。

    //使用完后释放空间
    free(ptr);
    //指针指向发生变化,但是指针ptr扔指向初始指针;
    ptr=NULL;
    //动态内存归还后,重新将指针ptr置空
    //动态内存空间已经归还,若不将指针ptr进行置空,指针ptr将变为野指针,指向将不可控,这在程序运行中非常危险
    return 0;
}

但哪怕时在成功扩容时,也仍会出现两种情况:当前空间与后相邻的空间之间的空间是否足够realloc函数进行扩容:①如果足够,则会直接执行扩容操作,也就是原地扩容,并在扩容之后返回指向起始位置的指针。②不够的话,则会在堆区重新找一片合适空间进行扩容,并将原来空间的数据全部拷贝回来,接着释放空间,并在扩容完成之后返回指向新空间其实位置的指针。(异地扩容)

但是还有最特殊的情况,即内存堆空间中没有能够容纳整个扩容后的动态内存空间的空间时,将会返回空指针。所以,我们在想要使用扩容后的动态内存空间时,为了避免使用指针内容为空指针而造成的意外错误,在使用之前首先应当对 realloc 函数返回的指针进行非空判断,之后再拿来使用。

三、常见动态内存错误:

  1. 对NULL指针的解引用操作:

int main()
{
    int* p=(int*)malloc(INT_MAX);
    //当尝试开辟的空间过大时,将会导致动态内存开辟失败
    //此时malloc函数将会返回空指针,即此时的指针p也是空指针

    *p=29;
    //没有判断非空就对指针p进行解引用操作,导致产生对空指针进行了解引用操作
    //将会造成内存非法访问的错误

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

避免出现这类问题出现的方式:在指针使用前对malloc等函数的返回值进行非空判断。

2.对动态内存空间的越界访问:

int main()
{
    int* p=(int*)malloc(10*sizeof(int));
    if(NULL==p)
    {
        perror("malloc");
        return 1;
    }
    int i=0;
    for(i=0;i<=10;i++)
    {
        *(p+1)=i;
        //当i=0时,将会出现越界访问的错误;
    }
    free(p);
    p=NULL;
    return 0;
}

避免此类问题的方法是:在使用时仔细的进行内存边界的检查,并选择合适的空间访问范围。

3.对非动态内存空间使用free函数

int main()
{
    int a=10;
    int* p=&a;
    free(p);
//变量a所使用的内存空间非动态内存空间
    return 0;
}

避免此类错误的方法:在进行空间释放时注意区分静态内存空间与动态内存空间。

4.使用free函数释放动态内存空间的一部分:

int main()
{
    int* p=(int*)malloc(10*sizeof(int));
    if(NULL==p)
    {
        perror("malloc");
        return 1;
    }
    int i=0;
    for(i=0;i<=10;i++)
    {
        *p=i;
        p++;
        //此时指针p的指向已经发生改变
    }
    free(p);
    //释放部分动态内存空间
    p=NULL;
    return 0;
}

避免此类错误的方法是:在使用指针前保存好初始指向,并在进行动态内存释放时释放完整的动态内存空间。

5.对同一片动态内存空间多次释放:

int main()
{
    int* p=(int*)malloc(10*sizeof(int));
    free(p);
    //....
    free(p);
    //重复释放动态内存空间
    return 0;
}

避免此类错误的方法是:在已经释放过的动态内存空间之后,一定要加以注释以提醒自己避免重复释放同一块动态内存空间

6.不释放动态内存空间(内存泄漏):

void test()
{
    int* p=(int*)malloc(10*sizeof(int));
    if(NULL==p)
    {
        perror("malloc");
        return 1;
    }
    *p=20;
    //使用后没有释放动态内存空间,在程序终止前该动态内存空间将无法被释放,将会逐渐蚕食计算机系统的内存
}
int main()
{
    test();
    return 0;
}

避免此类错误的方法是:使用一个释放一个,特别是在函数里使用,使用结束后立刻释放。

总结:

至此我们就将动态内存管理的介绍与学习就结束了,通过今天的学习,相信大家都已经掌握了动态内存开辟、释放与动态修改,并且对动态内存空间的各项使用注意事项已经有了一定了解和认知。本文仍有许多不足之处,欢迎各位随时批评指正。

评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值