动态内存管理

发布于:2025-05-01 ⋅ 阅读:(64) ⋅ 点赞:(0)

目录

1.为什么存在动态内存管理

2.动态内存函数的介绍

2.1动态内存函数的位置介绍

        2.2 malloc和free函数

2.2.1为什么指针类型是void*呢?

2.2.2 malloc和free函数的使用

2.2.3代码解析

2.2.4 总结

2.3 calloc函数

2.3.1 calloc函数的使用

2.3.2 代码解析

2.3.3 malloc和calloc的区别

2.4 realloc函数

2.4.1 realloc函数是用来干什么的?

2.4.2 realloc函数的使用

2.4.3新空间是怎么加的呢?

3.常见的动态内存错误

3.1对NULL指针解引用操作

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

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

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

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

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

4.几道经典的笔试题

4.1 题目一

4.1.1 错误解析

4..1.2 代码修改

4.1.3 一个小疑惑

4.2 题目二

4.2.1 错误解析

4.3 题目三

4.3.1错误解析

4.3.2代码修改

4.4 题目四

4.4.1错误解析

4.4.2代码修改

5.柔性数组

5.1 柔性数组的定义

5.2 柔性数组的特点

5.3柔性数组的使用

5.4 柔性数组的优势


1.为什么存在动态内存管理

int a=10;//固定的向内存申请4个字节的空间

int arr[10];//申请连续的空间,大小为40字节

缺点:一旦空间申请好,空间大小不会改变,我们在后续想要存储更多的内容,存储空间

会不够

那么动态内存管理的好处就来了:它可以让空间与内容适配度更高,实现利用的最大化

2.动态内存函数的介绍

2.1动态内存函数的位置介绍

我们的动态内存函数的位置是在堆区

C/C++程序内存分配的几个区域:

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

2.2 malloc和free函数

2.2.1为什么指针类型是void*呢?

解答:malloc和free函数不知道申请的空间是用来存放什么数据的,所以在后续使用时,进行强制类型转换。

2.2.2 malloc和free函数的使用

#define  _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>

int main()
{
	//开辟一个空间,大小为40个字节,用来存储10个整型数据
	int* p = (int*)malloc(40);
	//检查空间是否开辟成功
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	//malloc开辟空间,但未把空间初始化
	//赋值
	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,防止越界访问
	p = NULL;

	return 0;
}

2.2.3代码解析

	//开辟一个空间,大小为40个字节,用来存储10个整型数据
	int* p = (int*)malloc(40);

a.此时我们已经确定好开辟空间是用来存放什么数据的,我们就可以强制类型转化一下。

b.对于开辟空间的大小,不能太大,否则很容易出现开辟空间失败的情况

#define  _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>
#include<string.h>
#include<errno.h>

int main()
{
	//开辟一个空间,大小为40个字节,用来存储10个整型数据
	int* p = (int*)malloc(9999999999999);
	//检查空间是否开辟成功
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	return 0;
}

输出结果

	//检查空间是否开辟成功
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}

a.为什么我们要检查一下空间是否开辟成功呢?

解答:若空间未开辟成功,那么p就是野指针,很危险

b.检查空间是否开辟成功

        如果没有开辟成功,我需要看一下是为什么没有开辟成功,我们在这里不但可以使用strerror函数,还可以使用perror函数

perror("malloc");

	//赋值
	int i = 0;
	for (i = 0;i < 10;i++)
	{
		*(p + i) = i;
	}

为什么我们还要主动对其进行赋值呢?

解答:因为malloc函数在开辟空间时,它知到开辟空间的总大小,并不知道这个空间我们是怎么分配的,用来干什么的,那么这是就需要我们主动进行赋值

	//归还空间
	free(p);
	//将p置为NULL,防止越界访问
	p = NULL;

a.为什么我们要主动归还空间呢?

解答:就好比我们在图书馆借书,我们看完书之后一直不还,我们就占用了资源

空间也一样,如果我们不主动还,有时操作系统也不会主动回收,那么空间就会被浪费掉。

b.为什么我们还要主动将指针p置为NULL呢?

解答:我们在将空间释放掉是,指针p任指向原来的位置,这是p变成了野指针,很危险

2.2.4 总结

  • 如果开辟成功,则返回一个开辟好空间的指针
  • 如果开辟失败,则返回一个NULL指针(因此malloc函数开辟空间是一定要检查)
  • 返回值类型为void*,所以malloc函数并不知道开辟空间的类型,具体由作者自己决定
  • 如果size为0,那么malloc行为是标准未定义的行为,大小取决于编译器
  • 如果ptr指向的空间不是动态开辟的,那free函数的行为是未定义的
  • 如果ptr是NULL指针,则函数什么事都不用做

2.3 calloc函数

num:开辟元素的个数

size 每个元素的大小

2.3.1 calloc函数的使用

#define  _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>
#include<stdlib.h>

int main()
{

	//开辟一个存储10个整型数据的空间
	int* p = (int*)calloc(10, sizeof(int));
	//检查空间是否开辟成功
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	//使用开辟的空间
	int i = 0;
	for (i = 0;i < 10;i++)
	{
		printf("%d ", *(p + i));
	}
	//释放空间
	free(p);
	p = NULL;

	return 0;
}

2.3.2 代码解析

	//使用开辟的空间
	int i = 0;
	for (i = 0;i < 10;i++)
	{
		printf("%d ", *(p + i));
	}

为什么这里我们空间开辟之后就被初始化了呢?

解答:calloc函数在开辟空间时,就表明了是几个元素,是什么类型的,那么此时操作系统知到我们的意图,自动棒我们初始化了元素。

2.3.3 malloc和calloc的区别

  • malloc函数:在开辟空间时只说明里总大小,因此空间没有被初始化,直接返回起始地址
  • calloc函数:在空间时表明了元素个数和每个元素的大小,因此所有元素被初始化为0,然后返回起始地址

2.4 realloc函数

ptr: 要调整的内存地址

size:调整之后的空间大小

2.4.1 realloc函数是用来干什么的?

  • 有时我们会出现申请空间不合理的情况,如果申请空间太大会浪费掉我们的空间,如果申请空间太小,我们没办法存储完我们的数据,为了更加准确的使用空间,我们一定会对内存的大小进行灵活的调整,那么进行这个操作的函数就是 --realloc函数。
  • realloc函数的返回值是调整之后内存的起始位置
  • realloc函数在调整原内存空间大小的基础上,还会将原来内存中的数据,移动到新的空间去

2.4.2 realloc函数的使用

#define  _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>
#include<stdlib.h>

int main()
{
	//在内存中开辟空间,大小为5个整型数据的大小
	int* p = (int*)malloc(5 * sizeof(int));
	//检查空间是否开辟成功
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//使用

    
	//发现不够,需要再开辟5个整型数据大小的空间
	int* ptr = (int*)realloc(p, 10 * sizeof(int));
    
	//检查空间是否开辟成功
	if (ptr != NULL)
	{
		p = ptr;
	}
	//继续使用空间

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

2.4.3新空间是怎么加的呢?

情况一:原空间后的空间足够大,则在原空间后面追加


情况二:原空间后面的空间不足,在内存中重新找一块满足需求大小的空间,同时把原空间数据搬过来,并把原空间的数据释放掉,返回新空间起始地址

why:我们用了一个新指针,而不直接用原来的指针呢?

解答:如果我们直接用了原来的指针,那么我们存储的内容就会丢失

3.常见的动态内存错误

3.1对NULL指针解引用操作

 // 对NULL指针解引用操作
	int* p = (int*)malloc(100);
	int i = 0;
	for (i = 0;i < 10;i++)
	{
		*(p + i) = 0;
	}

这里我们没有对malloc开辟空间是否成功,如果空间开辟失败,那么p就是野指针

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

	//对动态开辟空间的越界访问
	int* p = (int*)malloc(100);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	int i = 0;
	for (i = 0;i < 100;i++)
	{
		*(p + i) = 0;
	}

这里malloc是开辟了100个字节的空间(相当于25个整型数据的空间),而不是100个整型数据的空间,而在后续的赋值中,却要给100个整型数据赋值,这就造成了越界访问

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

	int a = 10;
	int* p = &a;
	free(p);
	p = NULL;

free针对的是堆区,而p位于栈区

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

	//使用free释放一块动态开辟空间内存的一部分
	int* p = (int*)malloc(100);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	p++;
	free(p);
	p = NULL;

代码中p++之后p不再指向起始位置

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

#include <stdio.h> 
#include <stdlib.h> 
 
int main() { 
    // 分配动态内存 
    int *ptr = (int *)malloc(sizeof(int)); 
    if (ptr == NULL) { 
        printf("内存分配失败\n"); 
        return 1; 
    } 
 
    // 第一次释放内存 
    free(ptr); 
 
    // 错误:再次释放同一块内存 
    free(ptr); 
 
    return 0; 
} 

当你使用malloc()(C语言)、calloc()、realloc()或者new(C++)等函数来分配动态内存时,系统会在堆上为你分配一块指定大小的内存区域,并返回一个指向该区域起始地址的指针。当你使用free()(C语言)或者delete(C++)来释放这块内存时,系统会将这块内存标记为可用,归还给操作系统,以便后续的内存分配使用。

如果再次尝试释放同一块已经被释放的内存,就会引发问题,因为系统已经认为这块内存不再被使用,再次释放可能会破坏内存管理数据结构,导致程序崩溃或者产生不可预测的结果。

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

动态内存分配是指在程序运行时根据需要分配内存空间。在许多编程语言中,程序员需要手动管理这些动态分配的内存,即负责在不再使用时释放它们。如果忘记释放,就会造成内存泄漏。

4.几道经典的笔试题

4.1 题目一

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

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

结果:程序会因为引发异常而挂掉

4.1.1 错误解析

  • str传给指针p时,p是str创建的临时拷贝,有自己的独立空间,当GetMemory函数申请了空间后,地址放在p中时,str依然为空,当GetMemory函数返回之后,strcpy拷贝时,形成了非法访问
  • 在GetMemory内部,动态申请了内存,但没有释放,会形成内存泄漏

4..1.2 代码修改

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

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

4.1.3 一个小疑惑

为什么 printf(str); 是正确的呢?

解答:str存储的相当于字符串的首地址

printf("hello\n");

这也相当于将hello这个字符串的h传给了printf

char* p="hello\n";
printf(p);

4.2 题目二

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

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

4.2.1 错误解析

当出GetMemory函数后,空间被回收,str只记住了p空间的位置,当开始使用时却发现那块空间已经不见了,形成了非法访问内存,这也被叫做 返回栈空间地址问题

4.2.2 代码修改

char* GetMemory(void)
{
	char* p = "hello world";
	return p;
}

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

4.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);
}

4.3.1错误解析

这段代码在开辟并使用完空间后,没有释放空间,造成了内存泄漏

4.3.2代码修改

void GetMemory(char* p, int num)
{
	*p = (char*)malloc(num);
}

void Test(void)
{
	char* str = NULL;
	GetMemory(str, 100);
	strcpy(str, "hello");
	printf(str);
	free(str);
	str = NULL;
}
free(str);
str=NULL;

4.4 题目四

void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}

4.4.1错误解析

因为free(str);之后,str成为野指针,if(str != NULL)语句不起作用。

4.4.2代码修改

void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
    str=NULL;
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}

5.柔性数组

5.1 柔性数组的定义

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

typedef struct type
{    int i;    
     int a[0];//柔性数组成员
}type_a;

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

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

二选一,那个不报错就用那个

5.2 柔性数组的特点

  • 结构中的柔性数组成员前面必须至少一个其他成员。
  • sizeof 返回的这种结构大小不包括柔性数组的内存。
  • 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
typedef struct type
{    
    int i;
    int a[0];//柔性数组成员
}type_a;
    printf("%d\n", sizeof(type_a));//输出的是4

5.3柔性数组的使用

int i = 0;
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
//业务处理
p->i = 100;
for(i=0; i<100; i++)
{
    p->a[i] = i;
}free(p);

这样柔性数组成员a,相当于获得了100个整型元素的连续空间。

5.4 柔性数组的优势

优势一:方便内存释放

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

优势二:这样有利于访问速度.

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


网站公告

今日签到

点亮在社区的每一天
去签到