堆区是我们用户经常会使用到的内存区域,也是容易出错和比较容易忽视一些重要点的区域,更为重要的是在操作的过程中出现的一些错误会导致很多及其严重的问题。所以希望读者能够多看,多理解。避免日后的开发过程中出现错误。
堆内存可以存放任意类型的数据,但需要自己申请与释放。
主要用于大内存的申请使用,存放任意类型的数据。
堆大小,想像中的无穷大。对于堆来说,大空间申请,唯此,无它耳。但实际使用中,受限于实际内存的大小和内存是否连续性。
在实际的开发过程中,用户真正能够使用的就是内存中的栈区内存和堆区内存。
那么我们先来看一下堆内存空间的申请:
#include
#include
#include
int main()
{
int* p = (int*)malloc(1024 * 1024 * 1024); //1G 完全无压力
if (p == NULL)
{
printf("malloc error\n");
return -1;
}
else
{
printf("malloc success\n");
return 0;
}
}
返回值 void * 类型可以赋值给任意指针类型。
但是一般情况下,我们会将 malloc 函数的返回值类型强制转化为我们定义的用于接收申请到的堆内存空间的指针变量类型。
malloc函数的申请空间和初始化是以字节为单位在堆区申请内存空间,并且返回一个指针进行接收使用。
在栈内存和堆内存分别申请 int 类型大小的空间存放100。
#include
#include
int main()
{
int a = 100; //申请栈内存空间
printf("a = %d\n", a);
int* p = (int* )malloc(1 * sizeof(int));//申请堆内存空间
*p = 100;
printf("*p = %d\n", *p);
printf("%p\n", *p);
free(p);
return 0;
}
上面结果说明我们在栈内存申请空间并且使用malloc在堆内存申请成功,都赋值为100进行打印。
也就是说栈上的 int 类型指针变量 p 指向了地址为 0000 0064 的堆内存空间。
然后把地址为 0000 0064 的空间初始化为100。
栈内存和堆内存申请的空间如果没有初始化,编译器将其初始化为随机值:
#include
#include
int main()
{
int a; //申请栈内存空间
printf("a = %d\n", a);
int* p = (int*)malloc(1 * sizeof(int));//申请堆内存空间
printf("*p = %d\n", *p);
printf("%p\n", *p);
free(p);
return 0;
}
在Qt平台运行结果为:
我们可以从图解分析中看到:
a 和 p 指针 都是在栈区申请的内存空间。 p 指针指向的是在堆区申请的内存空间。然后把申请到的堆内存空间地址赋值给栈上的指针变量 p。
在栈内存和堆内存分别申请 10 个 int 类型大小的空间构造为一维数组。
#include
#include
#include
int main()
{
int array[10]; //申请栈内存空间
for (int i = 0; i < 10; i++)//打印堆内存空间数据
{
printf("%d\n", array[i]);
}
int* p = (int*)malloc(10 * sizeof(int));//申请堆内存空间
memset(p, 0, 10 * sizeof(int));//初始化堆内存空间
for (int i = 0; i < 10; i++)//打印堆内存空间数据
{
printf("%d\n", p[i]);
}
free(p); //释放堆内存空间
return 0;
}
memset(p,0,10 * sizeof(int));//初始化堆内存空间
上面函数调用,p 表示这段内存空间起始的首地址, 0代表每一个字节都初始化为0,10 * sizeof(int) 表示总共初始化 40 个字节。
我们可以从图解分析中看到:
array数组 和 p 指针 都是在栈区申请的内存空间。 p 指针指向的是在堆区申请的内存空间。然后把申请到的堆内存空间首地址赋值给栈上的指针变量 p。
如果我们把上面申请的空间通过memset初始化为 1 。
代码演示:
#include
#include
#include
int main()
{
int array[10]; //申请栈内存空间
int* p = (int* )malloc(10 * sizeof(int));//申请堆内存空间
memset(p,1,10 * sizeof(int));//初始化堆内存空间
for (int i = 0;i<10;i++)//打印堆内存空间
{
printf("%d\n",p[i]);
}
free(p); //释放堆内存空间
return 0;
}
我们看到运行结果并不是打印1。怎么会出现这样的情况呢?
前面说过:
malloc函数的申请空间和初始化是以字节为单位在堆区申请内存空间,并且返回一个指针进行接收使用。
我们对于上面打印结果进行图解说明:
我们把单独的一个 int 类型空间大小进行分析:
那么如果打印的时候打印 4个字节那么就是 16进制的 0101 0101
我们可以看到对应于十进制就是上面所打印的 16843009。
那么我们可以用下面方式打印出来每一个字节的数值:
#include
#include
#include
int main()
{
int array[10]; //申请栈内存空间
int* p = (int* )malloc(10 * sizeof(int));//申请堆内存空间
memset(p,1,10 * sizeof(int));//初始化堆内存空间
for (int i = 0;i<40;i++)//打印堆内存空间
{
printf("%d\t",((char*)p)[i]);
if (i % 5 == 0)
putchar(10);
}
free(p); //释放堆内存空间
return 0;
}
所以再次强调:
malloc函数的申请空间和初始化是以字节为单位在堆区申请内存空间,并且返回一个指针进行接收使用。
在上一篇博客中说明进程空间的图解分析之后,我们已经知道了堆内存空间的申请是从低地址向高地址申请。
代码演示:
#include
#include
#include
int main()
{
int* p = (int*)malloc(sizeof(int));
int* q = (int*)malloc(sizeof(int));
int* r = (int*)malloc(sizeof(int));
int* s = (int*)malloc(sizeof(int));
printf("p address is : %p\n", p);
printf("q address is : %p\n", q);
printf("r address is : %p\n", r);
printf("s address is : %p\n", s);
free(p);
free(q);
free(r);
free(s);
return 0;
}
我们在这里需要说明的一点就说:
栈内存空间申请之后如果没有初始化系统将其默认初始化为随机值,堆内存空间使用malloc函数进行堆内存申请之后,如果没有初始化,那么系统将默认初始化为随机值。
代码演示:
#include
#include
#include
int main()
{
int a;
printf("%d\n",a);
int* pa = (int*)malloc(10 * sizeof(int));
for (int i = 0; i < 10; i++)
{
printf("%d\n", pa[i]);
}
free(pa);
return 0;
}
其实也就是随机值。
那么如果上面代码我们修改为下面方式循环打印:
#include
#include
#include
int main()
{
int* pa = (int*)malloc(10 * sizeof(int));
for (int i = 0; i < 10; i++)
{
printf("%d\n", *pa++);
}
free(pa);
return 0;
}
上面打印方式会出现一个非常严重的问题:
把指向 malloc 申请的内存空间的指针 pa 改变了。也就是说,pa 每访问一次直接向后移动了,不再指向malloc申请的内存头地址。之后也就不能再通过 pa 指针来代表和使用 malloc 申请的内存了,因为 pa 指向了申请的这段堆内存空间的下一个地址未知区域,程序就会崩溃。
我们进行图解说明来分析:
那么我们申请了内存没有办法获取到,没有办法使用,那也就没有任何意义。所以如果要用指针变量操作申请的堆空间的话,必须重新定义一个新指针变量和pa指向同段申请的堆内存空间才保证申请堆内存空间的有效性。
代码演示:
#include
#include
#include
int main()
{
int* pa = (int*)malloc(10 * sizeof(int));
int* tmp = pa;
for (int i = 0; i < 10; i++)
{
printf("%d\n", *tmp++);
}
free(pa);
return 0;
}
我们看到程序正常运行,堆内存没有初始化的内存空间为随机值。
上面的写法就是可以的,因为并没有改变pa指针的位置。
也可以不定义新的指针变量来进行操作,使用数组用下标的访问就没有问题。因为没有破坏定义接收的指针变量指向堆内存空间的首地址。
代码演示:
for (int i = 0; i < 10; i++)
{
printf("%d\n", pa[i]);
}
我们知道 pa[0] 等价于 * ( pa + 0 ) ; 直到 pa[i - 1 ] 等价于 * ( pa + i - 1 ) ; 整个过程中,pa 指向这段空间的首地址是不改变的。
calloc的功能和malloc相似,但是会将所有申请到的堆内存空间的值自动初始化为0,并且是传递参数来实现申请,具体的参数功能在上面表格进行显示。
#include
#include
#include
int main()
{
int* pa = (int*)calloc(10 ,sizeof(int));
for (int i = 0; i < 10; i++)
{
printf("%d\n", pa[i]);
}
free(pa);
}
如果在堆区需要从10空间大小扩展到20个空间大小,步骤如下:
现在要扩展至20个空间:
如果从array指针向后,够20个空间则直接申请。
在申请完之后,realloc函数返回新申请的堆内存空间首地址。也就是继续使用开始定义的指针变量 array 接收堆内存新的20个空间。
扩展前后的指针变量array所指向内存地址是同一个地址。
如果原先的十个空间后面的十个空间是不可访问的或者可用的不足十个。
重新在堆区找到20个空间的内存,进行申请:
把原先的10个空间的数据拷贝到新申请的内存空间。
上面情况,如果继续使用开始定义的指针变量 array 来表示新申请的堆内存空间。并且新申请的堆内存空间和 array 所指向内存地址不是同一个地址。就需要把原先的array内存空间释放,然后把扩容之后的堆内存空间重新赋值给 array 指针,继续使用原来定义的指针变量array来表示扩容之后的堆内存空间。
如果不使用开始定义的array指针变量去接收新申请的堆内存空间,就需要重新定义新的指针变量去接收新申请的堆内存空间。
我们通过代码进行测试:
#include
#include
int main()
{
int* array = (int*)calloc(10, sizeof(int));
int* newArray = (int *)realloc(array, 80);
//array = realloc(array,80);
if (!newArray)
{
printf("realloc 失败\n");
return -1;
}
for (int i = 0; i < 20; i++)
{
printf("%d\n", newArray[i]);
}
return 0;
}
整个打印结果为就是realloc函数重新申请堆内存空间之后的结果,前面十个空间是calloc申请,所以初始化为0。后面的十个空间是新申请加上去的,没有初始化,所以为随机值。
通过上面图解我们知道,newArray和array指针是否指向同一个地址是由realloc申请空间的时候,新申请的空间大小是否够用来决定的。如果够用newArray和array指针指向同一个堆内存空间内存地址。如果不够用,newArray和array指针指向不同的内存地址。
这样来理解:如果在一个教室上课,有30张桌子,10人在上课,如果现在再来10个人那么需要20张桌子,现在的教室空间是够用的,所以那10个人直接进来坐,那么较是还是原来的教室,如果来40个人,那就需要50张桌子,那么这个教室就不够坐了,就需要换教室,那么换教室的时候原来教室的10个人也要起来换到新的教室。
当然我们还可以这样编写代码进行以防止两个指针指向的是不同的地址,我们直接让两个指针变成一个指针:
#include
#include
int main()
{
int* array = (int*)calloc(10, sizeof(int));
array = (int*)realloc(array, 20 * sizeof(int));
if (!array)
{
printf("realloc 失败\n");
return -1;
}
for (int i = 0; i < 20; i++)
{
printf("%d\n", array[i]);
}
return 0;
}
运行结果为:
这样我们仍然使用的是原来定义的指针变量来表示新申请的堆内存空间。
#include
#include
#include
int main(void)
{
int * array = (int*)calloc(10,sizeof(int));
free(array);
return 0;
}
这里有一个小问题就是:我们知道定义指针指向申请到的内存的时候,指向的是申请到内存的首地址,那我们使用free( )的时候参数只传递了指针,但是我们如何确定释放堆内存空间的大小呢?
这里我们只是简单说明一下,底层设计会记录出申请的位置和大小。所以例如上面个的过程,我们是不允许free(array+1)这样的形式出现的。因为后台是从array记录的。
我们在linux平台代码演示:
代码演示:
#include
#include
int main()
{
char * pa = (char *)malloc(10);
pa = realloc(pa,20);
strcpy(pa,"123456789abcde");
printf("pa = %s\n",pa);
free(pa + 1);
return 0;
}
运行结果为:
以前我们学过,VLA 变长数组,在运行时,只有一次初始化其长度的机会,且容易发生,栈溢出。现在有了堆空间的动态申请与释放,动态数组,就成了真正意义上 的动态数组了。
代码演示:
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
int main()
{
int len;
printf("pls start len:");
scanf("%d", &len);
int* p = (int*)malloc(len * sizeof(int));
memset(p, 0, len * sizeof(int));
for (int i = 0; i < len; i++)
printf("%d\n", p[i]);
//输入数组长度
printf("pls new len:");
scanf("%d", &len);
p = (int*)realloc(p,sizeof(int) * len);
if (!p)
{
printf("realloc 失败\n");
return -1;
}
for (int i = 0; i < len; i++)
{
printf("%d\n", p[i]);
}
free(p);
p = NULL;
return 0;
}
运行结果:
以下三个模型,也是针对,堆内存的使用特点而设计的。即:
返回判空。
配对使用。
自申请,自释放。
堆内存使用的逻辑是这样的,申请,判空,使用,释放,置空。常见错误
之一就是释放以后未置为 NULL 再次作判空使用或释放以后继续非法使用。
char*p=(char*)malloc(100);
strcpy(p,"hello");
free(p); /*p 所指的内存被释放,但是 p 所指的地址仍然不变*/
//p = NULL;忘了此句,后而又用到了,就出现了错误
.......
if(NULL!=p)
{
/*没有起到防错作用*/
strcpy(p,"hello"); /*出错*/
}
堆内存使用的逻辑代码演示:
#include
#include
#include
int main()
{
char* p = (char*)malloc(100); //申请
if (!p) //判空
{
printf("malloc error\n");
exit(-1);
}
strcpy(p, "hello"); //使用
printf("%s",p);
free(p); //释放
p = NULL; //置空
return 0;
}
我们很少会遇到内存申请失败的情况,但凡遇到了内存申请失败的问题,那一定是系统到了非常严重的问题。一般情况下我们很少遇到内存申请失败的情况。
在服务器模型中,常用到大循环,在大循环中未释放原有空间,重新申请新空间, 造成原有空间,内存泄漏。
while (1)
{
char *p = malloc(1000);
printf("xxxxxxoooooooooo\n");
printf("xxxxxxoooooooooo\n");
printf("xxxxxxoooooooooo\n");
printf("xxxxxxoooooooooo\n");
printf("xxxxxxoooooooooo\n");
printf("xxxxxxoooooooooo\n");
printf("xxxxxxoooooooooo\n"); //做了很多事情,忘记了 p 已经申请了内存空间
p = malloc(1000); // 中途可能忘了,重复申请,内存泄漏
free(p);
printf("xxxxxx\n");
sleep(10);
}
上面程序跑不了多久就会崩溃,因为无限循环的重复申请内存空间而不是放必然导致程序崩溃。内存泄漏导致的崩溃是非常可怕的。
如果没有协同的原则,则有可能会造成,重复释放。
#include
void func(char *p)
{
strcpy(p, "American");
printf("%s\n", p);
free(p); //此处违反了谁申请谁释放的原则。
}
int main()
{
char * p = malloc(100);
func(p);
free(p);
return 0;
}
上面代码会导致double free 程序崩溃。
在我们平时的程序中,如果出现内存泄漏,只要程序执行完成,或者电脑重启就会恢复。但是对于服务器来说很多程序通过大循环常驻内存,一直运行等着客户端区访问,比较好的服务器,只要一次配置好之后,不在改动的话,就会直到运行到这台服务器不再使用为止。
所以:内存泄漏导致的崩溃是非常可怕的。
有的服务器一跑就是几十年之久。
free 非 alloc 函数申请赋值的指针。
free(p+i); 这种写法我们是不允许使用的。
如果free 多于 malloc 会直接崩溃,编译器对于内存的操作还是很明确的。对于内存绝不允许有一点点姑息。
#include
#include
#include
int main()
{
while (1)
{
printf("xxxxxxxxxxxxoooooooooooo\n");
printf("xxxxxxxxxxxxoooooooooooo\n");
printf("xxxxxxxxxxxxoooooooooooo\n");
printf("xxxxxxxxxxxxoooooooooooo\n");
char * p = (char*)malloc(100 * sizeof(char));
printf("xxxxxxxxxxxxoooooooooooo\n");
free(p);
free(p); //多次释放
}
return 0;
}
结论:
malloc 和 free 要求配对使用,
malloc 多余 free 必然会导致内存泄露。
free 多余 malloc 会导致double free 程序崩溃。编译器绝不姑息。
传对象的地址到不同的作用域,可依据地址修改地址所指向对象的内容,根本原因是地址空间是开放的。
代码演示:
#include
void foo(char* p)
{
printf("foo () : %s\n",p);
}
void func(char* p)
{
printf("fun () : %s\n",p);
}
int main()
{
char* p = malloc(100);
strcpy(p, "American");
printf("main () : %s\n",p);
func(p);
foo(p);
free(p);
return 0;
}
运行结果为:
结论正确无疑,虽然有些平台并不报错。
** 栈上的空间不可以返回, 原因栈上的空间随用随开,用完即消 。**
#include
int func()
{
int a = 500; //可以取到变量a的值500
return a;
}
int* foo()
{
int a = 500;
int *pa = &a; //可以取到变量a的地址,地址是开放的。
printf("&a = %p\n",pa);
return pa;
}
int *func2()
{
int arr[100];
return arr;
}
int main()
{
int a = func();
printf("a = %d\n",a);
int *pa = foo();
printf("pa = %p\n",pa);
printf("%d\n",*pa);
*pa = 300;
return 0;
}
堆上的空间,是可以返回的,也是我们所需要的。
只要申请的内存没有手动释放,申请的堆内存空间就会一直存在。
#include
char * getFormatMem(int size,char content)
{
char *p = (char*)malloc(size *sizeof(char));
if(NULL == p)
exit(-1);
memset(p,content,size *sizeof(char)-1);
p[size *sizeof(char)-1] = '\0';
return p;
}
int main()
{
char *p = getFormatMem(30,'a');
printf("p = %s\n",p);
free(p);
return 0;
}
1 数值是可以返回的 。
2 地址也是可以返回 。
3 栈上的空间不可以返回, 原因栈上的空间随用随开,用完即消 。
4 堆上的空间,是可以返回的,也是我们所需要的。