什么是堆内存:
是进程的一个内存段(text代码段\date数据段\bss\heap堆\stack栈),是由程序员手动管理的。
特点:足够大 缺点:使用麻烦,需要手动管理
为什么要使用堆内存:
1、随着程序的复杂程序只会越来越多
2、其他内存段的申请释放不受控制,堆内存的申请释放是受控制的
如何使用堆内存:
注意:C语言中没有控制堆内存的语句,只能使用C标准库提供的函数
#include <stdlib.h>
void *malloc(size_t size);//void表示任意类型值
功能:从堆内存中申请size个字节的内存,申请到的内存存储是什么内容不确定
返回值:成功返回申请到的内存的首地址,失败返回NULL
#include <stdio.h>
#include <stdlib.h>
int main(int argc,const char* argv[])
{
// 从堆内存中分配一块内存,首地址存放在指针变量中
int* p1 = malloc(40);
for(int i=0; i<10; i++)//10=40/4 int类型
{
p1[i] = i;
}
free(p1);
p1 = NULL;
int* p = malloc(10*4);
for(int i=0; i<10; i++)
{
//p[i] = i;
printf("%d\n",p[i]);
}
}
p[i]并没有赋值,但却是p1清空前的值,证明申请到的内存是什么并不清楚,不一定是0
void free(void *ptr);
功能:释放一块堆内存,可以是释放NULL,但是不能重复释放和释放非法地址
注意:释放仅仅只是回收了使用权,里面数据是不会被特意清理
// 解引用 使用堆内存
*p = 100;
// 释放堆内存
free(p);
for(int i=0; i<10; i++)
{
//p[i] = i;
printf("%d\n",p[i]);
}
p = NULL;
}
拿到地址p[i]后虽然释放了但还是可以访问到可以看到
void *calloc(size_t nmemb,size_t size)
功能:从堆内存中申请nmemb块size个字节的内存,与malloc的区别是size=nmemb*size.申请到的内存块会被初始化为0,速度会慢
注意:申请到的依然是一个连续的内存,而不是单独的一块一块的。
//int* p = malloc(10*4);
int* p = calloc(10,4);
for(int i=0; i<10; i++)
{
//p[i] = i;
printf("%d\n",p[i]);
}
// 解引用 使用堆内存
*p = 100;
// 释放堆内存
free(p);
for(int i=0; i<10; i++)
{
//p[i] = i;
printf("%d\n",p[i]);
}
p = NULL;
}
证明calloc是可以清零的。
标准写法:calloc(10,sizeof(void))
void *realloc(void *ptr, size_t size);
功能:改变已有内存块的大小,size表示调整后的大小
返回值:是调整后的内存块首地址,一定要重新接收该函数的返回值,可能不是在原基础上调整的
如果无法在原基础上调整
1、申请一块size大小的内存
2、把原内存上的内容拷贝过去
3、把原内存释放。在返回新内存的首地址
具备上面三个函数的所有功能
1、当size=0时,相当于free
2、当ptr为NULL时,size为100,接一个新的地址,相当于malloc,但易产生错误。
#include <stdio.h>
#include <stdlib.h>
int main(int argc,const char* argv[])
{
int* p = malloc(40);
int* p2 = malloc(20);
int* p1 = realloc(p,80);
p[50] = 100;
printf("%p %p\n",p,p1);
当没有p2时,p和p1的地址相同,说明在原来的基础上调大调小,后面有空间调
当p2也申请内存时,把p申请内存的后面占住了,p1只能重新找放的下80 的位置,一定要接一下。
#include <stdio.h>
#include <stdlib.h>
int main(int argc,const char* argv[])
{
int* p = malloc(40);
for(int i=0; i<10; i++)
{
p[i] = i;
}
int* p2 = malloc(20);
int* p1 = realloc(p,80); //需要重新接一下
p[50] = 100;//看不出来有问题,但是有隐患
for(int i=0; i<10; i++)
{
printf("%d ",p1[i]);
}
printf("%p %p\n",p,p1);
}
p的数据被拷贝到p1首地址的前10个元素
melloc的内存管理机制
当首次向malloc申请内存时,malloc会向操作系统申请内存,操作系统会直接给33页 (1页=4096字节)内存交给milloc管理。
但是不意味着可以越界访问该内容,因为malloc也会把内存分配给“其他人”,这样就会产生脏数据
#include <stdlib.h>
int main(int argc,const char* argv[])
{
int* p = malloc(4);
for(int i=0; i<1024*33-2; i++) // 1024*变量类型字节 33页 -2,存储一些malloc维护信息
{
p[i] = 10;
}
}
每次申请的内存块之间是有空隙的(4·12个字节)。这些空隙是为了内存对齐,其中一定有4个字节记录着malloc维护信息,这些维护信息决定了下一次melloc时分配内存的位置,
也可以借助这些信息计算出内存块的大小,如果这些维护信息被破坏会影响下一次malloc和free
#include <stdio.h>
#include <stdlib.h>
int main(int argc,const char* argv[])
{
int* p1 = malloc(n);
printf("-----\n");
printf("%p\n",p1);
int* p2 = malloc(n);
printf("-----\n");
printf("%p\n",p2);
int* p3 = malloc(n);
printf("%p\n",p3);
int* p4 = malloc(n);
printf("%p\n",p4);
}
n变为4,8,12,16时,输出地址为16进制,间隔为16,当16时,间隔为8,因为占用了维护信息
#include <stdio.h>
#include <stdlib.h>
int main(int argc,const char* argv[])
{
int* p1 = malloc(12);
printf("-----\n");
p1[-1] = 0;
//free(p1);
printf("-----\n");
printf("%p\n",p1);
int* p2 = malloc(12);
printf("-----\n");
printf("%p\n",p2);
int* p3 = malloc(12);
printf("%p\n",p3);
int* p4 = malloc(12);
printf("%p\n",p4);
}
p1[3] = 0时程序也会报错,因为更改了维护信息,两个‘-------’可以打出,但当p2申请内存时会报错。
增加free后,第二个‘---------’无法打出,因为释放之后不知道要往后free多少个
p[-1]存储的是上一个的信息,影响到了p1及自身
p[3]存储的是下一个的信息,影响到了p2及自身
内存用完后会再分配,可以再申请使用。
使用堆内存需要注意的问题:
内存泄露:!!!!!
内存无法使用,也无法释放,而再次使用时只能重新申请,重复以上过程,一次两次影响不明显,但多次申请内存不释放多次后,日积月累系统中可用的内存就越来宇少,程序会越来越卡顿
{
int* p =malloc(4);
*p = 100;
p = NULL;
free(p);
// 运行正常但是先置空再释放,无法找到p的地址,无法使用,也无法释放
}
如果这样的操作重复循环,长期以往,不停的产生不停的生气,可用内存会越来越少。
注意:程序一旦结束属于它的资源都会被操作系统回收
如何避免内存泄露:
谁申请谁释放,谁知道释放谁释放
如何定位内存泄露:
1、Windows系统查看任务管理器,Linux 查看内存使用情况 ps -aux
2、分析代码,使用代码分析工具检查malloc的书写情况
3、封装malloc、free,记录申请和释放的信息到日志
内存碎片:
已经释放的内存但无法继续使用的内存叫做内存碎片,由于申请和释放的时间不协调导致的,内存碎片无法避免,只能尽量减少
如何减少内存碎片
1、尽量使用栈内存
2、不要频繁的申请释放内存
3、尽量申请大块内存自己管理
内存清理函数:
<strings.h>
void bzero(void *s, size_t n)
功能:把一块内存清理为0
s:清理的内存块的首地址
n:内存块的字节数
<string.h>
void *memset(void *s,int c,size_t n);
功能:把内存按字节设置为c
s:内存块首地址
c:想要设置的ASCII码
n:内存的字节数
返回值:成功返回设置后的首地址
#include <stdio.h>
#include <strings.h>
#include <string.h>
#include <stdlib.h>
int main(int argc,const char* argv[])
{
char* p = malloc(40);//int字节为4,char为1
for(int i=0; i<40; i++)
{
p[i] = i;
printf("%hhd\n",p[i]);
}
// bzero(p,40);
memset(p,1,40);
for(int i=0; i<40; i++)
{
printf("%hhd\n",p[i]);
}
free(p);
p = NULL; //良好习惯
}
bzero 清零
给每个字节赋1,int有4个字节,则为00000001000000010000000100000001,值为16843009
char只有一个字节,就为1
清0则把c赋0
#include <stdio.h>
#include <stdlib.h>
void CreateMem(int** p,size_t n)
{
*p = malloc(n);
printf("---%p\n",*p);
if(NULL == *p)
{
perror("malloc");
return;
}
}
int main(int argc,const char* argv[])
{
int* p = NULL;
CreateMem(&p,40);
// 因为跨函数传递指针变量,需要传递二级指针
printf("---%p\n",p);
for(int i=0; i<10; i++)
{
p[i] = i;
printf("%d\n",p[i]);
}
printf("----\n");
free(p);
p = NULL;
}
堆内存定义二维数组:
指针数组:
类型* a[n] //指针数组
int* a[n] = {};
for(int i=0;i<n;i++)
{
a[i] = malloc(sizeof(类型)*n);
}
注意:每一行的n可以不同,这就可以形成不规则的二维数组
#include <stdio.h>
#include <stdlib.h>
int main(int argc,const char* argv[])
{
// 指针数组
int* arr[10] = {};
for(int i=0; i<10; i++)
{
arr[i] = malloc(4*(i+1));
}
for(int i=0; i<10; i++)
{
for(int j=0; j<(i+1); j++)
{
printf("%d ",arr[i][j]);
}
printf("\n");
}
}
数组指针:
类型 (*a)[列数】 =malloc(sizeof(类型)*行数*列数);
//对内存要求高
注意:所谓的多维数组都是用一位数组来模拟的
#include <stdio.h>
#include <stdlib.h>
int main(int argc,const char* argv[])
{
// 数组指针
int (*arrp)[5] = malloc(sizeof(int)*5*4);
for(int i=0; i<4; i++)
{
for(int j=0; j<5; j++)
{
printf("%d ",arrp[i][j]);
}
printf("\n");
}
}
练习1:计算出100——1000之间所有素数,结果要存储在堆内存中,不能浪费内存
1、先算出多少个,一下申请内存,再计算放入
2、边算边扩充内存。
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <stdbool.h>
bool is_prime(int num)
{
for(int i=2; i<=sqrt(num); i++)
{
if(0 == num%i) return false;
}
return true;
}
int main(int argc,const char* argv[])
{
int* arr = NULL,cnt = 0;
for(int i=100; i<=1000; i++)
{
if(is_prime(i))
{
arr = realloc(arr,sizeof(int)*(cnt+1));
arr[cnt++] = i;
}
}
for(int i=0; i<cnt; i++)
{
printf("%d ",arr[i]);
}
free(arr);
arr = NULL;
return 0;
}
函数递归:
分治
函数自己调用自己的行为,易产生死循环
#include <stdio.h>
void func(int num)
{
printf("-----%d-----\n",num);
func(num);
}
int main(int argc,const char* argv[])
{
func(101);
}
递归可以实现分治这种算法,把一个复杂的大问题,分解成若干个相同的小问题,直到全部问题解决
1、出口
2、分解成小问题来解决
3、调用自己
计算第n项斐波那契数列 1 1 2 3 5 8 13
include <stdio.h>
int func1(int n)
{
if(1 == n||2 == n) return 1;
return func1(n-1) + func1(n-2);
}
int main(int argc,const char* argv[])
{
printf("%d", func1(1));
}
函数递归每次调用时都会在栈内存产生一份拷贝,直到达到出口,才一层一层的倒着释放,因此递归非常的耗费时间,因此能使用循环解决问题就不要使用递归。
递归的优缺点:
1、耗费内存,速度慢
2、好理解,思路清晰
作业
1、使用递归模拟n层汉诺塔
#include <stdio.h>
void hanio(int n,char s,char t,char d)
{
if(1 == n)
{
printf("1 from %c to %c\n", s,d);
return;
}
hanio(n-1,s,d,t); //A->B
printf("%d from %c to %c\n", n,s,d);
hanio(n-1,t,s,d);//B->C
}
int main(int argc,const char* argv[])
{
hanio(4,'A','B','C');
}
要点总结
堆内存管理:
C语言中没有管理堆内存的语句,只能使用标准库中的函数
#include<stdlib.h>
void *malloc(size_t size)
功能:从堆内存中申请一块连续的大小为size个字节的内存
返回值:内存块的首地址
int* p = (int*)malloc(4)
注意:void*在C++编译器中不能自动类型转换为其他类型,如果想要让代码也能在C++中兼容,需要强制类型转换
void free(void *ptr);
功能:释放一块堆内存
ptr:要释放的堆内存的首地址
注意:可以释放NULL,但是不能连续释放其他地址
常见的笔试面试题:
1、堆内存与栈内存的区别
**定义**堆内存是进程的一个内存段(text\date\bss\heap\stack),是由程序员手动管理的。
**特点:**足够大 缺点:使用麻烦,需要手动管理
**大小**:33页 (1页=4096字节)内存
**使用**:
void *malloc(size_t size);
功能:从堆内存中申请size个字节的内存,申请到的内存存储是什么内容不确定
返回值:成功返回申请到的内存的首地址,失败返回NULL
void free(void *ptr);
功能:释放一块堆内存,可以是释放NULL,但是不能重复释放和释放非法地址
注意:释放仅仅只是回收了使用权,里面数据是不会被特意清理
void *calloc(size_t nmemb,size_t size)
功能:从堆内存中申请nmemb块size个字节的内存,申请到的内存块会被初始化为0,速度会慢
注意:申请到的依然是一个连续的内存。
void *realloc(void *ptr, size_t size);
功能:改变已有内存块的大小,size表示调整后的大小
返回值:是调整后的内存块首地址,一定要重新接收该函数的返回值,可能不是在原基础上调整的
如果无法在原基础上调整
1、申请一块size大小的内存
2、把原内存上的内容拷贝过去
3、把原内存释放。在返回新内存的首地址
但易产生错误
**安全性**:
内存泄露:指内存无法使用,也无法释放,而再次使用时只能重新申请,重复以上过程,一次两次影响不明显,但多次申请内存不释放多次后,日积月累系统中可用的内存就越来宇少,程序会越来越卡顿。
内存碎片:
已经释放的内存但无法继续使用的内存叫做内存碎片,由于申请和释放的时间不协调导致的,内存碎片无法避免,只能尽量减少
栈(stack)是由编译器自动分配和释放的一块内存区域,主要用于存放一些基本类型(如int、float等)的变量、指令代码、常量及对象句柄(也就是对象的引用地址)。
栈内存实际上就是满足先进后出的性质的数学或数据结构,栈内存的操作方式类似于数据结构中的栈(仅在表尾进行插入或删除操作的线性表)。
栈的优势在于,它的存取速度比较快,仅次于寄存器,栈中的数据还可以共享,当超过变量的作用域后,会自动释放掉为该变量所分配的内存空间。缺点表现在,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
2、堆内存越界的后果
1、会破坏malloc的维护信息,然后导致接下去malloc和free出错
2、超过操作系统给的33页内存会产生段错误
3、其他可能出现的情况就是由脏数据了
3、什么是内存泄露?如何定位内存泄露
由于业务逻辑出错或者最普遍的粗心忘记没有把已经使用完毕的堆内存及时释放,然后当再次需要使用时又重新申请堆内存,又没有释放,长期以往导致可用的堆内存越来越少,系统越来越慢甚至系统崩溃,这就叫内存泄露。
1、WIN下可以查看任务管理器,Linux可以通过ps -aux查看内存使用情况
2、通过类似于matrace代码分析工具 ,分析代码是否有没有释放的内存
3、封装malloc、free记录调用信息到日志中
4、什么是内存碎片?如何减少内存碎片
已经释放但是无法使用的内存叫做内存碎片,是由于申请和释放的世界、大小不协调导致的
1.尽量使用栈内存
2、尽量申请大块内存自行管理
3、不要频繁的申请和释放内存