栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

动态内存管理

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

动态内存管理

动态内存管理
  • 1.为什么存在动态内存分配
  • 2.动态内存函数的介绍
    • (1)malloc和free
    • (2)calloc
    • (3)realloc
  • 3.常见的动态内存错误
    • (1)对NULL指针的解引用操作
    • (2)对动态开辟空间的越界访问
    • (3)对非动态开辟内存使用free释放
    • (4)使用free释放一块动态开辟内存的一部分
    • (5)对同一块动态内存重复释放
    • (6)动态开辟内存忘记释放(内存泄漏)
  • 4.几个经典的笔试题
  • 4.C/C++程序的内存开辟
  • 5.柔性数组

1.为什么存在动态内存分配

我们已经掌握的连续内存空间的开辟方式有:

char arr[10] = {0};//在栈空间上开辟10个字节的连续空间

上述的开辟空间的方式有一个非常明显的缺陷,数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配,开辟的空间是固定的,有可能大了,有可能小了,很难满足我们的实际需求。这时候就只能试试动态内存分配了。

2.动态内存函数的介绍

我们知道内存可以划分位栈区,堆区,静态存储区,其中栈区主要存放局部变量和形式参数,而我们malloc,calloc,realloc动态申请的内存空间都是在堆区开辟的,同时free函数释放的内存空间也是堆区的内存空间,静态存储区主要存放静态变量和全局变量。

下面我们就深入探讨一下用于在堆区动态申请空间的动态内存函数malloc,calloc,realloc。

(1)malloc和free

malloc函数是C语言提供的一个动态内存分配函数,其原型为void * malloc (size_t size);

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。如果开辟成功,则返回一个指向所开辟空间的指针。如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查,检查方式主要有两种:

//代码1
//使用malloc函数申请了一片内存空间,让指针ptr指向这片内存空间
ptr = (int*)malloc(num*sizeof(int));

//后面我们想使用ptr时,一定要检查它是否为NULL,如果不为NULL,那么我们就可以使用这个指针
if(NULL != ptr)
{
	//业务处理
}

free(ptr);
ptr = NULL;
//代码2
ptr = (int*)malloc(num*sizeof(int));

//如果指针为NULL,那么我们就打印错误信息,然后终止程序
if(ptr == NULL)
{
	printf("%sn",strerror(errno);
	return 0;
}

//能走到后面的代码段说明指针不为NULL,我们就可以正常使用ptr了
//业务处理
//释放内存
free(ptr);
ptr = NULL;

返回值的类型是 void* ,是因为malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。

如果参数 size 为0,malloc的行为是标准未定义的,这取决于编译器。

C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型为void free (void * ptr);

free函数用来释放动态开辟的内存。如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。如果参数 ptr 是NULL指针,则free函数什么事都不做。

值得注意的是,使用free函数释放了动态申请的空间后,指针仍然保存着这片空间的地址,也就是野指针,也就是我们依然可以通过指针访问到这片已经被系统回收了的内存,但这就构成了非法访问,为了避免这种非法访问,我们释放这片内存空间后,要将指针赋值为NULL。

free(ptr);
ptr = NULL;

注:
栈区变量自动回收,无需我们手动释放,只有malloc,calloc,realloc动态申请的内存空间才需要free。

(2)calloc

calloc 函数也用来动态内存分配,其函数原型为void* calloc (size_t num, size_t size);

函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。

和malloc一样,calloc也一样需要检验。


所以如果我们对申请的内存空间的内容要求初始化,那么可以使用calloc函数很方便地来完成任务。

(3)realloc

realloc函数的出现让动态内存管理更加灵活。

有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们一定需要对内存的大小做灵活的调整,那 realloc 函数就可以做到对动态开辟内存大小的调整。 函数原型为:
void* realloc (void* ptr, size_t size);其中ptr 是要调整的内存地址,size是调整之后的新大小,返回值为调整之后的内存起始位置。

realloc在调整内存空间时存在两种情况:

情况1:原有空间之后有足够大的空间
情况2:原有空间之后没有足够大的空间

当是情况1的时候,要扩展内存就直接在原有内存之后直接追加空间,原来空间的数据不发生变化。当是情况2 的时候,原有空间之后没有足够多的空间,扩展的方法是:在堆空间上另开辟一片合适大小的连续空间来使用,这时函数会将原来内存中的数据复制到新开辟的空间,这样函数返回的是一个新的内存地址。

当然除了上述两种情况外,还有第三种情况,就是realloc函数开辟空间失败返回NULL,也正是因为这第三种情况,realloc函数的使用就要注意一些。 举个例子:

#include 
int main()
{
	int *ptr = malloc(100);
	if(ptr != NULL)
	{
		//业务处理
	}
	else
	{
	  exit(EXIT_FAILURE);  
	}
	//扩展容量
	//代码1
	ptr = realloc(ptr, 1000);//这样可以吗?

显然按照上述代码1的写法是不可取的,因为ptr本来指向一片由malloc函数开辟的空间,如果realloc函数调整空间失败,返回NULL被ptr接收了,那么ptr的值就为NULL,这就导致我不仅调整空间失败,连我原来所指向的那片空间也丢了,为了避免这种赔了夫人又折兵的情况发生,我们应该使代码更加严谨。

//代码2
	int*p = NULL;
	//先让一个指针p来接收返回值
	p = realloc(ptr, 1000);
	if(p != NULL)
	{
	//确认返回值不为NULL后,我们再将其赋值给ptr
		ptr = p;
	}
	//业务处理
	free(ptr);
	return 0;
}

需要注意的是,realloc传的是NULL,那功能就和malloc函数一样,直接开辟指定大小的空间,所以说realloc可以调整空间,也可以像malloc一样开辟空间。

3.常见的动态内存错误 (1)对NULL指针的解引用操作
void test()
{
	int *p = (int *)malloc(INT_MAX/4);
	*p = 20;//如果p的值是NULL,就会有问题
	free(p);
}

上述代码malloc有可能开辟空间失败返回一个NULL,那么p就接收了malloc的返回值后,p就成为了NULL指针,后面对p进行操作,实际上就是对NULL指针的解引用操作,为了避免这种错误一定要对malloc的返回值进行检查后再进行使用,代码应做如下修改:

void test()
{
	int *p = (int *)malloc(INT_MAX/4);
	if(p != NULL)
	{	*p = 20;
	}
	free(p);
}
(2)对动态开辟空间的越界访问
void test()
{
	int i = 0;
	int *p = (int *)malloc(10*sizeof(int));
	if(NULL == p)
	{
		exit(EXIT_FAILURE);
	}
	for(i=0; i<=10; i++)
	{
		*(p+i) = i;//当i是10的时候越界访问
	}
	free(p);
}
(3)对非动态开辟内存使用free释放
void test()
{
int a = 10;
int *p = &a;
free(p);//ok?
}

上述这种做法是错误的,局部变量a和指针变量p都是在栈区上开辟的,而free函数只能释放由malloc,calloc,realloc在堆区动态申请的内存空间。

(4)使用free释放一块动态开辟内存的一部分
void test()
{
	int *p = (int *)malloc(100);
	p++;
	free(p);//p不再指向动态内存的起始位置
}

上述代码是一种经典的错误,p原本指向malloc所申请空间的起始位置,执行完p++语句后,指针p就后移了4字节,假设此时p指向ptr,那么此时free§就是释放ptr之后动态申请的空间,前面动态申请的空间就没有被释放,即仅释放了动态开辟内存的一部分。

(5)对同一块动态内存重复释放
void test()
{
	int *p = (int *)malloc(100);
	free(p);
	free(p);//重复释放
}

我们之前已经释放过一次,那么这片空间已经还给操作系统了,然后我们再进行释放,释放已经还给操作系统的这一片空间,这就会造成访问冲突,编译器可能不会报错,但是程序会直接挂掉。

为了避免重复释放的错误,我们可以这样严谨地写代码:

free(p);
p = NULL;

也就是将空间释放完后,我们将指针置为NULL,这样即便我们重复释放,也不会造成非法访问,程序也不会报错。

(6)动态开辟内存忘记释放(内存泄漏)
void test()
{
	int *p = (int *)malloc(100);
	if(NULL != p)
	{
		*p = 20;
	}
}

int main()
{
	test();
}

堆区动态申请的内存空间在程序结束后,也会被操作系统自动回收,那这是否就意味着我们可以不用释放我们在堆区动态申请的内存空间呢?

如果我们动态开辟内存忘记释放了,那么在自己程序执行的这一段时间,我们既不使用它,又不释放它,这就导致了别人也用不了这片内存,那么这片内存资源就被浪费了,这就造成了内存泄漏。

更加严重点,如果我们的程序是死循环,也就是除非关机,否则该程序永远不会结束,这就意味着操作系统不会回收我们动态申请的内存空间,而如果我们还不主动释放这片内存空间,这就意味着这片内存空间永远也无法被其他人所使用,这片内存空间被永久地浪费了,这是更加严重的内存泄露!

比如这段代码:

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

int main()
{
	test();
	while(1);
}

在text函数里动态申请的这片空间并没有被释放掉,而程序因为while(1)的存在陷入死循环,程序永远不会结束,操作系统不会回收text函数里动态申请的这片空间,这就导致了别人永远也无法使用这片空间,造成了空间资源的永久浪费,这是十分严重的内存泄漏。

总而言之,言而总之,忘记释放不再使用的动态开辟的空间会造成内存泄漏。 切记: 动态开辟的空间一定要释放,并且正确释放 。

4.几个经典的笔试题

1.请问下面这段代码有哪些问题?

void GetMemory(char *p)
{
	p = (char *)malloc(100);
}

int main(void)
{
	char *str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
}

首先,将str传入GetMemory中,也就是将NULL传入GetMemory中,此时p = NULL,在GetMemory函数中动态申请了一片空间,由指针p指向这片动态申请的内存空间,GetMemory函数执行完毕后,局部变量p被操作系统自动回收了,但是动态申请的那片空间还在,而p不在了,由malloc动态申请的那片空间也就找不到了,所以在GetMemory中没有释放动态申请的内存空间,那么在GetMemory函数外也就无法释放动态申请的这片内存空间了,也就是整个程序都没有主动释放这片动态申请的内存空间,那么在该程序执行的这段时间,这片空间你不用,你也不释放,导致别人也用不了这片空间,这造成了内存空间的浪费,也就是造成了内存泄漏。

其次,str至始至终都没有改变过,它一直是NULL指针,那么执行strcpy(str, "hello world");这段代码的时候就会造成访问冲突。

#include

char* GetMemory(char* p)
{
	p = (char*)malloc(100);
	return p;
}

int main(void)
{
	char* str = NULL;
	str = GetMemory(str);
	//
	strcpy(str, "hello world");
	printf(str);
	//释放
	free(str);
	str = NULL;
	
	return 0;
}

我们还可以这样修改这段代码:

void GetMemory(char **p, int num)
{
	*p = (char *)malloc(num);
}

int main()
{
	char *str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
	free(str);
}

对于上述代码和分析我们可以得出结论,对于函数内动态申请的内存空间,要么我们就在函数内部释放,否则函数外指针变量被回收,动态申请的内存空间就找不到了,我们就无法释放了,要么我们将动态申请的内存空间的地址返回出去,由函数外的指针来接收,这样就可以在函数外进行释放,要么将函数外的指针地址传入函数,由一个二级指针来接收这个一级指针的地址,然后在函数内通过解引用操作,获得函数外指针,直接将动态申请的内存空间地址赋值给函数外的指针,然后在函数外进行释放。

2.下面这段代码有哪些问题?

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

GetMemory函数返回指针p所保存的数组首元素地址,由指针str来接收,那么此时指针str保存着数组首元素地址,由于GetMemory函数结束后,数组空间被操作系统自动回收,此时你再想通过首元素地址打印数组保存的字符串就构成了非法访问,且此时str为野指针!

4.C/C++程序的内存开辟

  1. 栈区(stack): 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等,函数运行结束后,这些空间会自动被操作系统回收。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
  3. 数据段(静态存储区)存放全局变量、静态数据。程序结束后由系统释放,所以数据段上变量的生命周期比栈上变量的生命周期更长。
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码以及常量。
5.柔性数组

也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。 C99 中,结构中的最后一个成员允许是未知大小的数组,这就叫做『柔性数组』成员。

例如:

typedef struct st_type
{
	int i;
	int a[0];//柔性数组成员
}type_a;

int a[0]中的0表示大小未指定,这个数组可能会当作柔性数组。

有些编译器会报错无法编译可以改成:

typedef struct st_type
{
	int i;
	int a[];//柔性数组成员
}type_a;

柔性数组的特点:

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

  • sizeof 返回的这种结构大小不包括柔性数组的内存。

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

    例如:

    typedef struct st_type
    {
    	int i;
    	int a[0];//柔性数组成员
    }type_a;
    
    printf("%dn", sizeof(type_a));//输出的是4
    

柔性数组的使用:

#include

struct S2
{
	int n;
	int arr[];
};

int main()
{
	struct S2* ps = (struct S2*)malloc(sizeof(struct S2) + 40);
	ps->n = 100;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->arr[i] = i;
	}
	
	//增容
	struct S2* ptr = (struct S2*)realloc(ps, sizeof(struct S2)+80);
	if (ptr == NULL)
	{
		return 0;
	}
	else
	{
		ps = ptr;
	}
	//释放
	free(ps);
	ps = NULL;

	return 0;
}

这样柔性数组成员a,相当于获得了100个整型元素的连续空间。

柔性数组的优势:

上述的 type_a 结构也可以设计为:

typedef struct st_type
{
	int i;
	int *p_a;
}type_a;

int main()
{
	type_a *p = malloc(sizeof(type_a));//给结构体动态申请一片空间

	p->i = 100;//给成员变量i赋值
	p->p_a = (int *)malloc(p->i*sizeof(int));//给成员变量p_a赋值一片动态申请的空间

	//业务处理
	for(i=0; i<100; i++)
	{
		p->p_a[i] = i;
	}
	//释放空间
	free(p->p_a);
	p->p_a = NULL;
	free(p);
	p = NULL;

	return 0;
}

上述代码1和代码2可以完成同样的功能,但是方法1的实现有两个好处:第一个好处是方便内存释放:

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

一句话来说就是代码2动态开辟和释放的次数多,容易出错,代码1一次开辟一次释放,更加方便,不容易出错!

第二个好处是:这样有利于提高访问速度。

内存开辟次数越多,内存碎片就越多,连续的内存有益于减少内存碎片,也有益于提高访问速度。(其实,我个人觉得也没多高了,反正你跑不了要用做偏移量的加法来寻址)

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1002285.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号