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++知识库]动态内存管理(动态内存函数的介绍,c/c++经典笔试题,柔性数组)

🎓为什么存在动态内存分配

常见的创建变量,开辟空间,分配内存的方式如下:

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

??上面的方式称为静态开辟,它有如下几个特点

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

如通过下面的简单例子,你就明白,下面是一个简略的未完善的代码:
假设我创建一个学生结构体数组,结构体成员包括姓名,性别,年龄,假设我首先静态的给数组开辟100个元素空间,之后向里面又添加元素,但慢慢的里面的元素个数会超过我们定义的最大元素个数,可能之后造成数组越界,不够用等情况,所以这种情况我们可以动态的开辟数组空间,够了就用,不够继系开辟即可。

#include<stdio.h>
#define MAX 1000//初始大小,100个学生
#define ADD 5//增加5个学生
 struct student
{
	char name[20];
	char sex[4];
	int age;
	
}student;
 //静态版本
//struct student data[MAX];
 //动态版本
 struct student *data;
 void add(struct student* pc)
 {
	 data = (struct student*)malloc((MAX+ADD) * sizeof(struct student));
	 if (data == NULL)
	 {
		 perror("InitContact");
		 return;
	 }
	 //.............................
	 //.............................
 }
 int main()
 {
	 add(data);
	 return 0;
 }

🎓动态内存函数的介绍

??malloc和free

这两个函数总是成对出现的,一个开辟内存,一个释放内存,这两个函数的单独使用极有可能会导致程序出错。
动态内存开辟的函数malloc

函数原型 void* malloc (size_t size);
函数说明

  • 这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。 如果开辟成功,则返回一个指向开辟好空间的指针
  • size_t size表示开辟几个字节大小的空间
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
  • 如果参数 size为0,malloc的行为是标准是未定义的,取决于编译器。

动态内存释放函数free

函数原型 void free (void* ptr);
函数说明

  • -ptr 传过来的是开辟空间的起始地址, 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的,如果为空指针什么也不会发生
  • 如果参数 ptr 是NULL指针,则函数什么事都不做。 free函数释放内存空间后,并不会将接受开辟空间起始地址的的指针置为空指针

malloc和free都声明在 stdlib.h 头文件中,接下来我举一个开辟内存释放内存的例子:

#include <stdlib.h>
int main()
{
	//动态内存开辟的
	int* p = (int*)malloc(10*sizeof(int));//void*
	//使用这些空间的时候首先判断空间是否开辟成功
	if (p == NULL)
	{
	
		perror("main");//如果在main函数里面,开辟空间失败,通过perror函数打印错误信息
		return 0;
	}
	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;//通过指针加减整数的方式赋初值
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));//p[i] --> *(p+i)这两种访问方式等价的,p指向开辟的那块空间的起始地址,
//相当于数组名通过数组名加下标的方式访问访问开辟的空间

	}

	//回收空间
	free(p);///free函数释放P指向的内存空间,但不会把p指针里面地址的内容释放,这可能就会造成,p又通过地址访问之前的内存空间,造成内存非法访问
	p = NULL;//所以自己动手把p置为NULL
	return 0;
}

💏💏这里有几个细节的地方学要注意:
malloc开辟空间后,free函数释放P指向的内存空间,但不会把p指针里面地址的内容释放,这可能就会造成,p又通过地址访问之前的内存空间,造成内存非法访问,所以一定要手动的把把P置为NULL

??calloc

C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。

函数原型如下: void* calloc (size_t num, size_t size); 函数说明:

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

举个例子我们使用calloc开辟10个整形空间的大小

#include <stdlib.h>
int main()
{
	//int*p = (int*)malloc(40);
	int* p = calloc(10, sizeof(int));

	if (p == NULL)
		return 1;//如果为空直接结束后面的执行

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

	free(p);
	p = NULL;

	return 0;
}

打印calloc开辟空间里面的内容如图:
在这里插入图片描述
所以如果我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。

??realloc

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

函数原型如下: void* realloc (void* ptr, size_t size);
函数说明:

  • 这个函数可以在原有的其它内存函数开辟空间的基础上,继系管理空间的大小,也可以自己重新开辟一块新的内存空间,开辟空间时不初始化里面的内容。
  • ptr 是要调整的内存地址(原内存的起始地址), size 为调整之后内存空间的新大小 返回值为调整之后的内存起始位置。
  • 这个函数调整原内存空间时,如果原内存空间后面有足够的空间则开辟相应的空间,如果原内存空间后面没有足够的空间可以开辟,就在堆区重新找一块空间开辟内存,之后还会将原来内存中的数据移动到新 的空间。
    realloc在调整内存空间的是存在两种情况:
    情况1:原有空间之后有足够大的空间
    情况2:原有空间之后没有足够大的空间

以下面的的代码为例,下图分析两种情况:
情况一:
在这里插入图片描述

情况二:
在这里插入图片描述

#include <stdlib.h>
int main()
{
	int* p = (int*)calloc(3, sizeof(int));//开辟3个大小的整形空间

	if (p == NULL)
	{
		perror("main");
		return 1;
	}
	
	//如果这里需要p指向的空间更大,需要6个int的空间
	//realloc调整空间

	int*ptr = (int*)realloc(p, 6*sizeof(int));
	if (ptr != NULL)
	{
		p = ptr;
	}
	//使用
	int i = 0;
	for (i = 0; i < 6; i++)
	{
		printf("%d ", *(p + i));
	}
	//回收空间
	free(p);
	p = NULL;

	return 0;
}

??当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。当是情况2 的时候,如果原内存空间后面没有足够的空间可以开辟,就在堆区重新找一块空间开辟内存,之后还会将原来内存中的数据移动到新的空间。

🎓常见的动态内存错误

👀对NULL指针的解引用操作

空指针就是没有任何指向的指针,不能对它进行解引用,加减整数等操作,任何对它的操作都是不和法的,都会造成程序的崩溃。malloc函数在开辟内存失败时,会返回空指针,所以在对malloc函数开辟的空间进行使用之前要判断是否为返回空指针。

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
	int* p = (int*)malloc(10000000000000000);
	//开辟失败,返回空指针没有判断就使用
	//使用

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

}

👀对动态开辟空间的越界访问
其实这个很好理解,就像静态的创建10个数组元素,你可不能访问20个元素啊;如下面的代码,malloc动态的开辟了10整形大小的空间,下面使用空间时却访问了40个整形元素,这也是不合法的。

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		return 1;
	}
	int i = 0;
	//越界访问
	for (i = 0; i < 40; i++)
	{
		*(p + i) = i;
	}
	
	free(p);
	p = NULL;
	return 0;
}

🔧🔩对非动态开辟内存使用free释放
通过什么方式创建,就要通过什么方式释放

int main()
{
	int arr[10] = { 0 };//栈区
	int* p = arr;
	//使用
	
	free(p);//使用free释放非动态开辟的空间
	p = NULL;

	return 0;
}

??使用free释放一块动态开辟内存的一部分free(p),p一定要指向开辟空间的起始位置,这样才能释放开辟的整块动态空间,如果p因为使用原因进行了移动一定要定义另一个指针记录p的开始指向位置,否则进行的内存释放是局部的内存释放。下面代码中通过指针的移动对空间内容进行了赋值,p发生了移动,但没有记录p的起始位置。

int main()
{
	int* p = malloc(10 * sizeof(int));
	if (p == NULL)
	{
		return 1;
	}
	int i = 0;

	for (i = 0; i < 5; i++)
	{
		*p++ = i;
	}
	
	free(p);
	p = NULL;

	return 0;
}

🎸🎧对同一块动态内存多次释放
对于开辟的一块动态空间,一次释放就行,但有时由于程序的复杂,在多个函数里面使用这块空间,也就可能会进行多次释放。

int main()
{
	int* p = (int*)malloc(100);
	//使用
	//释放
	free(p);
	p = NULL;

	test()
	{
	//释放
	free(p);
	return 0;
	}
	
}

👻🤡动态开辟内存忘记释放(内存泄漏)
如下p是局部变量,出了这个函数就销毁了,下面的main 函数里面就不能使用了,之后就找不到p原来指向的空间的内容了,下面就不能把这块空间释放掉,造成内存泄漏,时间一长会消耗很多内存,如果服务器里面有内存泄漏,导致这个服务器崩溃。

 void test()
{
	int* p = (int*)malloc(100);
	if (p == NULL)//p是局部变量,出了这个函数就销毁了
		
	{
		
		return;
	}
	else
	{
		//使用
	}
	//一块空间开辟了自己用,用完了一定要记得释放,这里不释放,下面就不能把这块空间释放掉,造成内存泄漏
}

int main()
{
	test();//函数调用
	//....
	return 0;
}

🎓几个经典的笔试题

📝题目一:
首先思考这个代码有哪些问题??

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. 存在内存泄漏

图示解释📐????

在这里插入图片描述
??代码修改的两种方法
??在函数调用时,传值调用是无法改变实参的大小的,要传地址。

//改1
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;
}
//改:2
//char* GetMemory(char* p)//改变函数的返回值,返回动态开辟空间的起始地址(char*)
//{
//	p = (char*)malloc(100);
//	return p;
//}
//void Test(void)
//{
//	char* str = NULL;
//	str = GetMemory(str);
//	strcpy(str, "hello world");
//	printf(str);//?
//	//printf("hello world");//char *p = "hello world";
//	free(str);
//	str = NULL;
//}
//int main()
//{
//	Test();
//	return 0;
//}

📝题目二:
?静态开辟的空间是在栈上,栈上开辟的空间出了作用域就销毁了,动态开辟的空间是在堆上开辟的,要么自己手动释放空间,要么程序结束自动释放空间。所以下面的代码,随机打印内存中的一些值,没有达到预想到的效果。

//GetMemory 函数内部创建的数组是在栈区上创建的
//出了函数,p数组的空间就还给了操作系统
//返回的地址是没有实际的意义,如果通过返回的地址,去访问内存就是非法访问内存的

char* GetMemory(void)
{
	char p[] = "hello world";
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);//没有返回确定的地址,随机打印内存中的一些值
}

📝题目三
?返回&x是无效的,局部变量返回地址是无效的

int test (void)
{
	int x = 10;
	return (&x);
}

📝题目四
?局部变量不初始化,随机赋值,导致指向不明确,解引用有问题,野指针问题。

int test(void)
{
	int *x;
	*x = 10;
	return x;

}

📝题目五
?开辟空间没有free释放掉

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

📝题目六
?动态空间开辟好后,free释放开辟的空间,那么维护开辟空间起始位置的指针也应该置为空指针,这是紧紧相连的步骤,不可缺少。虽然free(str)释放了开辟好的空间,但str里面任然存储着开辟空间的起始地址,free不会释放strl里面的内容的,导致非法访问,导致程序错误。

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/C++程序的内存开辟

💡C/C++程序内存分配的几个区域如下图:
不同的变量,程序在内存中占有不同的区域,理解他们所在的区域,理解他们的作用域与生命周期,可以帮助我们更好地编写程序。

在这里插入图片描述

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有
    限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似
    于链表。
  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
    编译产生的可执行代码(可执行程序)是放在代码段区域的,常量字符串
    也是放在代码段里面的。
    💡有了这幅图,我们就可以更好地理解static关键字修饰的局部变量了
    实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
    但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁所以生命周期变长。

🎓 柔性数组

柔性数组(flexible array)这个概念我们很少听到,但是它确实是存在的。 C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员。我自己对于柔性数组的理解就是,柔性数组是定义在结构体当中的一个成员,它的起始大小为零,在使用过程中,根据情况的需要,通过动态内存开辟函数改变它的大小,达到数组内容改变的效果。
如:

struct S
{
	int n;
	int arr[];//大小是未知
   // int arr[0];//大小是未知,这种柔性数组的写法也对,大小未知,不是柔性数组,这个写法就是非法的
};

??柔性数组的特点:

  • 结构体中的柔性数组成员前面必须至少一个其他成员。
  • sizeof 返回的这种结构大小不包括柔性数组的内存。
  • 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应 柔性数组的预期大小。

?? 如下代码进行结构体的大小的计算:

#include<stdio.h>
 struct s 
{
	int i;
	int a[];//柔性数组成员
}a;


int main()
{
	printf("%d\n", sizeof(struct s ));//输出的是4
	return 0;
}

下面为柔性数组的简单使用:
代码一
首先数组元素个数为10个,不够时再动态的开辟为20个

#include<stdio.h>
struct S
{
	int n;//4
	int arr[0];//大小是未知
};

int main()
{
	//期望arr的大小是10个整形
	struct S*ps = (struct S*)malloc(sizeof(struct S)+10*sizeof(int));
	ps->n = 10;
	int i = 0;
	//使用柔性数组
	for (i = 0; i < 10; i++)
	{
		ps->arr[i] = i;
	}
	//增加数组元素个数
	struct S* ptr = (struct S*)realloc(ps, sizeof(struct S)+20*sizeof(int));
	if (ptr != NULL)
	{
		ps = ptr;
	}
	//使用和上面的一样
	
	//释放
	free(ps);
	ps = NULL;
	return 0;
}
/

🙉可能有老铁觉得,柔性数组的存在是多余的,没有柔性数组,我也可以可以创建一个结构体的数组成员,实现动态的变化,那我们通过下面的代码简单的来分析一下吧,我们不使用柔性数组,实现上面柔性数组的功能,对比一下
代码二

#include<stdio.h>
struct S
{
	int n;
	int* arr;
};

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	if (ps == NULL)
		return 1;
	ps->n = 10;
	ps->arr = (int*)malloc(10 * sizeof(int));
	if (ps->arr == NULL)
		return 1;
//使用数组
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->arr[i] = i;
	}
	//增加
	int*ptr = realloc(ps->arr, 20 * sizeof(int));
	if (ptr != NULL)
	{
		ps->arr = ptr;
	}
	//使用和上面的一样只是数组元素改变

	//释放
	free(ps->arr);
	ps->arr = NULL;
	free(ps);
	ps = NULL;

	return 0;
}

在这里插入图片描述
🙉通过以上代码和图示分析,为了实现柔性数组的功能,我们通过上面的代码会存在如下的问题:

上述 代码1 和 代码2 可以完成同样的功能,但是 方法1 的实现有两个好处:
第一个好处是:方便内存释放包含柔性数组的空间开辟只需要一次malloc,不包含的需要两次malloc,两次free释放内存
第二个好处是:再由空间局部性原理可知,这样有利于访问速度.连续的内存有益于提高访问速度,也有益于减少内存碎片。多次开辟堆上面的空间,导致堆空间残留许多未被利用的内存块(内存碎片),占用内存,影响程序运行。

上面所写的内容只是一些基础的知识点,和本篇博客内容相关的博客文章可以参考这位大佬的文章:C语言结构体里的数组和指针
扩展阅读:
《高质量c/c++编程》这本书里面后面的附录里面有本篇博客一些笔试题的原题,及其他的一些题目知识点,其次这本书对于我们形成好的代码风格,编写高质量的程序也有帮助,值得一读,网盘分享链接链接:https://pan.baidu.com/s/1AOUSdyas7N1YP-XqQ2rFZQ
提取码:6666

记🉐点👍🏻🍹持哦

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-07-24 11:15:53  更:2021-07-24 11:15:55 
 
开发: 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/8 1:50:08-

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