动态内存管理与柔性数组

动态内存管理与柔性数组

在你立足处深挖下去,就会有泉水涌出!别管蒙昧者们叫嚷:“下边永远是地狱!”

博客主页:KC老衲爱尼姑的博客主页

博主的github,平常所写代码皆在于此

刷题求职神器

共勉:talk is cheap, show me the code

作者是爪哇岛的新手,水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!


刷题求职神器

在下给诸位推荐一款巨好用的刷题求职神器,如果还有小伙伴没有注册该网站,可以点击下方链接直接注册,注册完后就可以立即刷题了。

在这里插入图片描述

传送门:牛客网

文章目录

  • 动态内存管理与柔性数组
    • 前言
    • 动态内存管理函数
      • malloc-函数
      • calloc-函数
      • realloc-函数
      • free-函数
    • C/C++程序的内存开辟
    • 经典的动态内存面试题:
      • 题目1.
      • 题目2:
      • 题目3:
      • 题目4:
    • 柔性数组
      • 柔性数组的特点:
      • 柔性数组空间的开辟
      • 柔性数组的使用
      • 柔性数组的优势
        • 优势一:
        • 优势二:

前言

我们通常都是创建变量和创建数组来存储数据,这两种使用内存的方式都是在内存申请固定大小的空间,创建变量每种类型就限定了开辟内存时的空间大小,使用数组在声明的时候,指定数组的长度也就是确定在内存空开辟的空间。但是对于空间大小的需要有时候是在程序运行的时候才知道,此时就只能试试动态内存开辟,接下来就来介绍一些开辟内存的函数。

动态内存管理函数

malloc-函数

void* malloc (size_t size);

malloc函数的功能是开辟指定字节大小的内存空间,如果失败则返回NULL,成功则返回开辟空间的首地址。传参是只需要传入开辟内存所需的字节个数。

假设我们开辟40个字节的空间

#include
#include
int main()
{
	int* ps = (int*)malloc(40);//返回值为void*,需强转
	if (ps == NULL)
	{
		perror("malloc:");
	}
		free(ps);
		ps = NULL;
	return;

注意:

  1. malloc所开辟的空间,不会对空间的内容进行初始化所以空间中的值为随机值。
  2. 使用malloc开辟空间后最好其返回值进行判断避免出现访问空指针。
  3. 使用完空间后需对开辟的空间进行释放,避免造成内存泄漏。

calloc-函数

void* calloc (size_t num, size_t size);

calloc函数和malloc函数同样是开辟指定大小的空间,如若开辟成功则返回成功开辟空间的首地址,失败则返回NULL。但它有两个参数,第一个参数为存放元素的个数,第二个参数为每个元素的字节大小。

在使用calloc函数开辟40个整型的空间

#include
#include
int main()
{
	int* ps = (int*)calloc(40,sizeof(int));//返回值为void*,需强转
	if (ps == NULL)
	{
		perror("malloc:");
	}
	else
	{
		free(ps);
		ps = NULL;
	}
	return;
}

注意:

calloc函数开辟的内存空间的内容会被初始化为0.

realloc-函数

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

realloc函数可以是动态内存的管理更加灵活,原因如下,有时我们会发现申请的空间过大或者过小,这机会造成内存浪费或者不利于我们使用空间。这时realloc就可以解决以上难题。realloc函数可以对已经动态开辟好的内存进行调整,它的第一个参数为原有空间的起始地址,第二个参数为调整后空间的新大小。返回值同malloc函数和calloc函数一致。申请空间成功返回该空间的首地址,失败则返回NULL。

我们在原有空间上再开辟40个空间,使用realloc调整空间大小

#include
#include


int main()
{
	int* ptr = (int*)malloc(40);
	if (ptr == NULL)
	{
		perror("malloc:");
	}
	int* ps = (int*)realloc(ptr,80);
	free(ps);
	ps = NULL;
	return 0;  
}

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

使用malloc开辟40个字节的空间,当需要在扩大40个字节的时候有两种情况

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

动态内存管理与柔性数组_第1张图片

情况一:我们在原有空间上再开辟40个字节的空间,如果原有空间的后面依旧有足够的空间够用,就在后面开辟40 个字节,并返回该内存空间的首地址(原有空间的首地址)。

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

动态内存管理与柔性数组_第2张图片

情况二:原有空间后没有足够的空间,此时会再堆内存中重新开辟一块足够大的空间,同时会把原有空间中的数据拷贝到新空间中来,并且会返回新空间的首地址。

free-函数

void free (void* ptr);

free函数的作用就是把动态开辟空间的内存全部都释放掉,即还给操作系统。把动态开辟的内存释放后,该内存就没有使用权限了。

注意

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

举个例子

#include 
int main()
{
 //代码1
 int num = 0;
 scanf("%d", &num);
 int arr[num] = {0};
 //代码2
 int* ptr = NULL;
 ptr = (int*)malloc(num*sizeof(int));
 if(NULL != ptr)//判断ptr指针是否为空
 {
 int i = 0;
 for(i=0; i<num; i++)
 {
 *(ptr+i) = 0}
 }
 free(ptr);//释放ptr所指向的动态内存
 ptr = NULL;
 return 0;
}

使用free函数释放后,需将指针ptr置为空指针,因为ptr指向的空间被释放后,该内存就无法访问了,相当于ptr成了野指针即使它存了该空间的首地址,为避免我们去使用它去访问非法空间,故将它置为NULL。

常见的动态内存错误

1 对NULL指针的解引用操作

当我们使用malloc,calloc,realloc开辟空间是,若是开辟空间失败是会返回NULL。假如我们没有对返回的指针进行检测就容易在后面使用的时候对NULL解引用。

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

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

假如我们只开辟了40个字节,我们绝不能去访问第41个字节。这就好比手上只有100快钱,但是在商店确要用100快钱买200块的东西。显然是不可能的。

void test()
{
 int i = 0;
 int *p = (int *)malloc(10*sizeof(int));
 if(NULL == p)
 {
 exit(EXIT_FAILURE);
 }
 for(i=0; i<=10; i++)
 {
 *(p+i) = i;//当i是10的时候越界访问
 }
 free(p);
}

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

上文已经说到,free是对动态开辟的空间进行释放,不能对变量开辟或者数组开辟的空间进行释放。

void test()
{
 int a = 10;
 int *p = &a;
 free(p);
}

运行结果

动态内存管理与柔性数组_第3张图片

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

free函数的第一个参数是需要释放空间的首地址,所需传参是必须传动态开辟空间的首地址,而不能是其后面的地址,不能只释放一部分,只能全部释放。

void test()
{
 int *p = (int *)malloc(100);
 p++;
 free(p);
}

运行依旧报错

动态内存管理与柔性数组_第4张图片

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

动态开辟的空间只能释放一次,不能多次释放。因为第一个释放后,第二次再释放的话是无法访问该内存的,避免进行多次释放可以在第一次释放完后将该指针置为NULL即可,如此第二次释放的时候因为是空指针所以free什么都不会做。

void test()
{
 int *p = (int *)malloc(100);
 free(p);
 free(p);//重复释放
}

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

切记每次使用完动态开辟的空间之后需要对该空间进行释放,并且将该指针置空。如果忘记将动态开辟的空间释放会造成内存泄漏。

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

C/C++程序的内存开辟

C/C++程序内存分配的几个区域:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结 束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是 分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返 回地址等。
  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分 配方式类似于链表。
  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

经典的动态内存面试题:

题目1.

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

程序时先调用Test(),进入Text()函数后为str赋值后调用GetMemory函数,进入 GetMemory函数动态开辟100个字节,并将该空间的首地址赋值给p。当执行完GetMemory函数时,存放p的单元被销毁。所以str依旧没有发生变化,既然p没有被销毁也无法改变str,因为GetMemory函数采用传值调用是无法改变str的。因此str依旧是空指针,当strcpy时自然不能将字符串拷贝到str中。

题目2:

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

题目2与题目1相差无几,进入 GetMemory()函数使用数组开辟内存,最后返回数组名。实际上一出函数p就会被销毁。即使返回给str,str也不能访问数组p中的字符串,只能打印随机值出来比如:烫烫烫烫烫烫烫烫圉??。

题目3:

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

GetMemory();函数采用传址调用,进入GetMemory()函数后动态开辟100个字节的空间并将该空间的首地址赋值给str是会改变str的值,所以是能打印出来hello,但是该程序存在漏洞就是没有对动态开辟的内存进行释放,会造成内存泄漏。

题目4:

void Test(void)
{
 char *str = (char *) malloc(100);
 strcpy(str, "hello");
 free(str);
 if(str != NULL)
 {
 strcpy(str, "world");
 printf(str);
 }
}

动态开辟100个字节并将该空间的首地址赋值给str,然后使用free函数释放了动态开辟的内存。也就是str无法访问该空间了。后面将world的拷贝到str中,也就是将world的首地址赋值给了str,所以能打印出来world。

柔性数组

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

例如:

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

柔性数组存在于结构体中,为结构体中的 最后一个成员且数组的大小没有指定。

柔性数组的特点:

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

比如使用sizeof计算包含柔性数组的结构体

struct s
{
	int i;
	int arr[];
};
int main()
{
	printf("%d", sizeof(struct s));//打印结果为4
	return 0;
}

柔性数组空间的开辟

柔性数组空间的开辟需使用malloc函书动态开辟内存,使得柔性数组的成员能存放5个整型的元素。

struct S
{
	int i;
	int arr[];
};
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));//sizeof计算结构体大大小并没有包含数组的内存大小
	return 0;
}

柔性数组的使用

typedef struct st_type
{
	int i;
	int a[0];//柔性数组成员
}type_a;//结构体变量

int main()
{
	int i = 0;
	type_a* p = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int));
	//业务处理
	p->i = 100;
	for (i = 0; i < 100; i++)
	{
		p->a[i] = i;
	}
	free(p);

	return 0;
}

这样柔性数组成员a,相当于获得了100个整型元素的连续空间。

柔性数组的优势

模拟实现柔性数组的功能


typedef struct st_type
{
	int i;
	int* p_a;
}type_a;

int main()
{

	type_a* p = (type_a*)malloc(sizeof(type_a));
	p->i = 100;//对结构体成员i初始化
	p->p_a = (int*)malloc(p->i * sizeof(int));
	
	for (int i = 0; i < 100; i++)
	{
		p->p_a[i] = i;//
	}

	free(p->p_a);
	p->p_a = NULL;
	free(p);
	p = NULL;
	return 0;
}

柔性数组是结构体中的数组,该大小不定可以使用动态内存管理的函数来改变其空间大小。我们模拟实现其功能就是在结构体中定义一个指针,并用malloc函数开辟空间,然后使结构体中的指针指向动态开辟的内存即可。当我们使用完动态开辟的空间之后需要对内存释放,此时需先释放p->p_a指向的空间,再释放 p指向的空间。如果先释放p的空间,在释放p->p_a指向的空间的话,p->pa是无法找到所要释放的空间的。

第一种使用柔性数组

动态内存管理与柔性数组_第5张图片

第二种模拟柔性数组的功能

动态内存管理与柔性数组_第6张图片

使用柔性数组的时候我们只需要释放一次内存空间即可,并且访问速度快,因为它是结构体的成员都在结构体所在的内存空间里,空间是连续的,所以访问更快。而第二种虽然模拟实现了柔性数组的功能,但是我们需要释放两次动态开辟的内存,并且释放的顺序不能错,先释放结构体成员内的指针所指向的空间,再释放指向结构体的指针。我们在使用需要对访问两次才能得到数组的元素,因为我们单独为数组开辟了空间,而这块空间与结构体所占有的空间不是连续的所以访问速度也慢了。

上述 代码1 和 代码2 可以完成同样的功能,但是 方法1 的实现有两个好处:

优势一:

方便内存释放:

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

优势二:

这样有利于访问速度.

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

最后的话

各位看官如果觉得文章写得不错,点赞评论关注走一波!谢谢啦!。如果你想变强那么点我点我 牛客网。

在这里插入图片描述

你可能感兴趣的:(C语言,c语言,c++,开发语言)