【C语言进阶】动态内存管理及柔性数组

动态内存的开辟在C语言中相当重要的知识


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

内存的开辟方式:

int a=20;//在栈空间上开辟4个字节

int arr[10];//在栈空间上开辟40个字节的连续空间

这种开辟空间的方式有两个特点:

1、开辟的空间大小是固定的

2、数组在声明的时候,必须指定数组长度,它需要的内存在编译时分配。

但是这种内存开辟的方式存在缺陷,比如我们在写通讯录管理系统时指定了100个元素,但当我们填入元素过多时空间会不够用,当联系人较少时,又会产生空间的浪费,所以我们可以用一种灵活的方式,用多少提供多少,这种方式即动态内存管理。

2、动态内存函数的介绍

2.1malloc和free

void * malloc(size_t size);

**·**如果分配成功则返回指向被分配内存的指针,否则返回空指针NULL。

**·**返回类型为void*,所以malloc函数并不知道开辟空间的类型,具体在使用时侯使用者自己来决定。

**·**如果参数size为0,malloc的行为是标准为定义的,取决于编译器。

free是专门用来做动态内存的释放和回收的:

void free(void* ptr)

**·**如果参数ptr指向的空间不是动态开辟的,那么free的行为是未定义的

**·**如果参数ptr是NULL指针,则函数什么事都不做

malloc函数与free函数的声明都在stdlib.h中

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
//malloc函数的使用
int main()
{
	int arr[10] = { 0 };
	//动态内存分配
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		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;
}
//没有free,并不是说内存空间就不回收了,当程序退出的时候,系统会自动回收内存空间的

注意:在使用malloc开辟空间时,使用完成一定要释放空间,否则可能导致内存泄漏。

内存泄漏:是指程序动态分配内存后,未能及时释放这些内存,导致系统无法再为其他对象分配内存,或者可能导致系统内存耗尽的现象。

2.2calloc

calloc函数也用来动态内存分配

void * calloc(size_t num,size_t size);

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

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

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	free(p);
	p = NULL;
	return 0;
}//calloc与malloc的最大区别就是calloc会在返回地址之前把申请的空间的每个字节初始化为全0。

2.3realloc

·​​​​​​​ realloc函数的出现让动态内存管理更加灵活

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

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

·​​​​​​​ ptr是要调整的内存地址

·​​​​​​​ size是调整后的新大小

·​​​​​​​返回为调整之后的内存起始位置。

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

·​​​​​​​realloc在调整内存空间存在两种情况:

1、原有空间之后有足够大的空间;(直接追加)

2、原有空间之后没有足够大的空间;(会覆盖其他数据,所以开辟一块更大的空间,把原有数据拷贝过去)

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i+1;
	}//扩容
	int* ptr = (int*)realloc(p, 80);//不能放在p中,因为若是给的数字过大,无法申请空间,从而返回空指针,会导致p原来的数据丢失
	{
		if (ptr != NULL)//扩容成功
		{
			p = ptr;
		}
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
    free(p);
    p=NULL;
	return 0;
}

3、常见的动态内存错误

3.1对NULL指针解引用操作

int main()

{

int* p = (int*)malloc(40);//应该判断p是否为空指针,否则空间可能开辟失败

*p = 20;

return 0;

}

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

int main()

{

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

if (p == NULL)

{

return 1;

}

int i = 0;

for (i = 0; i <= 10; i++)

{

p[i] = i;

}

free(p);

p = NULL;

return 0;

}

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

int main()

{

int a = 10;

int* p = &a;

free(p);

return 0;//这个代码会崩溃因为p所指向的空间是在栈区开辟的,并不是动态开辟的(堆区),不能进行释放

}

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

int main()

{

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

if (p == NULL)

{

return 1;

}

int i = 0;

for (i = 0; i < 10; i++)

{

*p = i;

p++;

}

free(p);

p = NULL;//因为p的位置会发生改变不再是起始空间的地址,释放仅仅释放了一部分

return 0;

}

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

int main()

{

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

if (p == NULL)

{

return 1;

}

//.....

free(p);//p一旦释放完后,所指向的空间已经回收,但p依然会记得地址,此时p就相当于一个野指针相当危险

//.....

free(p);

return 0;

}

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

//情形一:

void test()

{

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

//........

int x = 0;

scanf("%d", &x);//如果在这里用户输入1,是会直接返回,函数瞬间结束,malloc开辟的空间就永远无法释放,从而导致内存的泄露

if (x == 1)

return;

//.......

free(p);

p = NULL;

}

int main()

{

test();

return 0;

}

//情形二:

int* test()

{

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

if (p == NULL)

{

return p;

}

//.........

return p;

}

int main()

{

int* ptr = test();

//忘记释放

return 0;

}

4、典型例题分析

4.1题目1:

cpp 复制代码
void GetMeory(char* p)//形参p,里面放的也是NULL
{
	p = (char*)malloc(100);//p被赋值,使用malloc申请100个字节的空间,此时p不再是NULL,而是所申请空间的首元素(是个地址)
}//这个函数一旦结束,因为p是形参,只能在函数内部使用,出了函数,p就销毁了,但malloc申请的空间依然还在,从而发生空间泄露
void test(void)
{
	char* str = NULL;//str是局部变量
	GetMeory(str);//传递的是实参str,存放的是空指针
	strcpy(str, "hello world");//str此时依然为空指针,代码必然崩溃,因为strcpy模拟实现包括解引用操作,而对NULL解引用会出现错误
	printf(str);
}
int main()
{
	test();
	return 0;
}

改写代码:

cpp 复制代码
void GetMeory(char** p)
{
	*p = (char*)malloc(100);//对p解引用得到的其实就是str
}
void test(void)
{
	char* str = NULL;
	GetMeory(&str);//这个时候str里面存放的就是动态内存开辟的100个字节的空间
	strcpy(str, "hello world");
	printf(str);
	//释放
	free(str);
	str = NULL;
}
int main()
{
	test();
	return 0;
}

4.2题目2:

cpp 复制代码
void GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);
}
int main()
{
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
	return 0;
}//这个逻辑上是没有任何问题的,可以打印出hello,唯一的问题就是没有free

4.3题目3:

cpp 复制代码
void test(void)
{
	char* str = (char*)malloc(100);//申请了100个字节的空间放到str
	strcpy(str, "hello");
	free(str);//把str指向的空间释放掉,但str并没有变,动态开辟的空间实际上已经还给操作系统了
	if (str != NULL)
	{
		strcpy(str, "world");//这个时候str就已经是一个野指针了,形成非法访问
		printf(str);
	}
}
int main()
{
	test();
	return 0;
}

5、c/c++程序的内存开辟

简单了解即可!

6、柔性数组

typedef struct st_type

{

int i;

int a[0];//也可以写成a[ ],柔性数组成员

}type_a;

6.1柔性数组的特点:

**·**结构中的柔性数组成员前面必须至少一个其他成员;

**·**sizeof返回的这种结构大小不包括柔性数组的内存;

**·**包含柔性数组成员的结构用malloc( )函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

6.2柔性数组的使用:

cpp 复制代码
//柔性数组的使用,如何访问空间
struct S
{
	int n;
	int arr[];
};
int main()
{
	struct S* ps=(struct S*)malloc(sizeof(struct S) + 40);//40是柔性数组想要开辟的字节大小,
    ps->n = 100;
    int i = 0;
    for (i = 0; i < 10; i++)
    {
	  ps->arr[i] = i;
    }
    for (i = 0; i < 10; i++)
    {
	  printf("%d ", ps->arr[i]);
    }//柔性数组成员
    struct S* ptr=(struct S*)realloc(ps, sizeof(struct S) + 80);
    if(ptr!=NULL)
    {
      ps=ptr;
    }
    //........
    free(ps);
    ps=NULL;//ptr已经赋给ps了所以不用释放ptr,释放平时即可
	return 0;
}
cpp 复制代码
struct S
{
	int n;
	int* arr;
};
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	if (ps == NULL)
	{
		return 1;
	}
	ps->n = 100;
	ps->arr = (int*)malloc(40);
	if (ps->arr == NULL)
	{
		return 1;
	}
	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->arr[i] = i;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", ps->arr[i]);
	}
    //释放
    free(ps->arr);
    ps->arr=NULL;
    free(ps);
    ps=NULL;
	return 0;
}

6.3柔性数组的优势

第一个好处:方便内存释放

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

第二个好处:有利于访问速度

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

相关推荐
胡少侠711 分钟前
python 获取当前git的repo地址
开发语言·git·python
Jason-河山15 分钟前
PHP爬虫:获取商品销量详情API的利器
开发语言·爬虫·php
奶香滴小馒头15 分钟前
Day101 代码随想录打卡|动态规划篇--- 分割等和子集
数据结构·算法·leetcode·游戏·动态规划
Change is good2 小时前
python: 数字类型的一些函数
开发语言·python·算法
卡卡卡卡罗特2 小时前
naocs注册中心,配置管理,openfeign在idea中实现模块间的调用,getway的使用
java·开发语言
星迹日2 小时前
Java: 数据类型与变量和运算符
java·开发语言·经验分享·笔记
六点半8882 小时前
【C++】vector 常用成员函数的模拟实现
开发语言·c++·算法
Kalika0-02 小时前
输出不能被3整除的数-C语言
c语言·开发语言
knoci3 小时前
【Go】-基于Gin框架的IM通信项目
开发语言·后端·学习·golang·gin
luluvx3 小时前
LeetCode[简单] 136. 只出现一次的数字
算法·leetcode·职场和发展