什么是堆内存:
是进程的一个内存段(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页范围就不会产生段错误。