带你彻头彻尾了解『动态内存管理』

文章目录

  • 一、为什么要动态内存分配
  • 二、动态内存函数的介绍
    • 1、free
      • (1)free函数声明
      • (2)为什么需要释放动态开辟的内存?
    • 2、malloc
      • (1)malloc函数声明
      • (2)malloc函数使用
    • 3、calloc
      • (1)calloc函数的声明
      • (2)calloc函数的使用
    • 4、realloc
      • (1)realloc函数声明
      • (2)realloc函数的使用
  • 三、常见的动态内存错误
    • 1、对NULL指针的解引用操作
    • 2、对动态开辟空间的越界访问
    • 3、对非动态开辟内存使用free释放
    • 4、使用free释放一块动态开辟内存的一部分
    • 5、对同一块动态内存多次释放
    • 6、动态开辟内存忘记释放(内存泄漏)
  • 四、两道经典的笔试题
    • 1、题目1
    • 2、题目2
  • 五、C/C++程序的内存开辟
  • 六、柔性数组
    • 1、什么是柔性数组?
    • 2、柔性数组的特点
    • 3、柔性数组的使用
    • 4、柔性数组的优势
  • 总结

一、为什么要动态内存分配

我们已经掌握的内存开辟方式有:

int num = 20;//在栈空间上开辟四个字节
int arr[10] = { 0 };//在栈空间上开辟40个字节的连续空间

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

  1. 空间开辟大小是固定的。
  2. 数组在声明的时候,必须指定数组的长度,即大小是固定的,一旦创建就不可更改。它所需要的内存在编译时分配。

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,并且可能需要随时对所需空间进行调整。那数组的编译时开辟空间的方式就不能满足了。这时候就只能试试动态存开辟了。

二、动态内存函数的介绍

C语言提供了几个动态内存开辟的函数:malloc、calloc、realloc和free,这些内存函数都声明在stdlib.h头文件中。下面分别介绍:

1、free

(1)free函数声明

C语言在提供动态内存开辟函数的同时提供了另外一个函数free,专门是用来做动态内存的释放和回收的,free需要配合内存开辟函数malloc、calloc,realloc使用,可以说,有动态内存开辟就有free。下面
我们首先介绍free函数,函数原型如下:

注释:

void free (void* ptr);
  1. free函数用来释放动态开辟的内存。
  2. 如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。
  3. 如果参数ptr是NULL指针,则函数什么事都不做。

(2)为什么需要释放动态开辟的内存?

首先对于局部变量来说,局部变量的内存是开辟在栈区上的,即进入作用域创建,出作用域自动销毁。而动态开辟的内存是在堆区上的,在堆区上申请的内存是不会自动销毁的,程序员需要自己销毁或等程序运行结束,操作系统自动回收,如果申请了不使用也不销毁就会造成内存泄漏。所以为了避免程序产生问题,当我们不再使用动态开辟的内存时需要及时地释放。

2、malloc

(1)malloc函数声明

void* malloc (size_t size);

注释:

  1. 这个函数向内存申请一块连续可用的空间,并返回指向这块空间的头指针。
  2. 如果开辟成功,则返回一个指向开辟好空间的头指针。
  3. 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  4. 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己
    来决定。
  5. 如果参数size为0,malloc的行为是标准是未定义的,取决于编译器。

(2)malloc函数使用

#include
#include
int main()
{
	//动态内存申请
	int* p = (int*)malloc(40);//动态开辟40个字节的空间,10个整形
	//判断是否开辟成功
	if (p == NULL)
	{
		perror("");//打印错误信息
		return 1;//失败返回
	}
	//使用
	//……
	//释放
	free(p);
	//释放之后内存将还给操作系统
	//但是此时指针p中还存储着申请空间的地址,
	//如果此时继续使用p就会造成非法访问,所以
	//需要将p指针置空。令p=NULL
	
	//置空
	p = NULL;
	return 0;
}

带你彻头彻尾了解『动态内存管理』_第1张图片

3、calloc

(1)calloc函数的声明

void* calloc (size_t num, size_t size);

注释:

  1. 函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0
  2. 与函数 malloc 的区别在于二者函数参数不同,另外 calloc 会在返回首地址之前把申请的空间的每个字节初始化为全0。其它规则完全相同。

(2)calloc函数的使用

#include 
#include 
int main()
{
	//开辟动态内存
	//这里同样是开辟40个字节空间
	int* p = (int*)calloc(10, sizeof(int));
	//判断开辟是否成功
	if (NULL == p)
	{
		perror("");//显示错误信息
		return 1;//失败返回
	}
	//使用
	//......

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

带你彻头彻尾了解『动态内存管理』_第2张图片

4、realloc

realloc函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。那realloc 函数就可以做到对动态开辟内存大小的调整。

(1)realloc函数声明

void* realloc (void* ptr, size_t size);

注释:

  1. ptr 是要调整的内存地址,size 调整之后新大小,返回值为调整之后的内存的起始位置地址。
  2. realloc的其他使用规则同malloc
  3. 当传入参数为空指针时,其效果和malloc相同。如:realloc(NULL,40)==malloc(40)。

realloc在调整内存空间的是存在两种情况:

情况1:原有空间之后有足够大的空间

当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。

带你彻头彻尾了解『动态内存管理』_第3张图片
带你彻头彻尾了解『动态内存管理』_第4张图片

情况2:原有空间之后没有足够大的空间

当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。

带你彻头彻尾了解『动态内存管理』_第5张图片

带你彻头彻尾了解『动态内存管理』_第6张图片

(2)realloc函数的使用

#include
#include
int main()
{
	//动态开辟内存
	int* p = (int*)malloc(40);

	//判断是否开辟成功
	if (p == NULL)
	{
		perror("");//打印错误信息
		return 1;//失败返回
	}

	//调整空间
	int* ptr = (int*)realloc(p,80);
	//注意:不能写成p=(int*)realloc(p,80);
	//因为realloc调整失败会返回空指针,
	//将导致原有空间丢失。
	
	//判断是否调整成功
	if (ptr == NULL)
	{
		perror("");//打印错误信息
		return 1;//失败返回
	}
	//成功
	p = ptr;//令p指向调整后空间
	ptr = NULL;//将临时ptr置空

	//使用……
	
	free(p);//释放
	p = NULL;//置空

	return 0;
}

三、常见的动态内存错误

1、对NULL指针的解引用操作

void test()
{
	int* p = (int*)malloc(LLONG_MAX);
	*p = 20;//如果p为NULL?
	free(p);
	p = NULL;
}

注释:如果开辟空间失败p为空指针,再对空指针进行操作就会出现问题。所以动态开辟内存一定要对返回值进行检查。

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

void test()
{
    int i = 0;
    //申请10个整形空间
    int* p = (int*)malloc(10 * sizeof(int));
    if (NULL == p)
    {
        perror("");
        return 1;
    }
    for (i = 0; i <= 10; i++)
    {
        *(p + i) = i;//当i是10的时候越界访问
    }
    free(p);
    p = NULL;
}

注释:当越界访问动态开辟的空间时会导致程序出现问题,所以一定要合理使用动态开辟的内存空间,避免越界访问。

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

void test()
{
	int a = 10;
	int* p = &a;
	free(p);//这样可以吗?
	p = NULL;
}

注释:free只能释放动态开辟的内存。

4、使用free释放一块动态开辟内存的一部分

void test()
{
	int* p = (int*)malloc(100);
	if (p == NULL)
	{
		perror("");
		return 1;
	}
	p++;
	free(p);//p不再指向动态内存的起始位置
	p = NULL;
}

注释:free只能从动态开辟内存的起始位置释放内存。

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

void test()
{
	int* p = (int*)malloc(100);
	if (p == NULL)
	{
		perror("");
		return 1;
	}
	free(p);
	free(p);//重复释放
	p = NULL;
}

注释:一块动态内存只能释放一次。

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

void test()
{
	int* p = (int*)malloc(100);
	if (p == NULL)
	{
		perror("");
		return 1;
	}
	*p = 20;
	//使用后不释放
}

注释:忘记释放不再使用的动态开辟的空间会造成内存泄漏。所以切记动态开辟的空间一定要释放,并且正确释放 。

四、两道经典的笔试题

1、题目1

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

错误:
1.未对p的返回值进行检查。
2.传值调用,对形参开辟的空间在实参中不起作用。str仍为空,使用strcpy会发生访问冲突。
3.最后未对动态开辟的内存进行释放和及时置空。

更正:

void GetMemory(char** p) 
{
	*p = (char*)malloc(100);
	//判断
	if (*p == NULL)
	{
		return;
	}
}
void Test(void) 
{
	char* str = NULL;
	GetMemory(&str);//传值调用
	strcpy(str, "hello world");
	printf(str);
	//释放
	free(str);
	str = NULL;
}
int main()
{
	Test();
	return 0;
}

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

错误:
返回局部变量的地址p,局部变量p的空间在栈区开辟,GetMemory函数调用完毕空间自动释放,str接收一个销毁空间的地址,为野指针。

更正:
使用局部变量,切忌返回局部变量的地址。

五、C/C++程序的内存开辟

带你彻头彻尾了解『动态内存管理』_第7张图片

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

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

六、柔性数组

1、什么是柔性数组?

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

struct S
{
	int n;
	float s;
	int arr[];//[柔性]数组成员
};

有的编译器不支持上面这种写法,可以写成如下这种:

struct S
{
	int n;
	float s;
	int arr[0];//[柔性]数组成员
};

2、柔性数组的特点

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

#include
struct S
{
	int n;
	float s;
	int arr[];//[柔性]数组成员
};
int main()
{
	printf("该结构体大小为:%d\n",sizeof(struct S));
	return 0;
}

3、柔性数组的使用

struct S
{
	int n;
	float s;
	int arr[];//[柔性]数组成员
};
int main()
{
	//统一使用malloc将内存开辟到堆区上
	struct S* ps = (struct S*)malloc(sizeof(struct S)+sizeof(int)*4);
	if (ps == NULL)
	{
		return 1;
	}

	//使用
	ps->n = 100;
	ps->s = 5.5f;
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		printf("请输入:");
		scanf("%d", &(ps->arr[i]));
	}

	printf("ps->n:%d ps->s:%f\n", ps->n, ps->s);
	printf("柔性数组成员:\n");
	for (i = 0; i < 4; i++)
	{
		printf("%d ", ps->arr[i]);
	}

	//调整
	struct S*ptr = (struct S*)realloc(ps, sizeof(struct S)+10*sizeof(int));
	if (ptr == NULL)
	{
		return 1;
	}
	else
	{
		ps = ptr;
	}

	//使用……
	
	//释放
	free(ps);
	ps = NULL;

	return 0;
}

带你彻头彻尾了解『动态内存管理』_第8张图片

上面的柔性数组成员arr,调整前相当于获得了4个整型元素的连续空间,调整后相当于获得了10个整形元素的连续空间。

4、柔性数组的优势

模拟实现柔性数组成员

//模拟实现柔型数组
struct S
{
	int n;
	float s;
	int* arr;
};
int main()
{
	//为了将空间全部开辟到堆上,使用malloc动态开辟
	//而不使用局部变量将其开辟到栈区上。
	struct S*ps = (struct S*)malloc(sizeof(struct S));
	if (ps == NULL)
		return 1;

	ps->n = 100;
	ps->s = 5.5f;
	
	int* ptr = (int*)malloc(4 * sizeof(int));
	if (ptr == NULL)
	{
		return 1;
	}
	else
	{
		ps->arr = ptr;
	}
	//使用……

	//调整
	realloc(ps->arr, 10*sizeof(int));

	//释放
	free(ps->arr);
	ps->arr = NULL;
	free(ps);
	ps = NULL;

	return 0;
}

虽然模拟实现柔性数组的功能,但是相对于模拟实现的代码,柔性数组有以下两个优势:(以下观点来自于——左耳朵耗子【陈皓】 )

第一个好处是:方便内存释放

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

第二个好处是:这样有利于访问速度

连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正 你跑不了要用做偏移量的加法来寻址)

总结

本章重点在于对内存开辟函数的理解和运用,通过分析常见的动态内存错误来进一步巩固和加强对动态内存的理解,同时详细介绍了柔性数组。希望通过阅读本文能够对您有所收获。
写在最后:Keep coding!

你可能感兴趣的:(『C语言』初阶+进阶,c语言,c++,动态内存,内存开辟,经验分享)