动态内存管理(含经典面试题)

动态内存管理

    • 1. 为什么要有动态内存分配
    • 2. malloc和free
      • 2.1 malloc
      • 2.2 free
    • 3. calloc和realloc
      • 3.1 calloc
      • 3.2 realloc
    • 4. 常见的动态内存的错误
      • 4.1 对NULL指针的解引用操作
      • 4.2 对动态开辟空间的越界访问
      • 4.3 对非动态开辟内存使用free释放
      • 4.4 使用free释放一块动态开辟内存的一部分
      • 4.5 对同一块动态内存多次释放
      • 4.6 动态开辟内存忘记释放(内存泄漏)
    • 5. 动态内存经典笔试题分析
      • 5.1 题目1:
      • 5.2 题目2:
      • 5.3 题目3:
      • 5.4 题目4:

1. 为什么要有动态内存分配

我们已经掌握的内存开辟⽅式有:

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

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

  • 空间开辟大小是固定的。
  • 数组在申明的时候,必须指定数组的⻓度,数组空间⼀旦确定了大小不能调整

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运⾏的时候才能知道,那数组的编译时开辟空间的⽅式就不能满⾜了。

C语⾔引⼊了动态内存开辟,让程序员自己可以申请和释放空间,就⽐较灵活了。

2. malloc和free

2.1 malloc

动态内存管理(含经典面试题)_第1张图片

C语言提供了⼀个动态内存开辟的函数:

 void* malloc (size_t size);

这个函数向内存申请⼀块连续可用的空间,并返回指向这块空间的指针。

  • 如果开辟成功,则返回⼀个指向开辟好空间的指针。
  • 如果开辟失败,则返回⼀个 NULL 指针,因此malloc的返回值⼀定要做检查。
  • 返回值的类型是void * ,所以malloc函数并不知道开辟空间的类型,具体在使⽤的时候使⽤者⾃⼰来决定。
  • 如果参数 size 为0,stdlib.hmalloc的⾏为是标准是未定义的,取决于编译器
#include 
#include 
int main()
{
	//int arr[10]
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("maclloc");
		return 1;
	}
	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	//打印
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	//这里没有释放空间,程序结束后会自动释放,但最好手动释放
	return 0;
}

运行结果如图:
动态内存管理(含经典面试题)_第2张图片

当申请的空间太大的时候会开辟失败,所以要对maclloc的返回值进行合理判断

#include 
#include 
int main()
{
	//int arr[10]
	int* p = (int*)malloc(90000000000 * sizeof(int));
	if (p == NULL)
	{
		perror("maclloc");
		return 1;
	}
	return 0;
}

VS2022 X86环境下 运行结果如图
动态内存管理(含经典面试题)_第3张图片

2.2 free

C语⾔提供了另外⼀个函数free,专⻔是⽤来做动态内存的释放和回收的,函数原型如下:
动态内存管理(含经典面试题)_第4张图片

void free (void* ptr);

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

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

malloc和free都声明在 stdlib.h 头⽂件中。

#include 
#include 
int main()
{
	//int arr[10]
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("maclloc");
		return 1;
	}
	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	//打印
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	//释放空间
	free(p);
	p = NULL;
	return 0;
}

运行结果如图:
动态内存管理(含经典面试题)_第5张图片

3. calloc和realloc

3.1 calloc

C语⾔还提供了⼀个函数叫 calloc , calloc 函数也⽤来动态内存分配。原型如下:
动态内存管理(含经典面试题)_第6张图片

void* calloc (size_t num, size_t size);
  • 函数的功能是为 num 个大小为 size 的元素开辟⼀块空间,并且把空间的每个字节初始化为0。
  • 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
int main()
{
	int* p = malloc(10, sizeof(int));
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%x ", *(p + i));
	}
	free(p);
	p = NULL;
	return 0;
}

运行结果如图:
动态内存管理(含经典面试题)_第7张图片
所以,如果malloc函数没有初始化打印结果是随机值

int main()
{
	int* p = calloc(10, sizeof(int));
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	free(p);
	p = NULL;
	return 0;
}

运行结果如图:
动态内存管理(含经典面试题)_第8张图片
所以如果我们对申请的内存空间的内容要求初始化,那么可以很⽅便的使⽤calloc函数来完成任务。

3.2 realloc

动态内存管理(含经典面试题)_第9张图片

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

函数原型如下:

void* realloc (void* ptr, size_t size);
  • ptr 是要调整的内存地址
  • size 调整之后新大小
  • 返回值为调整之后的内存起始位置,如果调整失败会返回NULL
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
  • realloc在调整内存空间的是存在两种情况:
    • 情况1:原有空间之后有足够大的空间
    • 情况2:原有空间之后没有足够大的空间
      动态内存管理(含经典面试题)_第10张图片

情况1
在已经开辟好的空间后边,有足够的空间,直接进行扩大。
扩大空间后,直接返回旧的空间的起始地址!

情况2
在已经开辟好的的空间后面没有足够的空间,会堆空间上另找⼀个合适大小的连续空间来使用。
在这种情况下,realloc函数会在内存的堆区重新找一个空间(满足新的空间的大小需求的),同时会把旧的数据拷贝到新的空间,然后释放旧的空间,同时返回新起始空间的地址

由于上述的两种情况,realloc函数的使用就要注意⼀些。

#include 
#include 
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	//打印
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}
	//空间不够,想扩大空间,20个字节
	int* ptr = (int*)realloc(p, 20 * sizeof(int));
	if (ptr != NULL)
	{
		p = ptr;
	}
	else
	{
		perror("realloc");
		return 1;
	}
	free(p);
	p = NULL;
	return 0;
}

通过监视窗口可以观察到
动态内存管理(含经典面试题)_第11张图片

#include 
#include 
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	//打印
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}
	//假如要扩大的空间比较大
	int* ptr = realloc(p, 100 * sizeof(int));
	if (ptr != NULL)
	{
		p = ptr;
	}
	else
	{
		perror("realloc");
		return 1;
	}
	free(p);
	p = NULL;
	return 0;
}

通过监视窗口可以观察到
动态内存管理(含经典面试题)_第12张图片

当realloc函数的的第一个参数是NULL,那么他和malloc函数的功能是一样的。

#include 
#include 
int main()
{
	int* p = (int*)realloc(NULL, 40);
	if (p == NULL)
	{
		perror("realloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		p[i] = i;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}
	free(p);
	p = NULL;
}

运行结果如图:
动态内存管理(含经典面试题)_第13张图片

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

4.1 对NULL指针的解引用操作

void test()
{
	int* p = (int*)malloc(INT_MAX / 4);
	*p = 20;//如果p的值是NULL,就会有问题
	free(p);
}

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

#include 
#include 
int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		return 1;
	}
	int i = 0;
	for (i = 0; i <= 10; i++)
	{
		*(p + i) = i;//当循环到11次时就越界访问了
	}
	//...
	free(p);
	p = NULL;
	return 0;
}

运行结果如图:
动态内存管理(含经典面试题)_第14张图片

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

#include 
#include 
int main()
{
	int a = 10;
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		return 1;
	}
	//使用
	//...
	p = &a;
	free(p);//p指向的空间就不是堆区的空间
	p = NULL;
	return 0;
}

运行结果如图:
动态内存管理(含经典面试题)_第15张图片

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

#include 
#include 
int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		return 1;
	}
	//使用
	p++;
	//释放
	free(p);
	p = NULL;
	return 0;
}

运行结果如图:
动态内存管理(含经典面试题)_第16张图片

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

#include 
#include 
int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//使用
	//...
	//释放
	free(p);
	free(p);
	p = NULL;
	return 0;
}

运行结果如图:
动态内存管理(含经典面试题)_第17张图片

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

#include 

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

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

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

切记:动态开辟的空间⼀定要释放,并且正确释放。

5. 动态内存经典笔试题分析

5.1 题目1:

#include 
#include 
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();
}

通过调试窗口可以看到:
动态内存管理(含经典面试题)_第18张图片
因为函数在传参的时候形参是实参的一份临时拷贝,所以这里只是把str的值传给p,p的改变并不会影响str,当出了GetMemory函数后str的值还是NULL,这时候在把字符串"hello world"拷贝给str时,就会对空指针进行解引用,而出现错误

  • GetMemory函数采用值传递的方式,无法将malloc开辟空间的的地址,返回在str中,调用结束后str依然是NULL指针
  • strcpy中使用了str,就是对NULL指针解引用操作
  • 因为在GetMemory函数中并没有释放p指向的空间,出了函数p被销毁就找不到动态开辟的空间了,所以会造成内存泄漏

那这个printf有问题吗?

#include 
int main()
{
	char arr[] = "abcdef";
	printf("%s\n", arr);
	printf("abcdef\n");
	printf(arr);

	return 0;
}

运行结果如图:
动态内存管理(含经典面试题)_第19张图片
可以看到这些写法都没有什么问题,这是因为常量字符串在使用的时候传递的首字符的地址,数组名也是首元素的地址,所以这样写是没问题的。

如果要修改的话,这个代码可以用二级指针把str的地址作为函数的参数传过去

#include 
#include 
#include 
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;
}
int main()
{
	Test();
	return 0;
}

运行结果如图:
动态内存管理(含经典面试题)_第20张图片

也可以把指向动态分配的内存的指针p作为返回值,传给str也能实现。

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

运行结果如图:
动态内存管理(含经典面试题)_第21张图片

5.2 题目2:

#include 
#include 
char* GetMemory(void)
{
	char p[] = "hello world";
	return p;
}

void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}

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

运行结果如图:
动态内存管理(含经典面试题)_第22张图片

这里为什么打印的是乱码呢?

这里p数组是在GetMemory函数里面定义的,所以它的作用域只在GetMemory函数里面有效,但GetMemory函数运行结束,p数组就被销毁的,这时候虽然把p,也就是数组首元素的地址返回了,但是str虽然能找到这块空间,但是没有使用权限了,这时候str就是野指针。

这是一个典型的返回栈空间的地址的问题。

5.3 题目3:

#include 
#include 
void 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()
{
	Test();
	return 0;
}

运行结果如图:
动态内存管理(含经典面试题)_第23张图片
这里运行结果没有什么问题,但是这个代码没有释放动态开辟的内存,这里不会出问题是因为在程序结束,编译器会自动把空间释放,但是最好要手动释放空间。

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

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

5.4 题目4:

#include 
#include 
void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	//str是野指针
	if (str != NULL)
	{
		strcpy(str, "world");//非法访问
		printf(str);
	}
}

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

这里虽然释放了str指向的空间,但是没有把str置为NULL,所以str这时是野指针,所以下面的代码就形成了非法访问的问题。

所以最好这样修改代码

#include 
#include 
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}

所以,大家一定要注意的些问题才能写出正确的代码。

你可能感兴趣的:(算法,c语言,开发语言,vscode)