IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 长篇详解C语言动态内存管理 -> 正文阅读

[C++知识库]长篇详解C语言动态内存管理

前言

在这里插入图片描述

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

int main()
{
	int arr1[10]; //40个字节
	char arr2[40]; //40个字节

	return 0;
}
  1. 上述开辟空间的方式有两个特点
  • 空间开辟的大小是固定的。
  • 数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
  1. 但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。
    这时候就只能试试动态内存开辟了。

2、动态内存函数的介绍

2.1 malloc函数和free函数

malloc函数是动态内存开辟函数。动态内存开辟的空间在使用完毕以后,一定要释放,free函数就是释放函数。
malloc和free都声明在 stdlib.h头文件中。

void* malloc(size_t size);

size是申请的字节大小

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

  • 如果开辟成功,则返回一个指向开辟好空间的指针。
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  • 返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候,使用者自己来决定。
  • 如果参数 size 为0,malloc的行为是未定义的,却决于编译器。

void free(void* ptr);

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

  • 参数 ptr是指向先前使用malloc、calloc或realloc分配的内存块的指针。
  • 如果参数 ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。
  • 如果参数 ptr是NULL指针,则函数什么事都不做。

malloc函数与free函数展示

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>

int main()
{
	//申请空间
	int* p = (int*)malloc(40); //开辟40个字节,malloc的返回值是void*,所以自己想要什么类型的空间,需要强制类型转换
	int* ptr = p;

	if (ptr == NULL)
	{
		perror("malloc");
		return 1;
	}

    //使用动态开辟的空间
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*ptr = i;
		ptr++;
	}

	//使用完后,释放
	free(p);
	p = NULL; //p指向的空间存储的内容被释放,及时赋值为NULL,避免成为野指针
	return 0;
}

通过内存查看(p,10)
在这里插入图片描述

  1. 想使用动态内存开辟的空间用来存放整型,想将malloc函数的返回值为 void* 强制转为为 int*,因为指针类型决定了其能访问什么样的数据。因此开辟了40个字节,就能访问操作10个 int型的数据。
  2. 我们可以看到指针 p指向的空间已经被赋值为 0 1 2 3 4 5 6 7 8 9
  3. 在使用动态开辟的空间之前,要先判断开辟成功没有,即判断 ptr是否为空指针。
  4. 指针 p所指向的空间释放后及时赋值为NULL指针,避免成为野指针。
  5. 因为在赋值时,p的位置在变,因此 p指向的就不是之前所开辟空间的位置了,为了正确使用 free函数,先将 p指向的地址赋值给指针 ptr,在给开辟的空间赋值时,使用 ptr来操作,这样指针 p所指向的位置保持不变,在使用完后用free函数释放之前开辟空间的内容 free( p);

上述写法,因为指向动态内存开辟空间的指针随着使用,指针指向的位置会发生变化,下面代码进行优化。

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>

int main()
{
	//申请空间
	int* p = (int*)malloc(40); //开辟40个字节,malloc的返回值是void*,所以自己想要什么类型的空间,需要强制类型转换

	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}

	//使用动态内存开辟的空间
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;  //p的位置没有变
	}

	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	free(p);
	p = NULL; //p指向的空间存储的内容被释放,及时赋值为NULL,避免成为野指针
	return 0;
}

打印结果
在这里插入图片描述

  1. 这才是我们平时使用指针的常规写法,p+i,p本身并没有发生改变,即p指向的位置没有发生改变 。
  2. 展示一下申请空间过大,返回空指针所得到的结果。
    在这里插入图片描述
    在32位环境下,malloc申请 NT_MAX大的空间会失败,返回空指针,进而执行代码中的 if条件语句。
  3. 关于perror函数的细节,请看文章链接: 《字符串函数和内存操作函数》第10节。

使用完毕动态开辟的内存空间后,我们一定要释放掉动态开辟的内存空间中的内容。
当我们不释放动态申请的内存的时候,如果程序结束,动态申请的内存由操作系统自动回收,但是如果程序不结束,动态内存不会自动回收,就会形成内存泄漏的问题。如下代码:

int main()
{
	while (1)
	{
		malloc(1000);
	}
	return 0;
}

运行前
在这里插入图片描述
运行后
在这里插入图片描述

  1. 可以看到在程序运行后,一直在动态开辟内存空间,并且没有释放掉,所以电脑的内存占用增大了。不过由于电脑系统保护的原因,在开辟到一定大小后,停止开辟了。
  2. 所以在动态开辟的内存空间使用完毕后,一定要用free函数及时进行释放,避免出现内存泄漏的情况

2.2 calloc函数

calloc函数也用来动态内存分配。
calloc函数的区别是:calloc函数在动态内存开辟后,能够自动将内存空间中的元素全都初始化为0。malloc函数没有初始化。

void* calloc(size_t num, size_t size);

num是申请元素的个数,size是申请的每个元素的大小。
calloc函数展示

int main()
{
	//申请10个整型空间
	int* p = (int*)calloc(10, sizeof(int)); //calloc的返回值是void*,第一个参数是元素个数
	                                        //第二个参数是每个元素的大小,并且calloc申请的空间
	                                        //会被初始化为0
	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;
}

打印结果
在这里插入图片描述

  1. 可以看到,calloc函数将每个元素初始化为0了。
  2. malloc需要注意的如,使用前判断是否为NULL指针、使用后及时释放动态开辟内存空间中的内容这些注意事项,calloc函数全部都要注意。
  3. 同理,与malloc一样,使用者自己决定calloc函数开辟空间的类型。

2.3 realloc函数

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

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

  • ptr是要调整的内存地址
  • size是调整之后的新大小,比如已经开辟了40个自节,需要再开辟40个字节,那么size为80.
  • 返回值为调整之后的内存起始位置。
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
  • relloc函数在调整内存空间时是存在两种情况的:
    -情况1:原有空间之后有足够大的空间,可以继续往后开辟。

在这里插入图片描述
当是情况1的时候,要扩展内存就原有内存之后直接追加空间,原来空间的数据不发生变化。
-情况2:原有空间之后没有足够大的空间可以往后开辟

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

情形1代码展示

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>

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

	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i; //0 1 2 3 4 5 6 7 8 9
	}

	//当使用完后,洗碗存放20个元素,空间不够了,这时需要扩容
	int* ptr = (int*)realloc(p, 80);
	if (ptr != NULL)
	{
		p = ptr;
	}
	//扩容成功,开始使用

	//不再使用,开始释放
	free(p);
	p = NULL;
	return 0;
}

调试过程监视
realloc前
在这里插入图片描述
realloc后
在这里插入图片描述

  1. realloc再多开辟40个字节,可以发现realloc前后,p指向的地址始终没有变,说明后面有足够的空间用来动态开辟。

情形2代码展示

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>

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

	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i; //0 1 2 3 4 5 6 7 8 9
	}

	//当使用完后,洗碗存放20个元素,空间不够了,这时需要扩容
	int* ptr = (int*)realloc(p, 1000000);
	if (ptr != NULL)
	{
		p = ptr;
	}
	//扩容成功,开始使用

	//不再使用,开始释放
	free(p);
	p = NULL;
	return 0;
}

调试过程监视
realloc前
在这里插入图片描述
realloc后
在这里插入图片描述

  1. realloc再多开辟999960个字节,可以发现realloc前后,p指向的地址发生了改变,说明后面没有有足够的空间用来动态开辟。
  2. 在堆空间上另找一个合适大小的连续空间来使用,并且将原来内存中的数据移动到新的空间,返回值是调整之后的内存起始位置。

3、常见的动态内存错误

3.1 对NULL指针解引用操作

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdlib.h>

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

	//使用
	int i = 0;
	for (i = 0; i < 250; i++)
	{
		*(p + i) = i;
	}

	//使用完后,释放
	free(p);
	p = NULL;

	return 0;
}
  1. 这里对开辟的空间直接使用,没有对指针p是否为 NULL指针的判断,我们发现在使用时,对指针p进行了解引用,如果开辟空间失败,那么返回值是NULL指针,对NULL指针解引用,有可能产生不可预见的错误,程序会崩。

解决办法:对动态开辟内存的返回值进行判断。

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdlib.h>

int main()
{
	int* p = (int*)malloc(1000);
	
	//对返回值进行判断
	if (p == NULL)
	{
		return 1;
	}

	//使用
	int i = 0;
	for (i = 0; i < 250; i++)
	{
		*(p + i) = i;
	}

	//使用完后,释放
	free(p);
	p = NULL;

	return 0;
}

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

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdlib.h>

int main()
{
	int* p = (int*)malloc(100);//开辟了25个整型空间
	
	//对返回值进行判断
	if (p == NULL)
	{
		return 1;
	}

	//使用
	int i = 0;
	for (i = 0; i <= 25; i++)//访问了26个整型空间,越界访问
	{
		*(p + i) = i;
	}

	//使用完后,释放
	free(p);
	p = NULL;

	return 0;
}

运行
在这里插入图片描述

  1. malloc函数一共开辟了25个整型空间,但是在使用时一共访问了 26个整型空间,越界访问。

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

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdlib.h>

int main()
{
	int a = 10;
	int* p = &a;
   //.....

	free(p); //p不是动态内存开辟的空间,不能释放
	p = NULL;

	return 0;
}

运行
在这里插入图片描述

  1. a是局部变量,是在栈区上开辟的。而动态开辟内存是在堆区上开辟的。

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

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdlib.h>

int main()
{
	int* p = (int*)malloc(100);//开辟了25个整型空间
	
	//对返回值进行判断
	if (p == NULL)
	{
		return 1;
	}

	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*p = i;
		p++;
	}

	//使用完后,释放
	free(p);   //p已经不是首位置,释放只释放了一部分
	p = NULL;
	return 0;
}

运行
在这里插入图片描述

  1. malloc函数一共开辟了100个字节,当开辟的空间在使用完后,指针p已经不再指向动态开辟空间的起始位置,因此释放出错。

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

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdlib.h>

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

	//对返回值进行判断
	if (p == NULL)
	{
		return 1;
	}

	free(p);
	//.....
	free(p);//对同一块内存多次释放
	p = NULL;

}

运行
在这里插入图片描述

  1. 动态开辟的内存在已经释放一遍的情况下,再次进行释放,出错。

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

#define _CRT_SECURE_NO_WARNINGS 1

void test()
{
	int* p = malloc(100);
	//使用

	//使用完后不释放
}

int main()
{

	test();
	//.....

	while (1)
	{
		;
	}

	return 0;
}

  1. 动态开辟的内存在使用完后没有释放,并且程序一直在运行,那么之前动态开辟的内存的使用权一直没有还给操作系统,这样就会造成内存泄漏。

4、几个经典的笔试题

4.1 题目1

下面代码能打印出 hello world 吗?

#define _CRT_SECURE_NO_WARNINGS 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");  //对NULL的解引用操作,程序奔溃
	                             //对形参的操作不会影响实参,str依然是NULL指针
	printf(str);
}
int main()
{
	Test();
	return 0;
}

打印结果
在这里插入图片描述

  1. 代码结果没有打印出 hello world,为什么?
  • GetMemory(str); 将实参指针str 传过去,用形参指针p 接收,这里用的是传值调用。在函数GetMemory函数中动态开辟内存,开辟内存空间的地址传给指针p。这里从始至终,指针str的值都是NULL,因为传值调用,函数的形参和实参分别占用两个不同的内存块,形参是实参的临时拷贝,对形参的操作不会影响实参。
  • strcpy函数的两个参数都不能为空指针,strcpy函数的详情请参考文章《字符串函数和内存操作函数》的第2节。

上述代码修改

#define _CRT_SECURE_NO_WARNINGS 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);

	//使用完后,释放
	free(str);
	str = NULL;
}
int main()
{
	Test();
	return 0;
}

打印结果
在这里插入图片描述

  1. 这里是函数实参传递的是指针的地址,形参用二级指针p 接收,传址调用可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。对二级指针p解引用,也就找到了指针str,str直接指向动态开辟的内存。

4.2 题目2

下面代码能打印出 hello world 吗?

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char* GetMemory(void)
{
	char p[] = "hello world";
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();  //出了GetMemory函数,里面的局部变量就没有访问权限了,
	                    //虽然能通过地址找到GetMemory函数中的变量,不过没有访问权限,str是一个野指针
	printf(str);
}
int main()
{
	Test();
	return 0;
}

打印结果
在这里插入图片描述

  1. 函数中的局部变量一出函数就会销毁,虽然返回了局部变量的地址,能通过地址找到局部变量的位置,但是已经没有权限访问局部变量了。

下面代码错位原因与上相同

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int* test()
{
	int a = 10;
	return &a;
}
int main()
{
	int* p = test();
	printf("%d\n", *p);
	return 0;
}

打印结果
在这里插入图片描述

  1. 这段代码依然是一个错误代码,并且错误原因也是局部变量在其他函数中没有访问权限,那么为什么这个打印出来了?
    因为这个局部变量还没有被其他数据写入,如果在打印函数之前进行一些操作,局部变量就有可能被其他数据写入,进而打印不出来。如下,再多写一个打印函数:
    在这里插入图片描述
    第二个打印函数访问到的就不是10了。

4.3 题目3

下面代码运行结果是什么,有什么问题?

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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

打印结果
在这里插入图片描述

  1. 这段代码成功运行,且跟第一个题目的修改版相似,但是这个代码在使用完动态开辟的内存后没有释放。

正确写法如下

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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

打印结果
在这里插入图片描述

  1. 动态开辟的空间在使用完后一定要释放。

4.4 题目4

下面代码有什么问题?

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL)
	{
		strcpy(str, "world"); //非法访问,因为经过free,str指向的空间已经还给操作系统了
		printf(str);
	}
}
int main()
{
	Test();
	return 0;
}

在这里插入图片描述

  1. 打印结果为word,但是这段代码进行了非法访问,为什么?
  • 动态开辟空间在使用完毕后,已经通过free函数将空间中存储的内容进行了释放,这块空间的使用权限还给了操作系统。
  • 但是释放后,没有及时将指向这块空间的指针置为NULL指针。因此条件语句 if 成立,再次在 if 语句块中使用了动态开辟的空间,造成了非法访问。
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-07-21 21:19:47  更:2022-07-21 21:21:17 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/10 12:28:11-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码