打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
在堆区开辟内存(动态内存的开辟)

简介1.基本概念1.什么是动态内存大家看这样一幅图片,我们通常定义的变量是在栈区为他分配空间,而如果使用malloc,calloc,realloc这样的函数就是在堆区为他开辟空间。2.开辟动态内存的作用1.在栈区开辟的空间...

目录

零.前言

1.基本概念

1.什么是动态内存

2.开辟动态内存的作用

1.在栈区开辟的空间

2.在堆区开辟空间

2.动态内存开辟的函数

1.void *malloc( size_t size )

1.含义

2.参数

3.返回值

4.用法

5.注意事项

2.void free( void *memblock )

1.含义

2.参数

3.返回值

4.用法

5.注意事项

3.void *calloc( size_t num, size_t size )

1.含义

2.参数

3.返回值

4.用法

4.void *realloc( void *memblock, size_t size )

1.含义

2.参数

3.返回值

4.用法

 3.动态内存开辟中常见的错误

1.对空指针的解引用操作

2.动态内存开辟的越界访问

3.对非动态内存开辟的空间使用了free

4.使用free释放开辟空间的一部分

5.对一块内存进行多次释放

6.动态内存开辟忘记释放

4.经典笔试题

1.笔试题1

2.笔试题2

3.笔试题3

4.笔试题4

5.柔性数组

1.含义

2.特点

3.举例

4.柔性数组的优势

1.方便内存释放

2.有利于提高访问速度

6.总结


零.前言

时间与空间,构成了我们处的这个世界,在神奇宝贝中,分别由帝牙卢卡和帕路奇犽所掌管。但在我们码农的世界里,无数大佬朝朝思,夜夜想如何节省空间的同时又能够节省时间,于是发明了好多复杂度的计算方法,如果说在栈中的存储是为了时间考虑,那动态内存的开辟则是为了空间的优势。

1.基本概念

1.什么是动态内存

大家看这样一幅图片,我们通常定义的变量是在栈区为他分配空间,而如果使用malloc,calloc,realloc这样的函数就是在堆区为他开辟空间。

1.栈区:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些内存单元自动被释放,栈内存分配运算内置于处理器的指令集中,效率更高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量,函数参数,返回数据,返回地址等。

2.堆区:一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。

3.数据段(静态区):存放全局变量,静态数据,程序结束后由系统释放。

4.代码段:存放函数体(类成员函数和全局函数)的二进制代码。

有了这张图我们也可以理解static关键字的含义了:

实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用于就销毁。

但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束时才销毁所以生命周期变长。

2.开辟动态内存的作用

1.在栈区开辟的空间

#includevoid print(int* n)
{
	int a = 10;
	n = &a;
}
int main()
{
	int* p = NULL;
	print(p);
	printf("%d", *p);
	return 0;
}

这段代码就是问题代码,虽然在函数中p通过n确实指向了a这块空间,但是出函数之后,a这段空间就释放了,使得p变成了一个野指针,所以不能进行解引用。

2.在堆区开辟空间

在堆区开辟的空间,空间是否释放是由用户决定的,并不是出某个空间就自动释放,下面就来介绍如何在堆区开辟空间。

动态内存开辟相对于在栈区开辟空间还有一个好处,当在栈区开辟空间时,需要一下子全都开辟完,比如开辟了一个1000个元素的数组,但最终只使用了30个元素,那么其他970个空间就发生了浪费,而在堆区开辟空间,可以用到哪里开辟到哪里,因为即使退出了加入数据的元素,之前加入的元素也不会被释放,再加入元素时只需要再次调用这个函数就可以了。

2.动态内存开辟的函数

1.void *malloc( size_t size )

1.含义

在堆区开辟空间。

2.参数

size表示开辟的空间为size个字节。

3.返回值

返回一个空类型的指针,该指针指向开辟的空间的首地址。

4.用法

由于我们返回的是一个空类型的指针,所以在我们想要使用这段空间的内容时,需要进行强制类型的转换,比方说我们在堆中存放了10个整型,那么接收的时候就要用整型指针进行接收。

int i;
int* p=(int*)malloc(40);//在堆上开辟40个字节,并将首地址赋值给p
for(i=0;i<10;i++)
{
*(p+i)=i;
}//将这段空间赋值

这样就成功在堆区开辟了一个40个字节的空间,用于存放10个整型(因为解引用时是4个字节一次的访问)。

5.注意事项

1.如果开辟空间成功则返回指向这块空间的指针。

2.如果开辟失败则返回一个空指针(NULL)。

3.返回值的类型是void*所以malloc函数不知道开辟这段空间的类型,具体的使用由使用者自己定义。

4.如果size的大小是0,malloc的标准是未定义的,行为取决于编译器。

2.void free( void *memblock )

1.含义

将动态内存开辟的空间还给操作系统。

2.参数

*memblock指动态内存开辟的空间的首地址。

3.返回值

无返回值。

4.用法

int i;
int* p=(int*)malloc(40);//在堆上开辟40个字节,并将首地址赋值给p
for(i=0;i<10;i++)
{
*(p+i)=i;
}//将这段空间赋值
free(p);//将开辟的40个字节的空间释放掉
p=NULL;//将p置为空

动态内存开辟的空间只有在程序运行结束时或者free掉才能还给操作系统,如果等程序运行结束,就很有可能出现内存崩溃的情况,所以我们引入free函数来回收空间。

在free函数回收空间之后,p指针变成野指针,需要用NULL赋值。

5.注意事项

1.如果p指向的空间不是动态内存开辟的,那么free函数的行为是未定义的。

2.如果p是空指针,那么free函数什么都不做。

3.void *calloc( size_t num, size_t size )

1.含义

在堆中开辟一段空间并初始化为0。

2.参数

num表示开辟了几个元素的空间,size表示一个元素开辟多大的空间。

3.返回值

返回一个空类型的指针,指向开辟空间的首元素的地址。

4.用法

实际上只是比malloc函数多了一个初始化的功能。

#include#includeint main()
{
	int* p = (int*)calloc(7, 5);//开辟七个元素,每个元素大小为5个字节
	return 0;
}

 我们在内存中可以看到一共开辟了35个字节的空间,且值均赋值为0。

4.void *realloc( void *memblock, size_t size )

1.含义

调整动态开辟空间的大小。

2.参数

memblock指向的是要更改大小的动态开辟的空间的首元素。size表示的是要增加几个字节的。

3.返回值

返回的是更改之后空间的首元素的地址。

4.用法

由于不确定在原有空间的基础上增加空间,是否会成功,所以在使用realloc函数的时候有两种情况:

第一种:

 我们知道在开辟空间的时候,开辟的空间在内存中是随机分布的。当我们在原有空间基础上增加空间时,如果原有空间后有足够的空间可供增加的时候我们是直接进行增加的。

第二种:

 当原有空间之后的空间不够进行再增加空间的时候,会对原有空间进行一份拷贝,再增加空间。那么此时realloc返回的就是拷贝后空间的首元素的地址。

#include#includeint main()
{
	int i;
	int* p = (int*)malloc(40);//在堆上开辟40个字节,并将首地址赋值给p
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}//将这段空间赋值
	realloc(p, 40);//在原有空间基础上增加了40个字节
	for (i = 0; i < 20; i++)
	{
		*(p + i) = i;
	}
	for (i = 0; i < 20; i++)
	{
		printf("%d\n", *(p + i));
	}
    free(p);
    p=NULL;//释放p指向的空间并将其初始化为0
	return 0;
}

打印的结果是:

即添加成功。

 3.动态内存开辟中常见的错误

1.对空指针的解引用操作

在开辟动态内存时,不一定每一次都能够开辟成功(当然大部分都能开辟成功),所以我们需要判断一下返回的指针是否为空,即开辟内存之后要进行一次判断。

int* p=(int*)malloc(30);
if(p==NULL)
{
return -1;//如果开辟失败返回-1
}

2.动态内存开辟的越界访问

int* p=(int*)malloc(30);
if(p==NULL)
{
return -1;//如果开辟失败返回-1
}
int i;
for(i=0;i<20;i++)
{
*(p+i)=i;
}

这里对动态内存进行了越界访问,一共开辟了30个字节,但是却访问了80个字节。

3.对非动态内存开辟的空间使用了free

a=10;
int* p=&a;
free(p);

 free的应用范围只能是动态内存开辟的。

4.使用free释放开辟空间的一部分

int* p=(int*)malloc(40);
p++;
free(p);此时p不指向起始元素地址,释放的是后一部分的空间

5.对一块内存进行多次释放

int* (int*)malloc(40);
free(p);//释放p的空间,此时p是一个野指针
free(p);//对野指针进行空间释放,是不对的

6.动态内存开辟忘记释放

void test()
{
int* p=(int*)malloc(100);
if(NULL!=p)
{
*p=20;
}
}
int main()
{
test();
while(1);
}

来看这一段代码,由于1永远是真,所以这段代码是无法执行结束的,所以p所指向的空间永远得不到释放,你可能会说实际工作中没有执行不完的代码,但如果工程量巨大,如果不释放空间的话,在程序结束之前,堆区可能很快就满了,后序的工作就无法进行了。

4.经典笔试题

1.笔试题1

void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}

我们运行程序发现程序挂掉了:

 1.str传给p的时候是值传递,p是str的临时拷贝,malloc开辟的空间起始地址放在p中不会影响str,str仍为NULL

2.str是NULL,strcpy想把hello world拷贝到str指向的空间时,程序就崩溃了,因为NULL指向的空间不能访问。

3.内存泄漏,没有free

2.笔试题2

#include#includechar* GetMemory(void)
{
	char p[] = "hello world";
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main()
{	
		Test();
		return 0;
}

显然这段代码也挂掉了,这是由于在GetMemory开辟的空间在出函数时就销毁了,所以str接收的仍然是一个野指针。 

3.笔试题3

#include#includevoid GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
}
int main()
{	
		void Test();
		return 0;
}

这段代码打印的结果是:

虽然打印出来了结果是hello,这里传递的是str的地址,用p来接收str的地址,对str地址解引用得到str本身,使str指向100个字节大小的空间,然后销毁p的空间。销毁的意思是不在程序中默认不能访问str的地址了,不过没关系,因为str已经指向了我们想让他指向的内容。 

4.笔试题4

#define _CRT_SECURE_NO_WARNINGS 1
#include#include#includevoid Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}
int main()
{	
		Test();
		return 0;
}

这段代码打印的结果是:

 虽然打印出来了,但是str已经被free掉了,所以str是一个野指针,不能进行拷贝。

5.柔性数组

1.含义

在C99标准中,结构体的最后一个元素允许是未知大小的数组。

2.特点

1.结构中的柔性数组成员前面必须至少有一个其他成员。

2.sizeof返回这种结构大小中不包含柔性数组的内存。

3.包含柔性数组成员结构用malloc()函数进行动态内存分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

3.举例

#includestruct A {
	int i;
	int a[];
};
int main()
{
	printf("%d", sizeof(struct A));
}

我们可以看出打印类型大小时并没有算柔性数组a的大小。

#includestruct A {
	int i;
	int a[];
};
int main()
{
	struct A* p = (struct A*)malloc(sizeof(struct A) + 5 * sizeof(int));
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		p->a[i] = i;
	}
	for (i = 0; i < 5; i++)
	{
		printf("%d ", p->a[i]);
	}
	return 0;
}

 在为柔性数组开辟空间的时候,要在为结构体开辟空间之后再加上想要开辟的字节数。

4.柔性数组的优势

1.方便内存释放

如果我们的代码是在一个给别人使用的函数中,你在里面做了一个二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事情,所以,如果我们把结构体的内存以及其成员要的内存一次性分配就好了,并返回给用户一个结构体指针,用户用一次free就可以把所有的内存给释放掉。

2.有利于提高访问速度

连续的内存有益于提高访问速度,也有益于减少内存碎片。

6.总结

在第一次做数据结构实验课的时候,链表部分就需要用到动态内存的开辟,曾经蒙了很长一段时间,动态内存开辟使我们对内存的把握更加灵活,就是访问速度慢了一些,但是总会有代价的不是吗,栈区和堆区,一个是用空间来换取时间,一个是用时间来换取空间。我们可以根据复杂度去计算,也可以把这两种方式当薛定谔的猫的问题来处理。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
c++知识 - 洋男的日志 - 网易博客
C语言的那些秘密之---函数返回局部变量
关于函数返回局部指针的总结(大部分来自网文)
谈C/C++指针精髓
【c++】指针参数是如何传递内存的
指针内存操作之malloc用法小结
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服