【C初阶】动态内存管理

目录

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

[2. malloc和free](#2. malloc和free)

[2.1 malloc](#2.1 malloc)

[2.2 free](#2.2 free)

[3. calloc和realloc](#3. calloc和realloc)

[3.1 calloc](#3.1 calloc)

[3.2 realloc](#3.2 realloc)

[4. 常见的动态内存的错误](#4. 常见的动态内存的错误)

[4.1 对NULL的解引用操作](#4.1 对NULL的解引用操作)

[4.2 对动态开辟内存空间的越界访问](#4.2 对动态开辟内存空间的越界访问)

[4.3 对非动态开辟内存使用free释放](#4.3 对非动态开辟内存使用free释放)

[4.4 使用free释放动态开辟内存的一部分](#4.4 使用free释放动态开辟内存的一部分)

[4.5 对同一块动态内存多次释放](#4.5 对同一块动态内存多次释放)

[4.6 (仅使用不释放)内存泄露](#4.6 (仅使用不释放)内存泄露)

[5. 动态内存笔试题分析](#5. 动态内存笔试题分析)

[5.1 练习1](#5.1 练习1)

[5.2 练习2](#5.2 练习2)

[5.3 练习3](#5.3 练习3)

[6. 柔性数组](#6. 柔性数组)

[6.1 柔性数组的特点](#6.1 柔性数组的特点)

[6.2 柔性数组的使用](#6.2 柔性数组的使用)

[6.3 柔性数组的优势](#6.3 柔性数组的优势)

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

我们已经掌握的内存开辟方式有:创建变量,创建数组申请空间。

复制代码
// 申请空间
int main()
{
	int a = 0; // 向内存申请4个字节的空间
	int arr[10]; // 向内存申请40个字节的连续空间
	return 0;
}

但是上面开辟空间的方式有两个特点:

  • 开辟的空间大小是固定的
  • 数组在声明的时候,必须指定数组的长度,数组空间一旦确定就不能调整

但是在生活中,我们对空间的需求不仅仅是固定的,还有一些情况,我们需要的空间大小需要在程序运行的时候才知道,而这些情况,固定的空间大小已经不能满足需求了。因此:

C语言引入了动态内存开辟,让程序员自己可以申请空间和释放空间,就比较灵活了。

2. mallocfree

2.1 malloc

C语言提供了一个动态内存开辟的函数:

代码块

复制代码
void* malloc (size_t size);功能:向内存的堆区申请一块连续可用的空间,并返回指向这块空间的起始地址。
  • 如果开辟成功,则返回这块空间的起始地址。

  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。

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

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

    // malloc 代码演示
    #include <stdlib.h>
    int main()
    {
    // 申请20个字节的空间,用来存放5个整数
    int* p = (int*)malloc(20);
    // 判断空间是否申请成功
    if (p == NULL)
    {
    printf("申请失败");
    }
    // 申请成功

    复制代码
      // 使用空间
      for (int i = 0; i < 5; i++)
      {
      	*(p + i) = 1 + i;
      }
      for (int i = 0; i < 5; i++)
      {
      	printf("%d ", p[i]);
      }
      return 0;

    }

2.2 free

C语言提供了另一个函数free,专门用来做动态内存的释放和回收的。

代码块

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

free函数是专门用来释放动态开辟的内存。

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

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

接上面的代码:

复制代码
free(p);
p = NULL;

3. callocrealloc

3.1 calloc

calloc函数也是向内存申请空间的,但是它会将初始化为0。

代码块

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

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

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

复制代码
#include <stdio.h>
#include <stdlib.h>
// calloc 代码演示
int main()
{
	int* p = calloc(10, sizeof(int));
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	// 使用空间
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}

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

3.2 realloc

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

有时候我们会发现过去申请的空间太小了又或是太大了,为了合理的使用内存,我们会对内存的大小进行调整,realloc函数就可以做到对动态开辟内存的调整。

代码块

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

参数:

  • ptr是要调整的内存地址
  • size调整之后的新大小,单位:字节

返回值:

  • 返回值是调整之后的内存的起始位置

注意事项:

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

代码演示

复制代码
int main()
{
	int* p = malloc(5 * sizeof(int));
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	// 使用空间
	for (int i = 0; i < 5; i++)
	{
		printf("%d ", p[i]);
	}
	// 空间突然不够用,需要存放6~10
	int * p2 = realloc(p, 40);
	if (p2 == NULL)
	{
		perror("realloc");
		free(p);
		p = NULL;
		return 1;
	}
	p = p2;
	// 使用新的空间
	for (int i = 5; i < 10; i++)
	{
		p[i] = i + 1;
	}

	// 释放空间
	free(p);
	p = NULL;
	return 0;
}

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

4.1 对NULL的解引用操作

复制代码
// 1. 对NULL的解引用操作
int main()
{
	int* p = (int*)malloc(INT_MAX);
	/*if (p == NULL)
	{
		return 1;
	}*/
	for (int i = 0; i < 10; i++)
	{
		p[i] = i + 1;
	}
	free(p);
	p = NULL;

	return 0;
}

我们正确的代码是在使用空间前进行判断p是否为NULL,如果我们不判断,malloc函数申请空间也不是100%成功的,万一造成空指针的解引用,不就发生错误了。

一般情况下,比较猴急的小伙伴会犯这样的错误。

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

复制代码
// 2.对动态开辟内存空间的越界访问
int main()
{
	int* p = (int*)malloc(5*sizeof(int));
	if (p == NULL)
	{
		return 1;
	}
	for (int i = 0; i < 10; i++)  // 越界访问
	{
		p[i] = i + 1;
	}
	free(p);
	p = NULL;

	return 0;
}

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

复制代码
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	// 使用
	// ...
	free(p);
	p = NULL;
	return 0;
}

free只能释放动态开辟内存空间。

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

复制代码
int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	for (int i = 0; i < 5; i++)
	{
		*p = i + 1;
		p++;
	}

	free(p);
	p = NULL;

	return 0;
}

这段代码是运行不了的。

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

复制代码
void* test()
{
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}

	// 使用
	free(p);
}
int main()
{
	int *p = test();

	free(p);
	p = NULL;
}

释放了多次,系统会崩溃。

4.6 (仅使用不释放)内存泄露

复制代码
void* test()
{
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
    //使用
}
int main()
{
	test();
    //
    //
	
}

5. 动态内存笔试题分析

5.1 练习1

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

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

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

这段代码打印了什么?

这段程序崩溃了,什么也打印不出。

原因:1.解引用空指针。str传递给GetMemory函数的时候,采用的是传值调用,形参p其实是str的一份拷贝,当我们把malloc申请的空间的起始地址存放在p中时,不会修改str,str依然为NULL。

2.没有free释放空间存在内存泄漏。

正确写法:

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

void Test(void)
{
	char* str = NULL;
	GetMefmory(&str);
	strcpy(str, "hello world");  // 把hello world 拷贝到 str中
	printf(str);

	free(str);
	str = NULL;
}

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

5.2 练习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;
} 

这段代码会不会打印hello world呢?

不会,会打印出一堆随机值。为什么呢?我们来画图分析。

正确写法:

复制代码
char* GetMemory(void)
{
	static char p[] = "hello world";
	return p;
}

void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}

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

5.3 练习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);
}

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

只有一个错误:没有释放,内存泄露。

6. 柔性数组

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

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

6.1 柔性数组的特点

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

例如:

6.2 柔性数组的使用

复制代码
struct S
{
	int n;
	int arr[]; // 柔性数组成员
};

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(int));
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	// 使用空间
	ps->n = 100;   // 给n赋值100
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->arr[i] = 1 + i;
	}

	// 调整空间
	
	struct S* tem = realloc(ps, sizeof(struct S) + 20 * sizeof(int));
	if (tem == NULL)
	{
		perror("realloc");
		return 1;
	}

	// 使用空间
	// ...

	free(ps);
	ps = NULL;

	return 0;
}

6.3 柔性数组的优势

上面我们写了柔性数组的使用,这种情况还有一种写法。

复制代码
struct S
{
	int n;
	int* arr;
};

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	// 使用空间
	ps->n = 100;   // 给n赋值100
	int* ptr = (int*)malloc(10 * sizeof(int));
	if (ptr == NULL)
	{
		perror("malloc");
		return 1;
	}
	ps->arr = ptr;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->arr[i] = i + 1;
	}

	// 调整空间


	// 使用空间
	// ...
	free(ptr);
	free(ps);
	ps = NULL;

	return 0;
}

这段代码和上面代码比较一下。

柔性数组更加方便且不容易出错,下面这段代码free了两次,容易发生空间泄露的问题。同时柔性数组减少了内存碎片,更加安全。

相关推荐
姜太小白38 分钟前
【VSCode】VSCode为Java C/S项目添加图形用户界面
java·c语言·vscode
工藤新一¹20 小时前
C/C++ 数据结构 —— 树(2)
c语言·数据结构·c++·二叉树··c/c++
kyle~1 天前
C/C++---浮点数与整形的转换,为什么使用sqrt函数时,要给参数加上一个极小的小数(如1e-6)
c语言·开发语言·c++
用户6120414922131 天前
C语言做的排队叫号系统
c语言·后端·敏捷开发
JasmineX-11 天前
STM32的Sg90舵机
c语言·stm32·单片机·嵌入式硬件
XH华1 天前
C语言第十一章内存在数据中的存储
c语言·开发语言
3壹2 天前
单链表:数据结构中的高效指针艺术
c语言·开发语言·数据结构
knd_max2 天前
C语言:内存函数
c语言
YLCHUP2 天前
【联通分量】题解:P13823 「Diligent-OI R2 C」所谓伊人_连通分量_最短路_01bfs_图论_C++算法竞赛
c语言·数据结构·c++·算法·图论·广度优先·图搜索算法