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

【C语言】动态内存管理

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

【C语言】动态内存管理

目录

1.动态内存简介

2.动态内存函数管理介绍

2.1 malloc

2.2 free

2.3 calloc

2.4 realloc

3.常见的动态内存的错误

3.1 对空指针解引用操作

3.2 对动态开辟空间的越界访问

3.3 对非动态开辟内存使用free释放 

3.4 使用free释放一块动态开辟内存的一部分

3.5 对同一块动态内存多次释放 

3.6 动态开辟内存忘记释放(内存泄漏)

 3.7 使用动态内存时的传参

4. C/C++内存开辟

5.柔性数组 

5.1 柔性数组的大小

5.2 柔性数组的使用


1.动态内存简介

动态内存是相对静态内存而言的。所谓动态和静态就是指内存的分配方式。动态内存是指在堆上分配的内存,而静态内存是指在栈上分配的内存。、

 

那么动态内存的意义是什么,我们为什么要使用动态内存呢?

先来看一下传统的内存开辟:

char ch ;//开辟了一个char类型的空间
int arr[5];//开辟了5个整形的空间

可以发现有以下几个特点:

1. 空间开辟大小是固定的。

2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

如果我们不知道在程序运行是所需开辟空间的大小,那么就必须在一开始,开辟一个特别大的空间以保证不会溢出。但是这样就造成了空间的浪费。

如果我们想要在程序运行时,根据需要动态地调整空间大小,就得使用动态内存了。


2.动态内存函数管理介绍

2.1 malloc
void* malloc (size_t size);

简介:

malloc 是一个系统函数,它是 memory allocate 的缩写。其中memory是“内存”的意思,allocate是“分配”的意思。 malloc 函数的功能就是“分配内存”。要调用它必须要包含头文件

功能:

向堆区申请开辟size个字节大小的连续可用的空间,并返回开辟空间起始位置的指针,类型为void*。

参数:

  • size 需要开辟空间的字节大小
  • void* 开辟的空间的地址

使用:

如果开辟成功,则返回一个指向开辟好空间的指针。

如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。

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

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

int *p = (int *)malloc(4);

此处,就是使用malloc向堆区申请4个字节的空间,由于返回指针类型为void*,那么必须要强转为(int*)才能给p赋值。如果申请成功,则返回开辟空间的起始地址;如失败,则返回NULL。

失败案例:

#include
#include 
int main()
{
    double* p = (double*)malloc(sizeof(double) * 1000000000000);
    if (p == NULL)
        perror("malloc");
    return 0;
}


2.2 free
void free (void* ptr);

简介:

动态分配的内存空间是由程序员手动编程释放的,为了释放动态内存,就有了free这个函数,需引用

功能:

free函数用来释放动态开辟的内存。

参数:

void* memblock:需要释放内存的地址

无返回值

使用:

如果参数 memblock指向的空间不是动态开辟的,那free函数的行为是未定义的。

如果参数 memblock 是NULL指针,则函数什么事都不做。

eg.

int main()
{
	//int arr[10] = {0};
	//申请空间
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		return -1;
	}
	//开辟成功了
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	//释放空间
	free(p);
	p = NULL;
	
	return 0;
}

特别注意:

1.在申请完内存后,一定要判断内存申请是否成功;

2.将内存释放后要养成好习惯,将原空间的指针赋为空指针。因为如不释放,指针仍然会指向这一片空间,此时这个指针为野指针,但是此时你未拥有这片空间的使用权,这会导致非法访问,


2.3 calloc
void* calloc (size_t num, size_t size);

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

参数:

mun:申请元素个数

size:申请元素每一个的大小(byte)

void* :返回初始空间地址

使用:

开辟成功返回数组地址,不够返回NULL。

 eg.

int main()
{
	//申请10个int的空间
	int*p = (int*)calloc(10, sizeof(int));
	
	if (p == NULL)
	{
		printf("%sn", strerror(errno));
		return -1;
	}
	
	
	//打印
	int i = 0;

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

	//释放空间
	free(p);
	p = NULL;

	return 0;
}


2.4 realloc
void* realloc (void* ptr, size_t size);

简介:

有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。

功能:

将新的动态内存的大小变为 size 个字节

参数:

memblock:是要调整的内存地址

size: 调整之后新大小

使用:

返回值为调整之后的内存起始位置。

这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

下面我们讨论一下realloc函数的空间开辟方式 

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

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

当是情况1的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。

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

当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。

特别注意:

1.此时原本空间上的数据会被复制到新空间,同时,realloc函数会自动地帮你释放掉原本的空间,千万不要再将原本空间释放一遍。

2.千万不要用原空间的指针,直接接收realloc的返回值

int* p = (int*)malloc(20);
p = realloc(p , 1000000000);//error

如果开辟失败,会返回NULL将原地址覆盖,同时原数据也丢失,输麻了。

应用新指针接收,判断不为NULL后,再赋值给原指针

eg.

int main()
{
	//申请10个int的空间
	int*p = (int*)calloc(10, sizeof(int));
	
	if (p == NULL)
	{
		printf("%sn", strerror(errno));
		return -1;
	}
	
	//申请成功
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	//空间不够了,增加空间至20 个int
	int*ptr = (int*)realloc(p, 20*sizeof(int));
	if (ptr != NULL)//判断realloc返回的是否为空指针
	{
		p = ptr;//不为空指针,赋值给原指针
	}
	else
	{
		return -1;
	}
	for (i = 10; i < 20; i++)
	{
		*(p + i) = i;
	}
	//打印
	for (i = 0; i < 20; i++)
	{
		printf("%d ", *(p + i));
	}

	//释放空间
	free(p);
	p = NULL;

	return 0;
}


 


3.常见的动态内存的错误

3.1 对空指针解引用操作
int  main()
{
	int*p = (int*)malloc(20);
	
	for (int i = 0; i < 5; i++)//直接这样写代码是有风险的!!!
	{
		*p = i;
	}
    
	return 0;
}

如果空间开辟失败,p的值是NULL,就会有问题,空指针是不能解引用的。

正确改法:

int  main()
{
	int*p = (int*)malloc(20);

	if (NULL == p)
	{
		perror("malloc");
		return -1;
	}
	
	for (int i = 0; i < 5; i++)
	{
		*p = i;
	}

	free(p);
	p = NULL;

	return 0;
}

3.2 对动态开辟空间的越界访问
int main()
{
	int* p = (int*)malloc(200);
	if (p == NULL)
	{
		return -1;
	}
	//使用
	int i = 0;
	//越界访问
	for (i = 0; i < 80; i++)
	{
		*(p + i) = i;
	}

	for (i = 0; i < 80; i++)
	{
		printf("%dn", *(p + i));
	}
	//释放
	free(p);
	p = NULL;

	return 0;
}

 没有打印任何东西

上面的错误其实算是非常隐蔽了,很多新手在使用动态内存时都会犯这样的错误,而且编译器不会给出错误或警告。

其本质原因还是对函数的参数认识不清,malloc函数开辟空间是按字节大小来开辟空间的,不是按照类型的大小,如果要按照类型大小开辟空间,可以在开辟后使用realloc调整。


3.3 对非动态开辟内存使用free释放 
int main()
{
	int a = 10;
	int*p = &a;
	
	free(p);
	p = NULL;

	return 0;
}

free只能释放堆区内存,但是p指向的空间在栈区


3.4 使用free释放一块动态开辟内存的一部分
int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		return -1;
	}
	//使用
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*p++ = i;//此时p的值发生改变
	}
	//释放
	free(p);//释放掉了不完全的空间
	p = NULL;

	return 0;
}


3.5 对同一块动态内存多次释放 
int main()
{
	int *p = (int*)malloc(40);
	if (p == NULL)
		return -1;
	//使用
	//...

	//2次释放-是错误的
	free(p);

    //进行其他操作
    //......
	free(p);

	return 0;
}

编译器会直接报错,终止运行。


3.6 动态开辟内存忘记释放(内存泄漏)
int main()
{
    int* p[10000] = { NULL };
    int i = 0;
    while (1)
    {
        p[i] = (int*)malloc(100);
        
        //进行其他操作
        //.......
        i++;
        //忘记释放
     }
    return 0;
}

//在堆区上申请的空间,有2中回收的方式
//1. 主动free
//2. 程序退出的时候,申请的空间也会回收

忘记释放导致堆区内存不断开辟,但并没有释放,所以会开辟到超出堆区大小

这种错误很危险,如果在大公司中,各个进程同时运行,并且24小时不退出,疯狂开辟不释放会使其他进程无法使用堆区,导致内存泄漏。

忘记释放不再使用的动态开辟的空间会造成内存泄漏。


 3.7 使用动态内存时的传参
void GetMemory(char* p)
{
	p = (char*)malloc(100);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
}

int main()
{
	Test();
	return 0;
}

以上程序运行会出现什么问题呢?

程序会崩溃,具体图解如下:

那么这种呢,对不对呢?

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

答案是依旧是错误的

1.未判断malloc返回是否为NULL;

2.未释放内存,导致内存泄漏。

具体图解如下:

正确改法:

void GetMemory(char** p)
{
	*p = (char*)malloc(100);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str);

	strcpy(str, "hello world");
	printf(str);
	//释放
	free(str);
	str = NULL;
}

再来看一种非常隐蔽的:

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

乍一看好像没问题,但是不要忘了p是一个临时变量,出函数会销毁。即使传过去了p的地址,出函数的p也是野指针,不能访问。

总结:以上问题全部都是传参和返回时的问题,在传参和返回时一定要注意区分和辨别



4. C/C++内存开辟

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

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

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

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


5.柔性数组 

C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

eg.

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

 

 

 

5.1 柔性数组的大小

先来看上述柔性数组大小:

struct tag
{
	int i;
	int a[];
};

int main()
{
	printf("%d", sizeof(struct tag));
	return 0;
}

我们发现,大小竟然为4,只有int类型的大小,这是怎么回事,难道柔性数组成员的大小没有被计算在内吗?

其实就是这样,柔性数组的大小不会被计算在内,算含有柔性数组成员的结构体大小可以将柔性数组成员忽略,按计算一般结构体的方法计算大小。(计算结构体大小方法:自定义类型:结构体,枚举,联合)


5.2 柔性数组的使用

使用动态内存开辟函数开辟 含有柔性数组的结构体的空间大小+柔性数组成员所需大小 的空间,同时由于malloc等函数返回的是指针,所以要用结构体指针接收。

 eg.

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

int main()
{
	//printf("%dn", sizeof(struct st_type));
	//包含柔性数组成员的结构体的使用,要配合malloc这样的动态内存分配函数使用
	//struct st_type st;
	struct st_type* ps = (struct st_type*)malloc(sizeof(struct st_type) + 10*sizeof(int));
	if (ps == NULL)
	{
		printf("%sn", strerror(errno));
		return -1;
	}
	//开辟成功了
	ps->i = 100;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->a[i] = i;
	}

	for (i = 0; i < 10; i++)
	{
		printf("%d ", ps->a[i]);
	}
	//a数组的空间不够了,希望调整为20个整型数据
	struct st_type* ptr = (struct st_type*)realloc(ps, sizeof(struct st_type)+20*sizeof(int));
	if (ptr == NULL)
	{
		printf("扩展空间失败n");
		return - 1;
	}
	else
	{
		ps = ptr;
	}

	//使用
	//...
	//释放
	free(ps);
	ps = NULL;

	return 0;
}

当然,由于柔性数组在C99中才被定义,那么以前的程序员都是怎么解决结构体中的数组大小不确定的问题的呢?

答:用所需类型的指针就可以实现

现在我们实现一下与上文使用柔性数组相同的功能

struct st_type
{
	int i;//4
	int* a;//4
};

int main()
{
	struct st_type* ps = (struct st_type*)malloc(sizeof(struct st_type));

	ps->i = 100;
	ps->a = (int*)malloc(10*sizeof(int));
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->a[i] = i;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", ps->a[i]);
	}
	//a指向的空间不够了,希望可以调整大小
	int* ptr = (int*)realloc(ps->a, 20*sizeof(int));
	if (ptr == NULL)
	{
		printf("扩容失败n");
		return -1;
	}
	else
	{
		ps->a = ptr;
	}
	//使用
	//..
	
	//释放
	free(ps->a);
	ps->a = NULL;
	free(ps);
	ps = NULL;

	return 0;
}

不同点是要开辟两次空间,同时也要释放两次。

 这两个方法都可以做到相同的事情,但柔性数组有几个好处是传统结构体比不了的:

第一个好处是:

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

第二个好处是:

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


柔性数组特点总结 :

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

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

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


 


 

以上就是本次的分享内容了,喜欢我的分享的话,别忘了点赞加关注哟!

如果你对我的文章有任何看法,欢迎在下方评论留言或者私信我鸭!

我是白晨,我们下次分享见!!

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

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

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