欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

内存空间———堆(堆内存,malloc,calloc,realloc,free,常见错误案例剖析,其他常见错误,堆与栈的空间返回)【C】(o)

程序员文章站 2022-03-09 16:05:14
...

堆区是我们用户经常会使用到的区域,也是容易出错和比较容易忽视一些重要点的区域,更为重要的是这个在操作的过程中出现的一些错误会导致很多及其严重的问题。所以希望读者能够多看,多理解。避免日后的开发过程中出现错误。

堆内存

主要用于大内存的申请使用
存放任意类型的数据,但是需要自己申请与释放

那么我们先开看一下堆的内存申请:

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
int main() 
{ 
int * p = (int*)malloc(1024*1024*1024); //1G 完全无压力 
if(p == NULL) 
{ 
printf("malloc error\n"); 
return -1; 
}
}

打印结果为:

内存空间———堆(堆内存,malloc,calloc,realloc,free,常见错误案例剖析,其他常见错误,堆与栈的空间返回)【C】(o)

没有报错 正常显示。

malloc

内存空间———堆(堆内存,malloc,calloc,realloc,free,常见错误案例剖析,其他常见错误,堆与栈的空间返回)【C】(o)
malloc申请的时候是以字节为单位进行申请,并且返回一个指针进行接收使用。

#include<stdio.h>
#include <stdlib.h>
#include <malloc.h>
/*
堆内存使用
*/
int main()
{
	int* p = (int *)malloc(1 * sizeof(int));
	*p = 100;
	printf("*p = %d\n",*p);
	free(p);
	return 0;
}

打印结果为:
内存空间———堆(堆内存,malloc,calloc,realloc,free,常见错误案例剖析,其他常见错误,堆与栈的空间返回)【C】(o)
上面结果说明我们的malloc使用成功,堆区内存申请成功,并且赋值为100,并打印。
我们在这里需要说明的一点就说:malloc进行堆内存申请之后,如果没有初始化,那么里面的值为随机值。

include<stdio.h>
#include <stdlib.h>
#include <malloc.h>
int main()
{
	int* pa = (int*)malloc(10 * sizeof(int));
	for (int i = 0; i < 10; i++)
	{
          printf("%d\n", pa[i]);
	}
	free(pa);
	return 0;
}

打印结果为:
内存空间———堆(堆内存,malloc,calloc,realloc,free,常见错误案例剖析,其他常见错误,堆与栈的空间返回)【C】(o)
其实也就是随机值。
那么上面循环我们可以进行修改:

for (int i = 0; i < 10; i++)
	{
          printf("%d\n", *pa++);
	}

如果修改称为这样的话,打印结果是没有问题的,但是出了一个更加严重的问题就说,把指向malloc申请的内存指针给改变了,也就是说,pa每次访问一次直接向后移动了,不再指向malloc申请内存之后的头部了,之后必能通过pa指针来使用malloc申请的内存了,那么我们申请了内存没有办法获取到,没有办法使用,那就没有任何意义。所以所过要用指针形式的话,必须定义一个新指针指向pa才可以。

int *p = pa;
for (int i = 0; i < 10; i++)
	{
          printf("%d\n", *p++);
	}

上面的写法就是可以的,因为并没有改变pa指针的位置。

calloc

内存空间———堆(堆内存,malloc,calloc,realloc,free,常见错误案例剖析,其他常见错误,堆与栈的空间返回)【C】(o)

calloc的功能和malloc相似,但是会将所有值自动初始化为0,并且是传递参数来实现申请,具体的参数功能在上面表格进行显示。

#include<stdio.h>
#include <stdlib.h>
#include <malloc.h>
int main()
{
int* pa = (int*)calloc(10 ,sizeof(int));
	for (int i = 0; i < 10; i++)
	{
		printf("%d\n", pa[i]);
	}
	free(pa);
}

打印结果为:
内存空间———堆(堆内存,malloc,calloc,realloc,free,常见错误案例剖析,其他常见错误,堆与栈的空间返回)【C】(o)
申请的所有空间全部清零。

realloc

内存空间———堆(堆内存,malloc,calloc,realloc,free,常见错误案例剖析,其他常见错误,堆与栈的空间返回)【C】(o)
内存空间———堆(堆内存,malloc,calloc,realloc,free,常见错误案例剖析,其他常见错误,堆与栈的空间返回)【C】(o)
内存空间———堆(堆内存,malloc,calloc,realloc,free,常见错误案例剖析,其他常见错误,堆与栈的空间返回)【C】(o)

我们通过代码进行测试:

#include<stdio.h>
#include <stdlib.h>
#include <malloc.h>
int main()
{
	{
		int* array = (int*)calloc(10, sizeof(int));
		int* newArray = (int *)realloc(array, 80);
		//array = realloc(array,80);
		if (newArray == NULL)
		{
			printf("realloc 失败\n");
			return -1;
		}
		for (int i = 0; i < 20; i++)
		{
			printf("%d\n", newArray[i]);
		}
		return 0;
	}
	return 0;
}

打印结果为:
内存空间———堆(堆内存,malloc,calloc,realloc,free,常见错误案例剖析,其他常见错误,堆与栈的空间返回)【C】(o)
整个打印结果为就是realloc之后的结果,前面十个空间是calloc申请,所以初始化为0
通过上面图解我们知道,newArray和array指针是否指向同一个地址是由realloc申请空间的时候,原来的空间大小是否够用来决定的。如果够用newArray和array指针指向同一个内存地址,如果不够用,newArray和array指针指向不同的内存地址。

这样来理解:如果在一个教室上课,有30张桌子,10人在上课,如果现在再来10个人那么需要20张桌子,现在的教室空间是够用的,所以那10个人直接进来坐,那么较是还是原来的教室,如果来40个人,那就需要50张桌子,那么这个教室就不够坐了,就需要换教室,那么换教室的时候原来教室的10个人也要起来换到新的教室。

当然我们还可以这样编写代码进行以防止两个指针指向的是不同的地址,我们直接让两个指针变成一个指针:

#include<stdio.h>
#include <stdlib.h>
#include <malloc.h>
int main()
{
	{
		int* array = (int*)calloc(10, sizeof(int));
		int* array = (int *)realloc(array, 80);
		//array = realloc(array,80);
		if (array == NULL)
		{
			printf("realloc 失败\n");
			return -1;
		}
		for (int i = 0; i < 20; i++)
		{
			printf("%d\n", array[i]);
		}
		return 0;
	}
	return 0;
}

这样我们仍然使用的是原来的指针。

free

内存空间———堆(堆内存,malloc,calloc,realloc,free,常见错误案例剖析,其他常见错误,堆与栈的空间返回)【C】(o)

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
int main(void) 
{ 
int * array = (int*)calloc(10,sizeof(int)); 
free(array); 
return 0;
}

这里有一个小问题就是:我们知道定义指针指向申请到的内存的时候,指向的是申请到内存的首地址,那我们使用free( )的时候参数只传递了指针,但是我们如果确定释放的大小呢?这里我们只是提一下,底层设计会记录出申请的位置和大小。所以例如上面个的过程,我们是不允许free(array+1)这样的形式出现的。因为后台是从array记录的。

常见错误案例剖析

以下三个模型,也是针对,堆内存的使用特点而设计的。即:
返回判空。
配对使用。
自申请,自释放。

置空与判空

堆内存使用的逻辑是这样的,申请,判空,使用,释放,置空。常见错误
之一就是释放以后置未置为 NULL 再次作判空使用或释放以后继续非法使用。

char*p=(char*)malloc(100); 
strcpy(p,"hello"); 
free(p);/*p 所指的内存被释放,但是 p 所指的地址仍然不变*/ 
//p = NULL;忘了此句,后而又用到了,就出现了错误
....... 
if(NULL!=p) 
{ 
/*没有起到防错作用*/ 
strcpy(p,"hello"); /*出错*/ 
}

重复申请

在服务器模型中,常用到大循环,在大循环中未释放原有空间,重新申请新空间,
造成原有空间,内存泄漏。

while (1) 
{ 
char *p = malloc(1000); 
printf("xxxxxx\n"); 
printf("xxxxxx\n"); 
printf("xxxxxx\n"); 
printf("xxxxxx\n"); 
p = malloc(1000); // 中途可能忘了,重复申请,内存泄漏 
free(p); 
printf("xxxxxx\n"); 
sleep(10); 
}

谁申请谁释放模型(并非绝对)

如果没有协同的原则,则有可能会造成,重复释放。

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; 
}

其它常见错误

free 非 alloc 函数申请赋值的指针
free(p+i); 这种写法我们是不允许使用的。

如果free多余malloc 会直接崩溃但是windows平台并不要求并不是很严格,不会崩溃。

堆与栈空间的返回

1.栈空间不可以返回

结论正确无疑,虽然有些平台并不报错。
#include <stdio.h>
//1 数值是可以返回的
//2 地址也是可以返回
//3 栈上的空间不可以返回, 原因,随用随开,用完即消
//4 堆上的空间,是可以返回的

int func() 
{ 
int a = 500; 
return a; 
} 
int* foo() 
{ 
int a = 500; 
int *pa = &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; 
}

上面的代码我们不给出运行结果,有兴趣的读者可以在linux平台进行测试。

2.堆空间可以返回

#include <stdio.h> 
//1 数值是可以返回的 
//2 地址也是可以返回 
//3 栈上的空间不可以返回, 原因,随用随开,用完即消 
//4 堆上的空间,是可以返回的 
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(100,'a'); 
printf("p = %s\n",p); 
free(p); 
return 0; 
}