动态内存管理

本文详细介绍了C/C++中的动态内存管理,包括malloc、calloc、realloc和free函数的使用,以及动态内存错误的种类,如空指针解引用、越界访问、释放非动态内存和内存泄漏等。此外,还讨论了C99中的柔性数组特性和使用注意事项,强调了良好内存管理习惯的重要性。
摘要由CSDN通过智能技术生成
一、为什么存在动态内存管理?
已知的开辟空间方式特点有:
1.空间开辟的大小是固定的
2.数组在声明时,必须指定数组长度,它所需要的内存在编译时分配
但是对空间的需求不仅仅是上述的情况,有时我们需要的大小只有在程序运行时才能知道,那之前的空间开辟方式就不适合了。这时就需要动态内存开辟了。
动态内存的开辟发生在堆区。
二、动态内存函数的介绍
2.1malloc和free
动态内存开辟函数malloc:void* malloc(size_t  size);
头文件:#include<stdlib.h>
返回值是指向该空间的指针,又因为这块空间不知道用来存储什么变量,所以返回类型是void*,size是开辟空间的大小,单位是字节。如果开辟空间失败,即没有足够的空间来开辟,返回空指针。
开辟一个存放10个int变量的空间:int*  p = (int*)malloc(40);
动态内存释放函数free:void  free(void* ptr);
头文件:#include<stdlib.h>
当不再使用这块空间,使用free函数释放该空间,free(p);即释放后p变成野指针。所以应该在释放后立即将p置为空指针。
如果不进行free操作,那么这块空间将在程序结束后才释放。在程序运行时,这块空间永远被占用且不使用,这种情况叫内存泄漏。自己申请的空间在不用时就释放,这是一个良好的习惯。
2.2calloc
特殊动态内存开辟函数calloc:void*  calloc(size_t  num , size_t  size);
num是开辟元素的个数,size是每个元素的大小。返回值是开辟空间的起始地址。并且将开辟的空间全部初始化为0。开辟失败返回空指针。
2.3realloc
可以开辟空间,也可以调整空间。
realloc:void*  realloc(void*  ptr , size_t  size);
ptr是所开辟空间的起始地址。如果传空指针其功能和malloc一样。size是改变后的空间大小,单位是字节。返回的是开辟空间的起始地址。
注意:返回的值可能和原来的值(ptr)不一样,原因是:在开辟空间后内存也要使用,会让开辟好的空间后面部分空间被使用,这时如果用realloc调整空间,且原来开辟空间后面的大小已经不能满足新空间的需求,就会找到一块足够大的空间来开辟,并将原来空间中的内容复制到新空间中,再释放原来的空间。如图所示:
还有一种情况,使用realloc进行增容,增容失败,返回空指针;那么将连原来的空间都丢失。所以realloc的使用不能简单的赋值给指针(int* p = (int*)realloc(p , 40);)而是:
int* ptr = (int*)realloc(p , 40);
if(NULL != ptr)
{
    p = ptr;
}
三、常见的动态内存错误
3.1对NULL的解引用操作
怎么会对空指针进行解引用操作呢?
int  main()
{
    int*  p = (int*)malloc(INT_MAX);
    //INT_MAX是整型最大值,INT_MAX需要引用头文件#include<limits.h>才能使用
    //INT_MAX过大,开辟失败,返回空指针
    int  i = 0;
    for(i = 0 ; i < 10 ; i++)
    {
        *(p+i) = i;
    }
    return  0;
}
这种情况下就会造成对空指针进行解引用。
所以要判断是否返回空指针,如果返回直接return 0;(不一定要return 0;但要考虑返回空指针的情况,并采取应对措施)
3.2对动态开辟空间的越界访问
int  main()
{
    char* p = (char*)malloc(10*sizeof(char));
    if(NULL == p)
    {
        printf("%s\n" , strerror(errno)); //errno的头文件是#include<errno.h>
        return  0;
    }
    int i = 0;
    for(i = 0 ; i <= 10 ; i++) //越界使用了,只开辟了10字节空间,却使用了11字节
    {
        *(p+i) = 'a' + i;
    }
    free(p);
    p = NULL;
    return  0;
}
3.3对非动态内存开辟的空间用free释放
int main()
{
    int i = 0;
    int* p = &i;
    free(p);
    p = NULL;
    return 0;
}
在这种情况下会报错,free释放的是堆区的空间,而p是在栈区开辟的空间。
3.4使用free释放动态开辟内存的一部分
int  main()
{
    int* p = (char*)malloc(10*sizeof(int));
    if(NULL == p)
    {
        printf("%s\n" , strerror(errno));
        return  0;
    }
    int i = 0;
    for(i = 0 ; i < 5 ; i++)
    {
        *p = 1 + i;
        p++;
         //p++指针指向的位置发生了变化,指向了5后面的空间,之后free释放时的指针就不是动态内存开辟空间的首地址了
        //free只能一次将开辟的内存释放干净,不能释放一部分,保留一部分。
    }
    free(p);
    p = NULL;
    return  0;
}
3.5对同一块动态内存的多次释放
int  main()
{
    int* p = (char*)malloc(10*sizeof(int));
    if(NULL == p)
    {
        printf("%s\n" , strerror(errno));
        return  0;
    }
    int i = 0;
    for(i = 0 ; i < 5 ; i++)
    {
        *(p+i) = 1 + i;
    }
    free(p);
    
    free(p);
    //对同一块空间释放两次会有问题,会报错
    //如果释放p后,立即置为空指针,就不会报错,NULL指向无效空间,free(NULL);什么都不会发生
    return  0;
}
3.6动态内存开辟后忘记释放(内存泄漏)
void test()
{
    int*p = (int*)malloc(100);
    if(NULL == p)
    {
        return ; //函数返回类型为void时也可以有return,但是不能有返回值
    }
    //忘记释放就会出现内存泄漏的问题。
}
int main()
{
    test();
    return 0;
}
解决方案:谁申请的空间谁释放
四、几个经典笔试题
4.1运行下面程序会发生什么
void GetMemory(char *p)
{
    p = (char *)malloc(100);
}
void Test(void)
{
    char *str = NULL;
    GetMemory(str);
    strcpy(str, "hello world");
    printf(str);
    //关于这样的打印方式是合理的
    //有这样的赋值方式char* p = "hello world";  printf("hello world"); printf函数打印常量字符串是根据常量字符串首地址
    //那么直接将首地址给printf函数打印没有问题,但仅限于字符串。
}
int  main()
{
    Test();
    return 0;
}
运行结果:程序崩溃
崩溃原因:GetMemory函数的形参p是str的临时拷贝,str指向NULL,malloc开辟内存后返回内存的起始地址,地址赋值给p,GetMemory函数结束后p作为栈区上的变量被释放,没有返回给str,所以str还是指向NULL。对NULL进行赋值会造成非法访问,导致程序崩溃。
还有,没有对malloc开辟的空间进行内存释放。
4.2下面程序运行会发生什么
char *GetMemory(void)
{
    char p[] = "hello world";
    return p;
}
void Test(void)
{
    char *str = NULL;
    str = GetMemory();
    printf(str);
}
int main()
{
    test();
    return 0;
}
运行结果:打印未知字符串
原因:函数GetMemory中的数组p,在函数结束时空间释放了。再返回栈空间的地址,地址就变成了野指针。
4.3下面程序运行会发生什么
void GetMemory(char **p, int num)
{
    *p = (char *)malloc(num);
}
void Test(void)
{
    char *str = NULL;
    GetMemory(&str, 100);
    strcpy(str, "hello");
    printf(str);
}
int main()
{
    Test();
    return 0;
}
问题:忘记内存释放
4.4运行Test有什么结果
void Test(void)
{
    char *str = (char *) malloc(100);
    strcpy(str, "hello");
    free(str);
    if(str != NULL)
    {
        strcpy(str, "world");
        printf(str);
    }
}
int  main()
{
    Test();
    return  0;
}
问题:因为之前已经对str指向的空间进行释放,不能再次使用,会形成非法访问。
五、C/C++程序的内存开辟
C/C++内存分配的几个区域
1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
六、柔性数组
c99中,结构体的最后一个元素允许是未知大小的数组,这就叫做柔性数组的成员
例如:
typedef  struct  st_type
{
    int  a;
    int  a[0]; //柔性数组成员,大小未指定
}type_s;
还有一种写法
typedef  struct  st_type
{
    int  a;
    int  a[ ]; //柔性数组成员,大小未指定
}type_s;
有时其中一种写法不支持,但两个必有一个写法是支持的
6.1柔性数组的特点
1.结构体中柔性数组成员前必须先至少包含一个其他成员。
2.当sizeof计算柔性数组结构体大小时,柔性数组是不参与计算的。
3.包含柔性数组成员的结构体用malloc()函数进行结构体的动态内存分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小。
6.2柔性数组的使用
柔性数组变量的创建
struct  s1
{
    int  a;
    int  arr[0];
};
int  main()
{
    //柔性数组的创建不能简单的struct  s1  a;
    //柔性数组是c99中的
    struct  s1* p = (struct  s1*)malloc(sizeof(struct  s1) + 40);
    //a[0]部分可以用realloc来改变,这就是柔性数组称为柔性的原因
    p->a = 10;
    int i = 0;
    //柔性数组的使用
    for (i = 0; i < 10; i++)
    {
        p->arr[i] = i;
    }
    //增容
    //在增容时,开辟的动态内存要么一次释放完,要么一次创建完,不可以说只增容结构体中柔型数组部分
    struct s1* ptr = (struct  s1*)realloc(p , sizeof(struct  s1) + 80);
    if (NULL == ptr)
    {
        return  0;
    }
    else
    {
        p = ptr;
    }
    //增容后的使用部分省略
    free(p);
    p = NULL;
    return  0;
}
疑问:开辟的空间是否对齐?
这里的变量a和柔性数组a都在堆上。结构体中为变量a和指针a也可以达到类似的效果,从设计上,柔性数组更优。用指针的方案需要的开辟和释放次数更多。空间开辟的次数多,会产生较多的内存碎片(内存中开辟空间之间的空隙),导致内存利用率不高。
柔性数组是c99中的语法。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值