C语言 动态内存管理

目录

  • [1. C/C++程序的内存分配](#1. C/C++程序的内存分配)
  • [2. 动态内存分配的作用](#2. 动态内存分配的作用)
  • [3. malloc - 分配内存](#3. malloc - 分配内存)
  • [4. free - 释放内存](#4. free - 释放内存)
  • [5. calloc - 分配并清零内存](#5. calloc - 分配并清零内存)
  • [6. realloc - 调整之前分配的内存块](#6. realloc - 调整之前分配的内存块)
  • [7. 常见的动态内存的错误](#7. 常见的动态内存的错误)
    • [7.1 对空指针解引用](#7.1 对空指针解引用)
    • [7.2 对动态开辟空间的越界访问](#7.2 对动态开辟空间的越界访问)
    • [7.3 对非动态开辟内存使用free](#7.3 对非动态开辟内存使用free)
    • [7.4 使用free释放动态开辟内存的一部分](#7.4 使用free释放动态开辟内存的一部分)
    • [7.5 对同一块动态内存重复释放](#7.5 对同一块动态内存重复释放)
    • [7.6 动态开辟内存未释放](#7.6 动态开辟内存未释放)
  • [8. 动态内存相关题目](#8. 动态内存相关题目)
    • [8.1 题目1](#8.1 题目1)
    • [8.2 题目2](#8.2 题目2)
    • [8.3 题目3](#8.3 题目3)
    • [8.4 题目4](#8.4 题目4)
  • [9. 柔性数组](#9. 柔性数组)
    • [9.1 柔性数组的定义](#9.1 柔性数组的定义)
    • [9.2 柔性数组的特点](#9.2 柔性数组的特点)
    • [9.3 柔性数组的使用](#9.3 柔性数组的使用)
    • [9.4 柔性数组的优点](#9.4 柔性数组的优点)

正文开始

动态内存管理,顾名思义就是动态的、灵活的管理内存的分配,这在工程中有着重要的用途,下面我们来学习一下如何实现。

1. C/C++程序的内存分配

C/C++程序会分配在以下位置:

  • 栈区(stack):主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等,函数执行结束后自动释放。
  • 堆区(heap):动态内存管理的区域,由程序员自行开辟和释放。
  • 数据段 / 静态区(static):存放全局变量、静态数据。程序结束后由系统释放。
  • 代码段:存放可执行代码、只读常量(例如字符串常量)等。

图例:

今天我们所学习的动态内存所分配的区域就是堆区(heap)

2. 动态内存分配的作用

通常我们开辟内存的方式有:

c 复制代码
//在栈空间上开辟四个字节
int a = 0;

//在栈空间上开辟二十个字节的连续空间
char b[20] = { 0 };

但是!上述开辟空间的两种方式有两个很致命的缺点:

  • 空间开辟的大小是固定不变
  • 数组在定义时,就已经确定了数组的长度,后期不能再次调整

在实际需求中,我们对于内存的需求往往是多变的,所以我们需要灵活的、可调整的内存申请方式,C语言中为我们引入了动态内存开辟,让开发者可以自己申请和释放空间。

3. malloc - 分配内存

作用:分配指定字节的未初始化内存 ;使用该函数须引用头文件stdlib.h,本文其他所学函数也同样须引用此头文件,后文不再赘述。详情戳我>>><stdlib.h>

函数原型:

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

malloc 用法:

  • 在栈空间上开辟size个字节的未被使用的空间
  • 函数返回值为void *,需要使用强制类型转换来确定类型
  • 如果开辟成功,则返回一个指向所开辟空间的指针
  • 如果开辟失败,则返回空指针NULL,所以使用 malloc 函数要检查其返回值
  • 如果参数size为0,则该函数行为未定义

例如:

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

int main()
{
	int num = 0;
	scanf("%d", &num);
	int arr[256] = { 0 };
	int* p = (int*)malloc(num * sizeof(int));//动态内存申请
	if (p == NULL)//判断是否申请成功
		return 1;
	int i = 0;
	//使用动态内存
	for (i = 0; i < num; i++)
	{
		*(p + i) = i;
	}
	for (i = 0; i < num; i++)
	{
		printf("%d ", *(p + i));
	}
	return 0;
}

4. free - 释放内存

作用:释放之前动态内存分配的空间,防止多余的空间占用。

函数原型:

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

free 用法:

  • 释放动态内存空间,即之前由malloc()calloc()aligned_alloc()realloc()所分配的内存
  • 参数ptr指向动态开辟内存的起点,即上述动态内存管理函数的返回值
  • ptr所指向的内存不是动态开辟的或者不是动态开辟内存的起点,则函数行为未定义
  • 若参数为NULL,则函数啥都不干

例如,将上述代码优化一下:

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

int main()
{
	int num = 0;
	scanf("%d", &num);
	int arr[256] = { 0 };
	int* p = (int*)malloc(num * sizeof(int));//动态内存申请
	if (p == NULL)//判断是否申请成功
		return 1;
	int i = 0;
	//使用动态内存
	for (i = 0; i < num; i++)
	{
		*(p + i) = i;
	}
	for (i = 0; i < num; i++)
	{
		printf("%d ", *(p + i));
	}
	//释放内存
	free(p);
	//设为空指针,避免野指针的出现
	p = NULL;
	return 0;
}

5. calloc - 分配并清零内存

作用:分配内存,并将分配存储中的所有字节初始化为0

函数原型:

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

calloc 用法:

  • calloc 函数将num个大小为size的元素开辟一块空间,并且把空间的每个字节都初始化为0
  • 若开辟成功,返回值为指向开辟空间的首地址的指针
  • 若开辟失败,返回值为空指针NULL

例如:

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

int main()
{
	//申请空间
	int* p = (int*)calloc(10, sizeof(int));
	//判断是否申请成功
	if (p == NULL)
		return 1;
	*p = 2;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	//释放内存
	free(p);
	//置为空指针,避免野指针的出现
	p = NULL;
	return 0;
}

malloc 和 calloc 对比:

6. realloc - 调整之前分配的内存块

作用:对动态开辟内存大小进行调整

函数原型:

c 复制代码
void *realloc( void *ptr, size_t new_size );

realloc 用法:

  • ptr所指向的空间调整为new_size个字节的大小
  • ptr所指向的空间必须是动态开辟内存
  • 若待调整空间后面有足够大的空间,则直接在原有内存之后追加空间,原数据不发生变化
  • 若待调整空间后面没有足够大的空间,则重新在堆空间上另找一个合适大小的连续空间使用,并将原数据复制到新空间
  • 若成功,则返回指向新分配内存的指针;若失败,则返回空指针NULL

例如:

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

int main()
{
	//申请动态内存
	int* ptr = (int*)malloc(100);
	
	//判断是否申请成功
	if (ptr == NULL)
		return 1;
	
	//使用申请的空间
	//...

	//调整动态内存大小
	
	//1.直接使用待调整空间的地址接收返回值
	ptr = realloc(ptr, 200);

	//2.使用中间变量接收返回值
	int* p = (int*)realloc(ptr, 300);
	if (p == NULL)//判断是否调整成功
		return 1;
	ptr = p;
	return 0;
}

上述代码中,书写了两种接收 realloc 函数返回值的方式,我们更推荐第二种方式。第一种方式中,直接使用待调整空间的地址接收返值,若调整失败,则会返回空指针NULL,这样的话,原数据就会丢失。而第二种方式则是在确保了调整成功的情况下才将待调整空间的地址接收返回值,更为安全。

7. 常见的动态内存的错误

注:以下代码均为错误示范

7.1 对空指针解引用

c 复制代码
void test1()
{
	int *p = (int*)malloc(40);
	//若开辟失败,则返回空指针,没有进行判断就直接解引用
	*p = 2;
	free(p);
}

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

c 复制代码
void test2()
{
	int* p = (int*)malloc(20);
	if (p == NULL)
		return 1;
	//只能存放五个整型,所以越界访问了
	*(p + 5) = 3;
	free(p);
}

7.3 对非动态开辟内存使用free

c 复制代码
void test3()
{
	int a = 0;
	int* p = &a;
	//非动态开辟内存
	free(p);
}

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

c 复制代码
void test4()
{
	int *p = (int*)malloc(100);
	p++;
	//p不再是动态开辟内存的起点
	free(p);
}

7.5 对同一块动态内存重复释放

c 复制代码
void test5()
{
	int *p = (int*)malloc(100);
	free(p);
	free(p);//重复释放
}

7.6 动态开辟内存未释放

c 复制代码
void test()
{
	int *p = (int*)malloc(100);
	if(p == NULL)
		return 1;
	//申请完未释放
	//出了函数后使用者也不能再使用这一块空间
	//操作系统也没使用权限
	//造成了内存泄漏
}

int main()
{
	test();
	while(1);
}

所以在使用动态内存的时候,要确保在哪个函数内申请的空间,就在哪个函数内正确释放掉,否则就会出现内存泄漏

8. 动态内存相关题目

8.1 题目1

c 复制代码
#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);
}
int main()
{
	Test();
	return 0;
}

在主函数中调用了Test()函数,Test()函数中调用了GetMemory()函数,其中 str 作为参数传递进去,但 str 是一个指针,GetMemory()函数的参数是一个指针,所以将 str 传递进去就相当于传值调用,也就是说,GetMemory()函数并没有真正的改变 str 所指向的地址,它依旧为空指针,传进 strcpy 函数的第一个参数是一个空指针,这就导致了程序崩溃

可修改为:

c 复制代码
#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;
}

8.2 题目2

c 复制代码
#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();
	printf(str);
}
int main()
{
	Test();
	return 0;
}

GetMemory()函数返回变量 p,并且在Test()中使用 str 接收,但char p[]变量在Test()中已经销毁了,使用权限已经还给操作系统了

也就是说,GetMemory()仅仅是将一个地址传递了出去,但地址所指向的内存已经没有使用权限了

那么 str 接收地址后,就变成了一个野指针,所指向的內容是不确定的

8.3 题目3

c 复制代码
#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;
}

上述代码唯一的问题就是,使用完动态内存后没有将动态内存释放

8.4 题目4

c 复制代码
#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");
		printf(str);
	}
}
int main()
{
	Test();
	return 0;
}

上述代码中已经将 str 释放掉了,将使用权限还给了操作系统,后面却继续使用 str,导致非法访问内存空间

9. 柔性数组

当我们要保存相同类型的数据的时候,首先想到的肯定就是使用数组了,但是数组有着很大的限制,它并不能够根据使用者的需求来灵活的调整大小 ,尽管C99提供了变长数组的功能,但当他一旦确定了大小,后续的使用中同样也不可以改变

而柔性数组就可以完美地解决这个问题,下面我们一起学习一下

9.1 柔性数组的定义

C99中,结构中的最后一个元素允许是未知大小的数组,称为柔性数组成员

例如:

c 复制代码
struct Sarr
{
	int i;
	int a[0];
	//或者int a[];
};

9.2 柔性数组的特点

柔性数组有以下特点:

  • 结构中的柔性数组成员前必须有一个或多个其他成员
  • 柔性数组成员的大小是未知的
  • sizeof 返回的这类结构的大小不包括柔性数组的内存

例如:

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

struct Sarr
{
	int i;
	int a[0];
	//或者int a[];
};

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

运行结果:

9.3 柔性数组的使用

我们可以通过 malloc() 函数对柔性数组成员的结构进行动态内存分配,其中分配的内存应该大于结构的大小,以适应柔性数组的预期大小

例如:

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

struct Sarr
{
	int i;
	int a[0];
};

int main()
{
	//柔性数组成员申请内存
	struct Sarr* p = (struct Sarr*)malloc(sizeof(struct Sarr) + 20 * sizeof(int));
	int i = 0;
	//柔性数组的使用
	p->i = 20;
	for (i = 0; i < 20; i++)
	{
		p->a[i] = i;
	}
	for (i = 0; i < 20; i++)
	{
		printf("%d ", p->a[i]);
	}
	
	//释放动态内存
	free(p);
	p = NULL;
	return 0;
}

运行结果:

9.4 柔性数组的优点

上述代码也能写成这样:

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

struct Sarr
{
	int i;
	int* p_a;
};

int main()
{
	//给变量p开辟结构体大小的空间
	struct Sarr* p = (struct Sarr*)malloc(sizeof(struct Sarr));
	//指定数组大小
	p->i = 20;
	//给数组开辟空间
	p->p_a = (struct Sarr*)malloc(p->i * sizeof(int));
	int i = 0;
	//使用数组
	for (i = 0; i < 20; i++)
	{
		p->p_a[i] = i;
	}
	for (i = 0; i < 20; i++)
	{
		printf("%d ", p->p_a[i]);
	}
	//释放动态内存
	free(p->p_a);
	p->p_a = NULL;
	free(p);
	p = NULL;
	return 0;
}

运行结果:

上述代码同样实现了柔性数组的功能,但使用柔性数组有两个好处:

  • 方便内存释放:在代码2中,我们首先对结构的内存进行了分配,然后再对结构中的成员进行了内存分配,这样当我们使用完毕后释放内存时,就需要释放两次内存;而使用柔性数组就需要释放一次,一步到位!
  • 访问速度快:连续的内存有益于提高访问速度,也有益于减少内存碎片(多块使用中的内存之间的部分)


相关推荐
Ws_11 分钟前
leetcode LCR 068 搜索插入位置
数据结构·python·算法·leetcode
灼华十一13 分钟前
数据结构-布隆过滤器和可逆布隆过滤器
数据结构·算法·golang
谈谈叭43 分钟前
Javascript中的深浅拷贝以及实现方法
开发语言·javascript·ecmascript
lx学习44 分钟前
Python学习26天
开发语言·python·学习
大今野2 小时前
python习题练习
开发语言·python
爱编程的鱼2 小时前
javascript用来干嘛的?赋予网站灵魂的语言
开发语言·javascript·ecmascript
捕鲸叉3 小时前
C++设计模式和编程框架两种设计元素的比较与相互关系
开发语言·c++·设计模式
未知陨落3 小时前
数据结构——二叉搜索树
开发语言·数据结构·c++·二叉搜索树
大波V53 小时前
设计模式-参考的雷丰阳老师直播课
java·开发语言·设计模式
无敌最俊朗@4 小时前
unity3d————接口基础知识点
开发语言·c#