动态内存管理

目录

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

2.动态内存函数的介绍

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

[2.2 malloc和free函数](#2.2 malloc和free函数)

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

[2.2.2 malloc和free函数的使用](#2.2.2 malloc和free函数的使用)

2.2.3代码解析

[2.2.4 总结](#2.2.4 总结)

[2.3 calloc函数](#2.3 calloc函数)

[2.3.1 calloc函数的使用](#2.3.1 calloc函数的使用)

[2.3.2 代码解析](#2.3.2 代码解析)

[2.3.3 malloc和calloc的区别](#2.3.3 malloc和calloc的区别)

[2.4 realloc函数](#2.4 realloc函数)

[2.4.1 realloc函数是用来干什么的?](#2.4.1 realloc函数是用来干什么的?)

[2.4.2 realloc函数的使用](#2.4.2 realloc函数的使用)

2.4.3新空间是怎么加的呢?

3.常见的动态内存错误

3.1对NULL指针解引用操作

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

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

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

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

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

4.几道经典的笔试题

[4.1 题目一](#4.1 题目一)

[4.1.1 错误解析](#4.1.1 错误解析)

[4..1.2 代码修改](#4..1.2 代码修改)

[4.1.3 一个小疑惑](#4.1.3 一个小疑惑)

[4.2 题目二](#4.2 题目二)

[4.2.1 错误解析](#4.2.1 错误解析)

[4.3 题目三](#4.3 题目三)

4.3.1错误解析

4.3.2代码修改

[4.4 题目四](#4.4 题目四)

4.4.1错误解析

4.4.2代码修改

5.柔性数组

[5.1 柔性数组的定义](#5.1 柔性数组的定义)

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

5.3柔性数组的使用

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


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

cpp 复制代码
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函数的使用

cpp 复制代码
#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代码解析

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

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

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

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

输出结果

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

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

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

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

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

cpp 复制代码
perror("malloc");
cpp 复制代码
	//赋值
	int i = 0;
	for (i = 0;i < 10;i++)
	{
		*(p + i) = i;
	}

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

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

cpp 复制代码
	//归还空间
	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函数的使用

cpp 复制代码
#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 代码解析

cpp 复制代码
	//使用开辟的空间
	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函数的使用

cpp 复制代码
#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指针解引用操作

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

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

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

cpp 复制代码
	//对动态开辟空间的越界访问
	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释放

cpp 复制代码
	int a = 10;
	int* p = &a;
	free(p);
	p = NULL;

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

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

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

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

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

cpp 复制代码
#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 题目一

cpp 复制代码
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 代码修改

cpp 复制代码
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存储的相当于字符串的首地址

cpp 复制代码
printf("hello\n");

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

cpp 复制代码
char* p="hello\n";
printf(p);

4.2 题目二

cpp 复制代码
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 代码修改

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

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

4.3 题目三

cpp 复制代码
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代码修改

cpp 复制代码
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 题目四

cpp 复制代码
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代码修改

cpp 复制代码
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中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

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

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

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

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

5.2 柔性数组的特点

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

5.3柔性数组的使用

cpp 复制代码
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就可以把所有的内存也给释放掉。

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

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

相关推荐
健康的猪几秒前
golang的cgo的一点小心得
开发语言·后端·golang
祁同伟.9 分钟前
【数据结构 · 初阶】- 堆的实现
c语言·数据结构
夜夜敲码22 分钟前
C语言教程(十六): C 语言字符串详解
c语言·开发语言
宋康28 分钟前
C语言结构体和union内存对齐
c语言·开发语言
居然是阿宋38 分钟前
Kotlin高阶函数 vs Lambda表达式:关键区别与协作关系
android·开发语言·kotlin
学习噢学个屁1 小时前
基于51单片机的超声波液位测量与控制系统
c语言·单片机·嵌入式硬件·51单片机
Cao1234567893211 小时前
简易学生成绩管理系统(C语言)
c语言·开发语言
The Future is mine1 小时前
C# new Bitmap(32043, 32043, PixelFormat.Format32bppArgb)报错:参数无效,如何将图像分块化处理?
开发语言·c#
亿坊电商1 小时前
PHP框架在微服务迁移中能发挥什么作用?
开发语言·微服务·php
烁3471 小时前
每日一题(小白)模拟娱乐篇33
java·开发语言·算法