C语言-----动态内存管理

目录

1、内存

1.1 内存布局

1.2 为什么存在动态内存的分配 

2、动态开辟内存函数的介绍和使用

2.1 malloc

2.2 free

2.3 calloc

2.4 realloc

3、常见的动态内存错误

3.1 对NULL指针的解引用操作

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

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

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

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

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

4、动态内存经典笔试题 


1、内存

1.1 内存布局

在开始学习之前,先来了解内存的布局。也就是 C 语言中内存是如何布局的呢?

在 C 语言中,一般把内存分为 5 个区域,分别为全局区(静态区)、代码区、常量区、堆、栈。( 注意,这里所说的堆、栈是两个不同的概念,要与数据结构中的堆栈分开。不过也有人把二者合称为堆栈,因此,有人称内存分为 4 个区域。)

下面对这五个区域进行简单介绍:

(1)全局区:存放在这个区域的数据,在整个程序的运行期间都是有效的,也就是生存期贯穿整个程序运行期。全局区是用来存放全局变量、静态变量。全局区的分配、释放均由编译器自动完成。全局区也称为静态区。

(2)代码区:显而易见,就是存放程序的区域

(3)常量区:编程的时候,经常会用到大量常量,这些常量在程序运行期间不会改变,这类数据就存放在常量区。常量区的分配、释放也是由编译器自动完成。

(4)堆区:程序运行期间,可能临时产生大量有用的数据,但这些数据只是临时的,并不需要程序持续保留,这个时候就需要临时分配一些内存空间来保存。当这些数据不再使用时,我们就没有再保留它们的必要了,弃之即可。而内存大小是有限的,不能随意丢弃不管。所以,不再保留这些数据时,还得收回分配的内存空间。这就是内存的动态分配和释放的原因。整个这些操作就是在所谓的堆区进行的。显然,编译器是不知道什么时候有临时数据,而需要分配内存的。所以堆区进行的内存分配释放是编程人员控制的。由于有分配,而且可释放,所以称为动态内存分配。

(5)栈区:在没有操作系统的 C 语言编程中,除了 main 函数,都是可以被调用的函数。在被调函数中由函数本身、且只供函数本身使用的变量,称为局部变量。这些变量在调用函数时分配空间,调用函数结束后,自动释放空间。这类数据就是存放在栈区。类似的数据还有调用函数时的形参等。

如下图:

1.2 为什么存在动态内存的分配 

 为什么存在动态内存分配呢? 前面学到开辟空间的方式有:

int value = 10;//在栈空间上开辟了四个字节
int array[10] = { 0 };//在栈空间上开辟 40 个字节的连续空间

但是上述的开辟空间的方式有两个特点:

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

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

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才知道,那么数组在编译时开辟空间的方式就不能满足了。这就是为什么存在动态内存的分配了。

2、动态开辟内存函数的介绍和使用

需要存放的临时数据无限,内存有限。编程人员,需要随着程序运行,随时分配内存空间,并且随着临时数据的失效,及时地回收,正所谓动态分配内存。动态分配内存,分配的是堆区的内存空间。 分配内存的函数有多个不同的原型,这些都集成在库函数 stdlib.h 中。

2.1 malloc

函数原型如下: 该函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

void* malloc (size_t size);

函数说明:size_t 是什么呢?size_t是标准C库中定义的,在64位系统中为long long unsigned int,非64位系统中为long unsigned int。调用该函数,需要用户指定分配内存空间的大小。分配成功后,系统会为用户分配一块内存空间,空间大小为 size 字节,其值是随机值。该函数的返回值是 void 类型的指针,该指针指向分配的这块内存。由于是 void 类型的指针,所以在使用时,需要把该指针强制转换成需要的类型。内存空间有限,如果指定的大小超过了可分配内存空间的大小,则有可能分配失败。如果分配失败,返回的则是NULL。所以,在使用动态分配的内存空间之前,首先要判断是否分配成功。由于 malloc 函数只是分配内存,并不进行初始化,所以分配成功后,这块内存区域为随机值。

总结:

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

(2)返回值的类型是 void* ,所以 malloc 函数并不知道开辟空间的类型,需要使用者把该指针强制转换成需要的类型。

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

(4)新分配的内存块的内容未初始化,保留不确定的值。

(5)如果参数 size 为 0,malloc的行为是标准是未定义的,取决于编译器。(size_t size :开辟的空间大小,单位字节)

使用 malloc 函数开辟空间简单的例子: 

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>

//malloc 函数的简单例子
int main()
{
	int* ptr = (int*)malloc(10 * sizeof(int));//开辟了一块 10*4=40 字节大小的空间,并且放回一个指向开辟好空间的指针
	if (ptr == NULL)//对 malloc 函数的返回值进行检测,防止开辟空间失败
	{
		printf("%s\n", strerror(errno));//打印错误原因的一个方式
	}
	else//检测没有问题就可正常使用开辟好的这块内存空间了
	{
		//正常使用
		int i = 0;
		for (i = 0; i < 10; i++)//对这块空间赋值
		{
			*(ptr + i) = i;
		}
		for (i = 0; i < 10; i++)//打印该空间中的数据
		{
			printf("%d ", *(ptr + i));
		}
	}
	//当动态申请的空间不再使用的时候
	//就应该还给操作系统
	free(ptr);//释放开辟好的空间,还给操作系统,下面将会学到该函数,这里为了程序的正确性。
	ptr = NULL;//并且把该指针置为 NULL(空)。防止该指针还指向已经被释放(被回收)的内存空间,导致非法访问。
	return 0;
}

2.2 free

内存有限,分配出去的内存空间在不用时要及时回收。分配、回收配对使用,无疑是个很好的办法。内存的回收,也称内存的释放。与动态分配一样,同样需要编程人员来完成。 

函数原型如下: 专门是用来做动态内存的释放和回收的。

void free (void* ptr);

动态分配的内存使用结束后,要及时释放。调用该函数,指定需要释放的内存空间地址,即可完成释放。需要注意的是,内存释放与指针的关系。内存释放,只是把这块内存的数据变成无效数据,而指针 ptr 依然指向这块内存,虽然这块内存的数据无效,此时 ptr 已经成为野指针了,如果对该指针进行操作的话会导致非法访问内存,造成不可避免的损失。

为了防止该指针在这种情况下被继续使用,释放内存后,要及时把指针指向 NULL,这样的话下次使用该指针时,通过判错功能 if (ptr==NULL) 就可阻止使用无效指针。也就是说,放内存的同时,也要释放指针。同理,如果单纯的把指针指向 NULL,也是不行的,释放指针并不等于释放内存。

总结:
(1)ptr 是指向一块内存空间的地址。
(2)free 函数用来释放动态开辟的内存。
(3)free 函数释放动态开辟的内存后,要把指向动态开辟内存的指针置NULL。
(4)不能单纯只把指针指向NULL,不对内存进行释放,会导致这块开辟的内存没有没有相应的指针进行管理。
(5)如果参数 ptr 指向的空间不是动态开辟的,那 free 函数的行为是未定义的。
(6)如果参数 ptr 是NULL指针,则函数什么事都不做。
(7)free 函数要与开辟内存空间的函数成双成对的存在,防止开辟好的内存没有回收,导致内存泄漏。 (free 函数使用相应的例子如上)

2.3 calloc

函数原型如下:calloc 函数也是用来动态内存分配的。

void* calloc (size_t num, size_t size);

调用该函数,同样需要用户指定相应的参数,参数包括元素的数量和每个元素的字节数,这一点不同于 malloc 函数。calloc 分配的内存空间大小,由 num、size 两个参数决定。如果分配成功分配所得的内存空间大小为 num* size 字节,并且内存空间被初始化为 0 或 NULL。该函数返回的同样是指向这块内存的指针。如果分配失败,返回的则是 NULL。所以,该指针使用前同样建议进行是否为NULL的判断。

总结:

(1)函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为 0。

(2)与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全 0。

 使用 calloc 函数开辟空间简单的例子:

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
//calloc
int main()
{
	//int* ptr = (int*)malloc(sizeof(int))//效率高但不初始化
	int* ptr = (int*)calloc(10, sizeof(int));//效率低一些,但是全部初始化为0(开辟好空间了,初始化后,再返回指向该空间的地址)
	if (ptr == NULL)//对 calloc 函数的返回值进行检测,防止开辟空间失败
	{
		printf("%s\n", strerror(errno));//打印错误原因的一个方式
	}
	else//打印出该空间的数据
	{
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(ptr + i));
		}
	}
	free(ptr);//释放开辟好的空间,还给操作系统。
	ptr = NULL;//并且把该指针置为 NULL(空)。防止该指针还指向已经被释放(被回收)的内存空间,导致非法访问。
	return 0;
}

2.4 realloc

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

函数原型如下: realloc 函数用于修改一个原先已经分配的内存块的大小。也可以说是对已有的内存空间进行重新分配。

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

 void 类型的指针 ptr 指向已有的内存空间,size 用来指定重新分配之后分配所得的整个空间大小。如果分配成功,返回指向新分配空间的指针;如果分配失败,同样返回 NULL。

使用这个函数,你可以使一块内存扩大或缩小。如果它用于扩大一个内存块,那么这块内存原先的内容依然保留,新增加的内存添加到原先内存块的后面,新内存并未以任何方法进行初始化。如果它用于缩小一个内存块,该内存块尾部的部分内存便被拿掉,剩余部分内存的原先内容依然保留。

如果原先的内存块无法改变大小,realloc 将分配另一块正确大小的内存,并把原先那块内存的内容复制到新的块上。因此,在使用 realloc 之后,你就不能再使用指向旧内存的指针,而是应该改用 realloc 所返回的新指针。

最后,如果 realloc 函数的第1个参数是NULL,那么它的行为就和 malloc 一模一样。

总结:

(1) ptr 是要调整的内存地址 ,size 是调整之后新大小。

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

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

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

(1)原有空间之后有足够大的空间。要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。

 

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

  使用 realloc 函数修改内存空间简单的例子:

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>

/* realloc 调整动态内存空间的大小 */

int main()
{
	int* p = (int*)malloc(20);//开辟20个字节的空间
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
	}
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p + i) = i;
	}
	int* ptr = (int*)realloc(p, 40);//用一个新的指针变量来接收 realloc 函数的返回值
	if (ptr != NULL)//判断是否调整成功
	{
		p = ptr;
		for (i = 5; i < 10; i++)
		{
			*(p + i) = i;
		}
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i));
		}
	}
	//释放内存
	free(p);
	p = NULL;
	return 0;
}

 分析:这里使用 malloc 开辟的20个空间,假设在这里,20个字节不能满足我们的使用,我们希望能够有40个字节的空间,这里就可以使用 realloc 来调整动态开辟的内存空间了。

realloc 函数使用的注意事项:

(1)如果 p 指向的空间之后有足够的内存空间可以追加,则直接追加,然后返回 p。

(2)如果 p 指向的空间之后没有足够的内存空间可以追加,则 realloc 函数会重新找一块新的内存区域来开辟一块满足需求的空间,并且把原来内存中的数据拷贝过来,释放旧的内存空间,最后返回新开辟的内存空间地址。

(3)得用一个新的指针变量来接收 realloc 函数的返回值。

3、常见的动态内存错误

3.1 对NULL指针的解引用操作

#include<stdio.h>
#include<stdlib.h>
void test()
{
	int* p = (int*)malloc(INT_MAX / 4);
	*p = 20;//如果 malloc 开辟空间失败,那么 p 的值是NULL,解引用,就会有问题。对一个没有指向任何内存空间的指针进行解引用
	free(p);
}
int main()
{
	test();
	return 0;
}

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

#include<stdio.h>
#include<stdlib.h>
void test()
{
	int i = 0;
	int* p = (int*)malloc(10 * sizeof(int));//开辟了一块40个字节大小的空间
	if (NULL == p)//判断开辟的内存空间
	{
		exit(EXIT_FAILURE);//结束程序
	}
	for (i = 0; i <= 10; i++)//0~10 11*4=44个字节,访问的内存空间超过了开辟的内存空间,越界访问
	{
		*(p + i) = i;//当i是10的时候越界访问
	}
	free(p);
}
int main()
{
	test();
	return 0;
}

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

#include<stdio.h>
#include<stdlib.h>
void test()
{
	int a = 10;
	int* p = &a;
	free(p);// a 的空间栈区开辟出来的,不是在堆区开辟出来的,而 free 函数只能释放在堆区上开辟出来的动态内存空间
}
int main()
{
	test();
	return 0;
}

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

#include<stdio.h>
#include<stdlib.h>
void test()
{
	int a = 10;
	int* p = &a;
	free(p);// a 的空间栈区开辟出来的,不是在堆区开辟出来的,而 free 函数只能释放在堆区上开辟出来的动态内存空间
}

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

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

#include<stdio.h>
#include<stdlib.h>
void test()
{
	int* p = (int*)malloc(100);
	p++;
	free(p);//p不再指向动态内存的起始位置,不再是完整的动态内存开辟的空间
	//free 只能从开辟好的内存空间起始位置开始释放,不能只释放一部分。
}

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

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

#include<stdio.h>
#include<stdlib.h>
void test()
{
    int *p = (int *)malloc(100);
    if(NULL != p)
    {
        *p = 20;
    }
}
int main()
{
    test();
    while(1);
}

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

4、动态内存经典笔试题 

 题目 1:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

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

存在问题:

(1)运行代码程序会出现崩溃的现象。

(2)程序存在内存泄漏的问题。

str 以值传递的形式给 p,p 是 GetMemory 函数的形参,只能函数内部有效等 GetMemory 函数返回之后,动态开辟内存尚未释放并且无法找到,所以会造成内存泄漏。如下图:

正确的形式如下: 

① 

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

/* 正确的写法 */
void GetMemory(char** p)//二级指针接收一级指针的地址
{
	*p = (char*)malloc(100);//解引用拿到str,将开辟的内存空间的地址赋值给str
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str);//传递地址,形参的改变能够影响到实参,目的是使str能够指向开辟的内存空间
	strcpy(str, "hello world");
	printf(str);
	free(str);//释放开辟的动态内存
	str = NULL;//同时将指向改地址的指针置NULL,防止野指针的存在,导致非法访问内存
}
int main()
{
	Test();
	return 0;
}

 ②

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

/* 正确的写法 */
char* GetMemory(char* p)
{
	p = (char*)malloc(100);
	return p;
}
void Test(void)
{
	char* str = NULL;
	str=GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
	free(str);//释放开辟的动态内存
	str = NULL;//同时将指向改地址的指针置NULL,防止野指针的存在,导致非法访问内存
}
int main()
{
	Test();
	return 0;
}

 题目 2

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

//返回栈空间的地址的问题
char* GetMemory(void)
{
	char p[] = "hello world";//在栈上开辟了一个内存空间,存储着字符串 hello world
	return p;//将开辟的地址返回出去
}//该函数结束时,会对在栈上开辟的空间进行回收,也就是把开辟的数组内存空间进行回收
void Test(void)
{
	char* str = NULL;
	str = GetMemory();//当str拿到函数返回回来的地址,
	printf(str);//拿着地址去访问已经被回收的空间,导致非法访问内存,输出随机值。
}
int main()
{
	Test();
	return 0;
}

正确的改法: 

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

char* GetMemory(void)
{
	static char p[] = "hello world";//此时存储该字符串的内存空间在静态区,就算该函数的生命周期结束了,也不会对该空间进行回收
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main()
{
	Test();
	return 0;
}

 题目 3

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

void GetMemory(char** p, int num)//二级指针接收一级指针的地址
{
	*p = (char*)malloc(num);//对二级指针解引用,拿到一级指针,将 malloc 开辟的内存的地址放到 srt 中
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);//内存泄漏,没有释放动态开辟的内存空间。有开辟没有释放,内存泄漏
    
    //改进
    //free(str);
    //str=NULL;
}
int main()
{
	Test();
	return 0;
}

题目 4

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
/*释放后的空间再次被使用*/
void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);//释放内存了,没有把指向的指针置NULL,str成为野指针
	if (str != NULL)
	{
		strcpy(str, "world");//非法访问内存,访问不属于自己的空间
		printf(str);
	}
}
int main()
{
	Test();
	return 0;
}

篡改动态内存区的内容,后果难以预料,非常危险。因为 free(str);之后,str 成为野指针,if(str != NULL) 语句不起作用。 

 正确的改法:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
/*释放后的空间再次被使用*/
void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
    str = NULL;
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}
int main()
{
	Test();
	return 0;
}
  • 6
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值