C语言的堆内存管理

什么是堆内存:

是进程的一个内存段(text、data、bss、heap、stack),由程序员手动管理

特点就是足够大,缺点就是使用麻烦,比较危险。

为什么要使用堆内存:

  • 随着程序变复杂,数据量开始变多。

  • 其它内存段的申请和释放不受控制,堆内存的申请释放受程序员控制。

如何使用堆内存:

C语言中没有管理堆内存的语句,C标准库中提供一套管理堆内存的函数,这些函数底层封装了各操作系统的堆内存管理接口,所以可以跨平台使用,这些函数声明在 stdlib.h 头文件中。

malloc函数:
void *malloc(size_t size);
功能:向malloc申请size字节的堆内存块
size:
    要申请的内存块字节数
    如果申请数组形式的内存块,size=sizeof(数组元素类型)*数组长度
返回值:
	如果申请成功,则返回内存块首地址
    失败申请失败,则返回NULL,例如现在有堆内存无法满足size个字节的需求
    
// 如果size的值过大,就会申请失败
int main(int argc,const char* argv[])
{
    int* p = malloc(0xffffffff);                   
    printf("%p\n",p); // (nil)
}

    
// 向malloc申请n字节内存
int main(int argc,const char* argv[])
{
    int* p = malloc(4);
    printf("请输入一个整数:");
    scanf("%d",p);
    printf("%d\n",*p);  
}

// 向malloc申请一块 一维数组内存 
int main(int argc,const char* argv[])
{
    int len;
    printf("请输入数组的长度:");
    scanf("%d",&len);

    int* arr = malloc(sizeof(int)*len);
    for(int i=0; i<len; i++)
    {   
        printf("%d ",arr[i]);
    }   
}

// 向malloc申请一块 二维数组内存
int main(int argc,const char* argv[])
{
    int row,col;
    printf("请输入二维数组的行数和列数:");
    scanf("%d%d",&row,&col);

    //	数组指针 规则的二维数组
    int (*arr)[col] = malloc(sizeof(int)*row*col);
    for(int r=0; r<row; r++)
    {   
        for(int c=0; c<col; c++)
        {   
            printf("%d ",arr[r][c]);
        }   
        printf("\n");
    }   
    
    //	指针数组 不规则的二维数组
    int* arr[col] = {};
    for(int r=0; r<row; r++)
    {
        for(int c=0; c<col; c++)
        {
            arr[r] = malloc(sizeof(int)*col);	
        }
    }
}
注意:

1、使用malloc申请到的内存块,里面的内容是不确定的,malloc不会帮我们初始化,可以使用bzero,memset函数进行。

2、如果size等于0,返回NULL或唯一个的地址,并且该地址可以通过free释放而不出错,但不能使用它指向的内存。

calloc函数:
void *calloc(size_t nmemb, size_t size);
功能:申请nmemb个size个字节的内存块,专门用于申请数组型的内存块。
nmemb:数组的长度
size:数组元素的字节数
返回值:与malloc相同
注意:使用calloc申请的内存块,所有字节会被初始化0。
    
int main(int argc,const char* argv[])
{
    int len;
    printf("请输入数组的长度:");
    scanf("%d",&len);
                                                                                          
    int* arr = calloc(len,sizeof(int));
    for(int i=0; i<len; i++)
    {   
        // 输出结果肯定是0
        printf("%d ",arr[i]);
    }   
}
注意:

1、calloc所申请也是一块连续的内存块,所以nmemb和size的参数位置可以调换,就相当于calloc内部调用了malloc函数,只是比malloc多了初始化步骤,而且比malloc的可读性更高。

2、malloc比calloc申请内存的速度快,或者使用malloc+bzero配合。

free函数:
void free(void *ptr);
功能:释放堆内存 
ptr:要释放的内存块的首地址,它必须是malloc、calloc函数的返回值
    
注意:
int main(int argc,const char* argv[])
{
    int len = 20; 
    int* arr = malloc(sizeof(int)*len);
    for(int i=0; i<len; i++)
    {   
        arr[i] = 0x01020304;
        printf("%d ",arr[i]);
    }   
    printf("\n");

    // 释放掉使用权,并破坏一部分内容                             
    free(arr);

    // 非法使用
    for(int i=0; i<len; i++)
    {   
        printf("%d ",arr[i]);
    }   
    printf("\n");
}
注意:

1、free释放的是使用权,只破坏内存块的一部分内存,大部分数据还在,这样设计的原因是释放速度比较快,就像在硬盘上删除文件一样,只是把存储文件那片区域的使用释放旧,数据还存储在磁盘上,这也是我们能进行数据恢复的原因。

2、free的参数可以是空指针,不会出现错误,也不会执行任何操作,这也是空指针比野指针安全的原因。

3、如果内存被重复释放则会出现"double free or corruption (fasttop)",程序会异常停止,所以在第一次释放内存后,要把与堆内存配合的指针及时的赋值为空,防止重复释放产生的错误。

realloc函数:
void *realloc(void *ptr, size_t size);
功能1:把已有的堆内存块调小
    ptr是malloc、calloc、realloc的返回值,也就已有堆内存块首地址
    size < oldsize 
    此时不需要关心realloc的返回值
int main(int argc,const char* argv[])
{
    int* p1 = malloc(1024);
    p1 = realloc(p1,10);
    int* p2 = malloc(1024);
    printf("%p %p\n",p1,p2);
}
    
功能2:把已有的堆内存块调大
    ptr是malloc、calloc、realloc的返回值,也就已有堆内存块首地址
    情况1:如果ptr后续的内存没有被占用,realloc会在ptr的基础上进行扩大
    情况2:如果ptr后续的内存已经被占用,realloc会重新分配一块符合要求的内存块,并把ptr上的内容拷贝到新的内存块,然后释放ptr,再返回新内存块的首地址
   	使用此功能时,我们必须重新接收realloc函数的返回值,我们无法预料realloc执行的是情况1还是情况2。
// 情况1
int main(int argc,const char* argv[])
{                                                                         
    int* p1 = malloc(40);
    int* p2 = realloc(p1,80);
    // 此时p1 == p2,是在p1的基础上直接扩大的
    printf("%p %p\n",p1,p2);
}
// 情况2
int main(int argc,const char* argv[])
{
    int* p1 = malloc(40);
    for(int i=0; i<10; i++)
        p1[i] = i+1;
    
    // 占了p1r的后续位置,realloc就不能在p1的基础上直接扩展了
    int* tmp = malloc(4);
    
    int* p2 = realloc(p1,80);
    // 此时p1 != p2,重新分配了一块内存,先把p1的内容拷贝新内存块,并把p1释放掉了,然后返回新内存块的地址
    printf("%p %p\n",p1,p2);
    for(int i=0; i<10; i++)
    {
        printf("%d %d\n",p1[i],p2[i]);
    }
}
    
功能3:释放内存
    ptr是malloc、calloc、realloc的返回值
    0==size,此时realloc的功能就相当于free
    
功能4:申请内存
    NULL==ptr,0<size
    此时的功能就相当于malloc
注意:

虽然realloc具有释放和申请堆内存的功能,但我们一般不使用,而是直接使用malloc和free,主要使用的是realloc的调整内存块大小的功能。

堆内存越界时为什么超过135160才会出现段错误?

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

int main(int argc,const char* argv[])
{
    char* ptr = malloc(1);
    // 只要越界的不超过135160,就不会出现段错误
    printf("%c\n",ptr[135160]);     
    //printf("%c\n",ptr[1024*33-2]);  
}

1、当程序首次向malloc申请内存时,此时malloc手里没有堆内存可分配,malloc会向操作系统申请堆内存,操作系统会一次性分配33页内存交给malloc管理(一页内存=4096个字节),之后再向malloc申请内存时,malloc会从这33页内存中分配给调用者。

但这不意味着可以越界访问,因为malloc把使用分配给"其他人",这样会产生脏数据。

2、使用malloc申请的每个内存块前面会有4~12个字节的空隙,malloc会根据所申请的内存块的大小自动调整空隙的大小。

3、内存块前面的空隙有两部分:

空隙的前0~8字节:用于内存对齐(提高内存访问速度)

空隙的末尾4字节:也就是内存块前面的4字节,存储首malloc的管理信息,这块信息被破坏会影响后续malloc、free、printf、scanf函数的使用。

为什么是135160会出现段错误:

操作系统交给malloc33页内存(135168个字节),可访问的范围是0~135167,malloc会预留8个字节的空隙,返回给程序的是33页内存的第9个字节的地址(33页内存还剩135160个字节),所以可访问的范围是0~135159,只要在这个范围就不会出现段错误。

这种分配机制的优点:

1、避免了频繁打扰操作系统,而影响操作系统的速度。

2、段错误产生的原因是被操作系统发现非法使用内存,所以我们使用malloc分配的内存越界时,只要不超过33页范围就不会产生段错误。

  • 11
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值