C语言详解(动态内存管理)2

Hi~!这里是奋斗的小羊,很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~~
💥💥个人主页:奋斗的小羊
💥💥所属专栏:C语言


🚀本系列文章为个人学习笔记,在这里撰写成文一为巩固知识,二为展示我的学习过程及理解。文笔、排版拙劣,望见谅。


目录

  • 前言
  • 1、常见动态内存错误
      • [1.1 对NULL指针的解引用操作](#1.1 对NULL指针的解引用操作)
      • [1.2 对动态内存空间的越界访问](#1.2 对动态内存空间的越界访问)
      • [1.3 对非动态开辟内存使用free释放](#1.3 对非动态开辟内存使用free释放)
      • [1.4 使用free释放动态内存的一部分](#1.4 使用free释放动态内存的一部分)
      • [1.5 对同一快动态内存多次释放](#1.5 对同一快动态内存多次释放)
      • [1.6 动态开辟内存忘记释放(内存泄漏)](#1.6 动态开辟内存忘记释放(内存泄漏))
  • 2、动态内存经典笔试题分析
      • [2.1 题目一](#2.1 题目一)
      • [2.2 题目二](#2.2 题目二)
      • [2.3 题目三](#2.3 题目三)
      • [2.4 题目四](#2.4 题目四)
  • 3、柔性数组
      • [3.1 什么是柔性数组](#3.1 什么是柔性数组)
      • [3.2 柔性数组的特点](#3.2 柔性数组的特点)
      • [3.3 柔性数组的使用](#3.3 柔性数组的使用)
      • [3.4 柔性数组的优势](#3.4 柔性数组的优势)
  • 总结

前言

总的来说,动态内存管理为我们提供了更加灵活、高效和可扩展的内存管理方式,但动态内存管理函数可能会带来一些风险,主要包括内存泄漏、内存溢出和野指针等问题,我们在使用动态内存管理函数时要多留心,避免风险的出现


1、常见动态内存错误

1.1 对NULL指针的解引用操作

如果我们写的代码不严谨,没有考虑到动态内存分配失败的可能,就会写出类似于下面的代码:

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

int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	
	//直接使用指针p
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		p[i] = i + 1;
	}
	return 0;
}

这样的代码可能并没有什么问题,但是存在很大的隐患,因为动态内存函数是有可能开辟内存空间失败的,当开辟失败时会返回NULL,而NULL指针是不能解引用的

像VS这样比较强大的编译器会立马检测到并提示你

为了避免这种错误,我们需要对指针p进行判断,再决定是否使用

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

int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	//判断p是否为空指针
	if (p == NULL)
	{
		//打印出错误信息
		perror("malloc");
		//终止程序
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		p[i] = i + 1;
	}
	return 0;
}

1.2 对动态内存空间的越界访问

我们用动态内存函数开辟多大的空间,我们就使用多大的空间,不能越界访问,例如:

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

int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	//判断p是否为空指针
	if (p == NULL)
	{
		//打印出错误信息
		perror("malloc");
		//终止程序
		return 1;
	}
	int i = 0;
	//p+1跳过1个整型,p+10就会越界
	for (i = 0; i <= 10; i++)
	{
		p[i] = i + 1;
	}
	return 0;
}

聪明的VS也会检测出错误提示你


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

free函数是用来释放由动态内存函数开辟的空间的,不能释放普通内存

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

int main()
{
	int arr[10] = { 0 };
	int* p = arr;
	free(p);
	p = NULL;
	return 0;
}

当我们运行起来后就出问题了


1.4 使用free释放动态内存的一部分

上面我们用malloc函数申请了10个整型空间,然后通过for循环给这10个整型空间内放1~10的整数,有些同学可能会为了方便这样写代码:

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

int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	//判断p是否为空指针
	if (p == NULL)
	{
		//打印出错误信息
		perror("malloc");
		//终止程序
		return 1;
	}
	//给申请的动态空间内存1~10
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*p++ = i;
	}
	//释放动态内存空间
	free(p);
	p = NULL;
	return 0;
}

当我们运行起来才发现写出了BUG

这又是为什么呢?

事实上此时free(p)中的p指针已经不再指向malloc开辟的动态内存的起始地址了,因为*p++这里对p的指向不断递增

free操作的指针必须指向要被释放的动态内存的起始地址


1.5 对同一快动态内存多次释放

当我们用完一块动态内存空间后不再使用对其释放后,可能会因为忘记而重复释放一次,并且如果第一次释放时忘记给p指针赋NULL,那么程序就会出错

c 复制代码
	//使用...
	
	//释放动态空间
	free(p);

	//...

	free(p);
	p = NULL;
	return 0;

但是如果我们两次释放时都给p指针赋了NULL,那基本不会发生什么事,相当于没有错,只是逻辑上讲不通

所以,在我们用free释放完动态内存空间后,紧跟着对指针赋NULL是很有必要的


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

动态开辟的空间一定要释放,并且正确释放

当我们写代码的时候,存在这样一种可能会出现的错误,那就是动态开辟的内存忘记释放或者因为某些原因还没有到free语句就提前终止代码,这里举个简单的例子

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

void text()
{
	int flag = 1;
	int* p = (int*)malloc(100);
	if (p == NULL)
	{
		return 1;
	}

	//使用
	
	//因为某些原因函数提前返回了
	if (flag == 1)
	{
		return;
	}

	//free函数
	free(p);
	p = NULL;
}

int main()
{
	//自定义函数
	text();

	//后面还有大量代码
	//....

	return 0;
}

虽然我们确实用了free函数释放空间,但是当代码量较大时可能会因为某些原因还没到free函数就提前终止了,而我们还没意识到,就算后面我们意识到了这个问题这块内存我们也找不到了

只有整个程序结束后这块内存才能被释放,如果程序一直不结束这块空间就再也找不到了,这就叫内存泄漏

所以,就算动态内存申请使用后用了free,也是有可能犯内存泄漏的错误,我们要多加小心

内存泄漏是比较可怕的,尤其是某些24小时不断运行的服务器程序,如果存在内存泄漏,内存被耗干也只是时间的问题


2、动态内存经典笔试题分析

2.1 题目一

请问运行下面 text函数会有什么后果?

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

void get_memory(char* p)
{
	p = (char*)malloc(100);
}

void text(void)
{
	char* str = NULL;
	get_memory(str);
	strcpy(str, "hello world");
	printf(str);
}

int main()
{
	text();
	return 0;
}

上面的代码一共有两个问题
第一个问题: malloc申请动态内存空间后没有使用free函数释放,这可能会导致内存泄漏

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

void get_memory(char* p)
{
	p = (char*)malloc(100);
}

void text(void)
{
	char* str = NULL;
	get_memory(str);
	strcpy(str, "hello world");
	printf(str);

	free(str);
	str = NULL;
}

int main()
{
	text();
	return 0;
}

第二个问题: 函数传参传值调用和传址调用使用错误

这个代码的意思是申请一块动态内存空间地址交给指针p,通过指针p再交给指针str,再使用strcpy函数将字符串拷贝到动态内存空间内,最后打印出字符串

但是get_memory函数传参的时候使用的是传值调用,所以指针p跟指针str没有关系

有两种纠错方法
方法一: 将传值调用改为传址调用,此时p为二级指针

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

void get_memory(char** p)
{
	*p = (char*)malloc(100);
}

void text(void)
{
	char* str = NULL;
	get_memory(&str);
	strcpy(str, "hello world");
	printf(str);

	free(str);
	str = NULL;
}

int main()
{
	text();
	return 0;
}

方法二: 直接返回指针p的地址,不需要传参

c 复制代码
char* get_memory()
{
	char* p = (char*)malloc(100);
	return p;
}

void text(void)
{
	char* str = NULL;
	str = get_memory();
	strcpy(str, "hello world");
	printf(str);

	free(str);
	str = NULL;
}

int main()
{
	text();
	return 0;
}

2.2 题目二

请问运行下面 text函数会有什么后果?

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

char* get_memory(void)
{
	char p[] = "hello world";
	return p;
}

void text(void)
{
	char* str = NULL;
	str = get_memory();
	printf(str);
}

int main()
{
	text();
	return 0;
}

上面的代码是一个非常经典的例子,之前在C语言(指针)3中野指针一小节介绍过类似的例子

上面代码的问题:

我们在自定义函数get_memory中创建了一个局部临时数组存入字符串"hello world",再将字符串的首地址返回用指针str接收,虽然此时指针str确实指向字符串"hello world"的首地址,但是此时str是没有权限访问这块空间的

因为在局部数组p在出了get_memory函数后就销毁了,它申请的空间会被收回,即使指针str能找到这块空间,但是它已经没有权限使用了,此时str就是一个野指针

所以我们应该避免返回栈空间地址

想要改正上面的代码也很简单,我们申请一块动态内存就行,同时也别忘了释放

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

char* get_memory(void)
{
	char* p = (char*)malloc(20);
	strcpy(p, "hello world");
	return p;
}

void text(void)
{
	char* str = NULL;
	str = get_memory();
	printf(str);
	
	free(str);
	str = NULL;
}

int main()
{
	text();
	return 0;
}

2.3 题目三

请问运行下面 text函数会有什么后果?

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

void get_memory(char** p, size_t num)
{
	*p = (char*)malloc(num);
}

void test(void)
{
	char* str = NULL;
	get_memory(&str, 100);
	strcpy(str, "hello world");
	printf(str);
}

int main()
{
	test();
	return 0;
}

上面的代码是可以打印出"hello world"的,但是遗憾的是上面的代码中使用了动态内存函数malloc,但是没有使用free函数释放动态内存空间

虽然上面的代码可以实现我们想要的效果,但这样的代码是存在安全隐患的

动态内存开辟函数malloccallocrealloc和动态内存释放函数free必须成对出现

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

void get_memory(char** p, size_t num)
{
	*p = (char*)malloc(num);
}

void test(void)
{
	char* str = NULL;
	get_memory(&str, 100);
	strcpy(str, "hello");
	printf(str);
	free(str);
	str = NULL;
}

int main()
{
	test();
	return 0;
}

2.4 题目四

请问运行下面 text函数会有什么后果?

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

使用malloc函数申请一块100个字节大小的动态内存空间放入字符串"hello",然后使用free函数释放这一动态内存空间
但是此时指针str中还存着我们开辟的动态内存空间的地址,正确的写法free函数后应紧跟str = NULL;,但是上面的代码并没有这一条语句

if语句判断的时候指针str确实是不为空指针的,进入if语句后执行strcpy(str, "world");这条代码,根据我们对strcpy函数的了解,这里还要对指针str解引用,但是指针str我们之前已经用free函数释放过了,并且没有赋NULL所以str此时是野指针不能解引用 ,运行起来程序就会出错

这道题考察的还是free函数后紧跟p = NULL的问题


3、柔性数组

3.1 什么是柔性数组

C99中,结构体中的最后一个成员允许是未知大小的数组,这就叫柔性数组成员

  • 在结构体中
  • 最后一个成员
  • 未知大小的数组
c 复制代码
struct S1
{
	int n;
	char c;
	double d;
	int arr[];//未知大小的数组
};
c 复制代码
struct S2
{
	int n;
	char c;
	double d;
	int arr[0];//未知大小的数组
};

上面两种写法中arr都是柔性数组成员

有些编译器可能只支持其中的一种写法,VS中两种写法都支持


3.2 柔性数组的特点

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

正是因为sizeof返回的这种结构大小不包含柔性数组的内存,所以结构中的柔性数组成员前面必须至少有一个其他成员,否则结构体的大小没法计算


3.3 柔性数组的使用

包含柔性数组的结构怎么使用呢?

包含柔性数组的结构创建变量不会像一般结构那样创建,而是使用malloc函数进行内存的动态分配

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

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

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));
	if (ps == NULL)
	{
		perror("malloc");
		//终止程序
		return;
	}
	//使用空间
	ps->n = 100;
	int i = 0;
	for (i = 0; i < 20; i++)
	{
		ps->arr[i] = i + 1;
	}
	
	//...
	free(ps);
	ps = NULL;
	return 0;
}

柔性数组的柔性 怎么体现呢?

因为上面包含柔性数组的结构是由malloc函数进行内存的动态分配,所以我们可以使用realloc函数进行动态内存的调整,那这个数组的大小就可大可小

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

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

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));
	if (ps == NULL)
	{
		perror("malloc");
		//终止程序
		return 1;
	}
	//使用空间
	ps->n = 100;
	int i = 0;
	for (i = 0; i < 20; i++)
	{
		ps->arr[i] = i + 1;
	}
	//调整ps指向的空间大小
	struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 40 * sizeof(int));
	//进行指针的非空判断,保护原地址
	if (ptr != NULL)
	{
		ps = ptr;
		//防止ptr变成野指针
		ptr = NULL;
	}
	else
	{
		perror("realloc");
		//终止程序
		return 1;
	}

	for (i = 0; i < 40; i++)
	{
		printf("%d ", ps->arr[i]);
	}

	//...
	free(ps);
	ps = NULL;
	return 0;
}

如果不使用柔性数组,还有一种办法能实现上面的效果

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

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

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	int* tmp = (int*)malloc(20 * sizeof(int));
	if (tmp == NULL)
	{
		perror("malloc");
		return 1;
	}
	else
	{
		ps->arr = tmp;
		tmp = NULL;
	}
	ps->n = 100;
	int i = 0;
	//给指针arr指向的20个整型空间赋值
	for (i = 0; i < 20; i++)
	{
		ps->arr[i] = i + 1;
	}

	//调整指针arr指向的空间大小
	tmp = (int*)realloc(ps->arr, 40 * sizeof(int));
	if (tmp != NULL)
	{
		ps->arr = tmp;
		tmp = NULL;
	}
	else
	{
		perror("realloc");
		return 1;
	}
	for (i = 0; i < 40; i++)
	{
		printf("%d ", ps->arr[i]);
	}

	//...
	free(ps->arr);
	ps->arr = NULL;
	free(ps);
	ps = NULL;
	return 0;
}

结构struct S中有一个指针成员,我们的想法是用malloc函数申请一块动态内存空间,再让结构中的这个指针指向这块动态分配的内存,然后这块由指针指向的动态内存空间就可以用realloc函数进行大小的调整了

可以看到这样实现的效果和柔性数组相似,那柔性数组为什么还要存在呢?

其实相比之下柔性数组还是有它的优势的


3.4 柔性数组的优势

  • 方便内存释放

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

所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存释放

  • 这样有利于访问速度

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

因为malloc等动态内存函数在申请空间时会在堆区允许的地方申请一块连续的空间,但是动态内存函数申请的多个动态内存空间之间并不是连续的,这些空间之间就形成了内存碎片


总结

  • 动态内存管理是一把双刃剑,它能给我们提供灵活的内存管理方式,但同样也会带来风险
  • 检查动态内存分配是否成功:在使用动态内存管理函数时,应该检查分配内存是否成功,以确保程序正常运行,这是比较容易忽略的点

相关推荐
转调8 分钟前
每日一练:地下城游戏
开发语言·c++·算法·leetcode
Java探秘者9 分钟前
Maven下载、安装与环境配置详解:从零开始搭建高效Java开发环境
java·开发语言·数据库·spring boot·spring cloud·maven·idea
2303_8120444618 分钟前
Bean,看到P188没看了与maven
java·开发语言
秋夫人20 分钟前
idea 同一个项目不同模块如何设置不同的jdk版本
java·开发语言·intellij-idea
不穿格子衬衫36 分钟前
常用排序算法(下)
c语言·开发语言·数据结构·算法·排序算法·八大排序
萧鼎42 分钟前
Python调试技巧:高效定位与修复问题
服务器·开发语言·python
wdxylb43 分钟前
使用C++的OpenSSL 库实现 AES 加密和解密文件
开发语言·c++·算法
aqua35357423581 小时前
蓝桥杯-财务管理
java·c语言·数据结构·算法
Geek之路1 小时前
QT系统学习篇(1)
开发语言·qt·学习
罗曼蒂克在消亡1 小时前
GraphQL规范
开发语言·graphql