动态内存管理

目录

[1 · 为什么要有动态内存分配](#1 · 为什么要有动态内存分配)

[2 · malloc](#2 · malloc)

[3 · calloc](#3 · calloc)

[4 · free](#4 · free)

[5 · realloc](#5 · realloc)

[6 · 常见的动态内存相关错误](#6 · 常见的动态内存相关错误)

[6 - 1 · 对空指针的解引用操作](#6 - 1 · 对空指针的解引用操作)

[6 - 2 · 对动态开辟空间的越界访问](#6 - 2 · 对动态开辟空间的越界访问)

[6 - 3 · 对非动态开辟的内存使用free释放](#6 - 3 · 对非动态开辟的内存使用free释放)

[6 - 4 · free没有完全释放动态开辟的空间](#6 - 4 · free没有完全释放动态开辟的空间)

[6 - 5 · 对同一块动态内存多次释放](#6 - 5 · 对同一块动态内存多次释放)

[6 - 6 · 动态开辟内存忘记释放(内存泄漏)](#6 - 6 · 动态开辟内存忘记释放(内存泄漏))

[7 · 举个栗子](#7 · 举个栗子)

[8 · 柔性数组](#8 · 柔性数组)

[8 - 1 · 柔性数组的特点](#8 - 1 · 柔性数组的特点)

[8 - 2 · 柔性数组的使用](#8 - 2 · 柔性数组的使用)

[8 - 3 · 柔性数组的优点](#8 - 3 · 柔性数组的优点)

[9 · 总结 C/C++ 中程序内存区域划分](#9 · 总结 C/C++ 中程序内存区域划分)

总结


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

简单来说,就是想让程序员灵活的控制空间。

我们之前其实已经了解过两种内存开辟方式:

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

上述方式 都是在栈上开辟空间,并且
空间开辟大小是固定的。
数组在声明的时候,必须指定数组的长度,数组空间⼀旦确定了大小就不能调整。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知
道,那上述两种开辟空间的方式就不能满足需求了。
C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了。


2 · malloc

malloc 这个函数的作用是用来进行内存开辟的。使用需包含头文件 stdlib.h

原型如下:

复制代码
void* malloc (size_t size);

功能是申请一片连续的空间,单位是字节,有返回值,返回值是这片空间的起始地址。
如果开辟成功,则返回⼀个指向开辟好的空间的指针。
如果开辟失败,则返回⼀个 NULL 指针,因此malloc的返回值⼀定要做检查。
返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,使用的时候使用者自己来决定。这时就需要用到强制类型转换了。
如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器,可能会给你一个地址,也可能发生意料之外的事。
我们可以这样使用:

复制代码
int* p = (int*)malloc(sizeof(int) * 5);
if (p == NULL)
{
	perror("malloc");
	exit(1);
}

如果开辟失败,返回的就是空指针,所以在我们尝试开辟之后最后判断一下。可以使用perror ,如果开辟错误也就不用再使用,直接提前结束。这里的 exit 会直接结束程序,如果返回0说明是正常返回,如果返回其他值说明是异常返回。

注意:malloc 以及后面将介绍的 calloc free realloc 这四个与动态内存分配有关的函数操作的都是内存 都是在堆区上的。


3 · calloc

calloc 的使用和 malloc 差不多,区别只在与 calloc 会把申请的空间里的每个字节初始化为0。使用需包含头文件stdlib.h

原型如下:

复制代码
void* calloc (size_t num, size_t size);

功能是开辟 num 个大小为 size 的空间,并将申请的空间里的每个字节初始化为0。

与malloc 的区别仅有上述这一条,这里不过多赘述。

我们可以测试一下:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

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

运行一下:


所以如果我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。


4 · free

free 这个函数是专门用来进行动态内存的释放与回收的。使用需包含头文件 stdlib.h

原型如下:

cpp 复制代码
void free (void* ptr);

free函数用来释放动态开辟的内存
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
如果参数 ptr 是NULL指针,则函数什么事都不做。
简单来说,想要释放哪块空间 就把哪块空间的起始地址给 free
free 会把空间的使用权还给操作系统,但是不会对传给free的参数进行修改,所以我们传过去的指针的指向不会改变,但是指向的空间却还给了操作系统,所以此时就变成了野指针。
因此,为了避免野指针的情况,我们在使用完free之后,最好给参数赋值空指针。
比如:

cpp 复制代码
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
	perror("calloc");
	exit(1);
}

free(p);
p = NULL;

5 · realloc

realloc函数让动态内存管理更加灵活。
有时我们会发现之前申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使
用内存,我们⼀定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存进行大小的调整。
使用需包含头文件 stdlib.h

原型如下:

cpp 复制代码
void* realloc (void* ptr, size_t size);

ptr 是要调整的内存地址
size 是 调整之后的新大小
返回值为调整之后的内存起始位置。
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
realloc在调整内存空间的是存在两种情况的:
情况1:原有空间之后有足够大的空间,此时如果要扩展内存,就会在原有的空间后面直接扩展,原来空间的数据不发生变化。
情况2:原有空间之后没有足够大的空间,此时就会 在堆空间上另找⼀个合适大小 的连续空间来使用,并且将原有空间进行free,这样函数返回的就是⼀个新的内存地址。
因此对 realloc 的使用就要有所注意:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

int main()
{
	int* ptr = (int*)malloc(sizeof(int) * 50);
	if (ptr == NULL)
	{
		perror("malloc");
		exit(1);
	}
	//对申请的空间进行使用
	//......
	//空间不足
	int* p = realloc(ptr, sizeof(int), 500);
	if (p != NULL)
	{
		ptr = p;
	}
	//使用
	//......
	free(ptr);
	ptr = NULL;
	return 0;
}

6 · 常见的动态内存相关错误

6 - 1 · 对空指针的解引用操作

如下:

复制代码
#include <stdio.h>
#include <stdlib.h>

int main()
{
	int* p = (int*)malloc(sizeof(int) * 5);
	*p = 10;//此时如果p为NULL就会出问题
	return 0;
}

因此 我们在使用完内存开辟函数之后一定要检查一下。


6 - 2 · 对动态开辟空间的越界访问

如下:

复制代码
int main()
{
	int i = 0;
	int* p = (int*)malloc(10 * sizeof(int));
	if (NULL == p)
	{
		perror("malloc");
		exit(1);
	}
	for (i = 0; i <= 10; i++)
	{
		*(p + i) = i;//当i是10的时候就会越界访问
	}
	free(p);
	return 0;
}

6 - 3 · 对非动态开辟的内存使用free释放

如下:

复制代码
int main()
{
	int a = 10;
	int* p = &a;
	free(p);//对非动态开辟的内存使用free释放
	return 0;
}

局部变量是存储在栈区的,而我们上面介绍的四个函数操作的是堆区。


6 - 4 · free没有完全释放动态开辟的空间

如下:

复制代码
int main()
{
	int* p = (int*)malloc(sizeof(int) * 20);
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*p = i;
		p++;
	}
	free(p);
	p = NULL;
	return 0;
}

当for循环结束,p已经不在动态申请的内存的起始地址了,此时free就不会释放 p地址之前的动态申请的内存。


6 - 5 · 对同一块动态内存多次释放

如下:

复制代码
int main()
{
	int* p = (int*)malloc(100);
	free(p);
	//......
	free(p);//重复释放
	return 0;
}

有一种办法可以避免,在第一次free之后将p置为空指针,这样第二个free传过去的是空指针,那就什么也不会做。


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

如下:

复制代码
void test()
{
	int* p = (int*)malloc(100);
	if (NULL != p)
	{
		*p = 20;
	}
}
int main()
{
	test();//内存泄漏
	while (1);
}

忘记释放不再使用的动态开辟的空间会造成内存泄漏。
内存是一种资源 申请了之后不用了就要回收。
使用malloc/calloc/realloc 申请的内存,如果不想用的时候,就可以用free 释放,如果没有用free释放,当程序运行结束的时候也会由操作系统回收。
但是就像我们上面的代码一样,如果出现了24小时都要运行的情况,那么如果没有使用free,申请的空间就一直没有释放,就造成了内存泄漏。


7 · 举个栗子

了解完常见错误,我们实践一下看看:

复制代码
void GetMemory(char *p)
 {
     p = (char *)malloc(100);
 }
void Test(void)
 {
     char *str = NULL;
     GetMemory(str);
     strcpy(str, "hello world");
     printf(str);
 }

如果运行Test,会有什么样的结果呢?

问题有:

1.内存泄漏

2.程序崩溃

我们展开讲讲第二个问题:

str 初始化为空指针,然后将str传参,形参p拿到空指针,随后p拿到malloc申请空间的首元素地址,随后出了GetMemory函数,形参p销毁,在此过程中并没有改变str,因此str仍为空指针,此时程序进行到strcpy 就发生了对空指针的解引用操作,程序崩溃。

解决方法有二:

一是传参时 传 str 的地址,但由于str是一级指针,所以形参应是二级指针,那么GetMemory就应该改成这样:

复制代码
void GetMemory(char **p)
 {
     *p = (char *)malloc(100);
 }

第二种改法 将GetMemory的类型改为char* 将p作为返回值,用str接收。


8 · 柔性数组

C99 中,在结构体中,最后一个成员是数组 并且这个数组没有指定大小,这个数组才是柔性数组。
如下:

复制代码
struct St
{
	int i;
	int a[0];//柔性数组成员
};

有些编译器会报错无法编译可以改成:

复制代码
struct St
{
	int i;
	int a[];//柔性数组成员
};

8 - 1 · 柔性数组的特点

结构中的柔性数组成员前面必须至少有⼀个其他成员。
sizeof 计算的这种结构体大小不包括柔性数组的内存。
包含柔性数组成员的结构用malloc函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
我们测试一下:

cpp 复制代码
#include <stdio.h>

struct St
{
	int i;
	int a[];//柔性数组成员
};

int main()
{
	printf("%zd\n", sizeof(struct St));
	return 0;
}

运行一下:


8 - 2 · 柔性数组的使用

如下:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

struct S
{
	int i;
	int arr[];
};

int main()
{
	struct S* p = (struct S*)malloc(sizeof(struct S) + sizeof(int) * 5);
    if (p == NULL)
    {
    	perror("malloc");
    	exit(1);
    }
    p->i = 10;
    for (int j = 0; j < 5; j++)
    {
    	p->arr[j] = j;
    }
    //......
    //调整空间
    struct S* ptr = (struct S*)realloc(p,sizeof(struct S) + sizeof(int) * 10);
    if (ptr != NULL)
    {
    	p = ptr;
    }
    //......
    free(p);
    p = NULL;
	return 0;
}

这里我们用 maloc 申请了24个字节的空间,其中前4个字节归i,后20个归arr。我们可以使用realloc对这块申请的空间进行伸缩,这就是柔性。


8 - 3 · 柔性数组的优点

不使用柔性数组也能达到同样的效果,如下:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

struct S
{
	int i;
	int* arr;
};

int main()
{
	struct S* p = (struct S*)malloc(sizeof(struct S));
	if (p == NULL)
	{
		perror("malloc p");
		exit(1);
	}
	p->arr = (int*)malloc(sizeof(int) * 5);
	if (p->arr == NULL)
	{
		perror("malloc arr");
		exit(1);
	}
	p->i = 10;
	for (int j = 0; j < 5; j++)
	{
		p->arr[j] = j;
	}
	//......
	//调整空间
	int* ptr = (int*)realloc(p->arr,sizeof(int) * 10);
	if (ptr != NULL)
	{
		p->arr = ptr;
	}
	//......
	free(p->arr);
	p->arr = NULL;
	free(p);
	p = NULL;
	return 0;
}

但是使用柔性数组有两个好处:
第⼀个好处是:方便内存释放
如果我们的代码是在⼀个给别⼈用的函数中,你在里面做了⼆次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返回给用户⼀个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第⼆个好处是:这样有利于访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎片。
当多次使用malloc后,申请的空间中间的空隙就是内存碎片。


9 · 总结 C/C++ 中程序内存区域划分

如下图:


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


总结

以上简单介绍了动态内存管理相关内容,关于C语言的其余内容,请期待后续更新


以上内容如有错误或不准确之处,欢迎指出,或者你有更好的想法,也欢迎交流。

相关推荐
qeen878 小时前
【数据结构】二叉树基本概念及堆的C语言模拟实现
c语言·数据结构·c++·
凉、介8 小时前
C 语言类型强转引发的隐蔽内存破坏问题分析
c语言·开发语言·笔记·学习·嵌入式
mount_myj17 小时前
长长久久【C语言】
c语言
Legendary_00820 小时前
LDR6500:USB‑C DRP PD协议芯片技术详解与应用实践
c语言·开发语言
dgaf1 天前
DX12 快速教程(17) —— 立体图标与合并渲染
c语言·c++·3d·图形渲染·d3d12
念恒123061 天前
进程控制---自定义Shell
linux·c语言
程序员JerrySUN1 天前
Jetson边缘嵌入式实战课程第二讲:JetPack 和 SDK Manager 是什么
c语言·开发语言·网络·udp·音视频
我不是懒洋洋1 天前
布谷鸟过滤器:比布隆过滤器更优雅的判重方案
c语言·经验分享
忡黑梨1 天前
eNSP_从直连到BGP全网互通
c语言·网络·数据结构·python·算法·网络安全