动态内存分配及动态顺序表的实现

✅作者简介:嵌入式入坑者,与大家一起加油,希望文章能够帮助各位!!!!
个人主页:@rivencode的个人主页
系列专栏:玩转C语言
推荐一款模拟面试、刷题神器,从基础到大厂面试题点击跳转刷题网站进行注册学习

目录

  • 一.为什么要有动态内存分配
  • 二.动态内存分配函数(重点)
    • 1.malloc函数(memory allocation)
    • 2.free函数
      • 为什么要释放动态内存
    • 3.calloc函数
    • 4.realloc函数(重点)
  • 三.动态内存分配迷之操作
    • 1.对NULL指针的解引用操作
    • 2.对动态开辟空间的越界访问
    • 3.对非动态开辟内存使用free释放
    • 4.使用free释放一块动态开辟内存的一部分
    • 5.对同一块动态内存多次释放
    • 6.动态开辟内存忘记释放(内存泄漏)
  • 四.经典笔试题
  • 五.揉性数组
  • 六.动态顺序表

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

我们先看传统数组有哪些缺点

1.数组的长度必须事先给定且只能是常整数,不能是变量
动态内存分配及动态顺序表的实现_第1张图片
大部分编译器还是不支持数组的长度是可变的。

不过linux系统下的gcc编译器还是支持的
动态内存分配及动态顺序表的实现_第2张图片
2. 数组的长度一旦定义,其长度就不能在更改,数组的长度不能在程序运行起来的过程中动态的扩充或缩小。

有人会说了怎么不能改,我去修改数组里面的常数,不也可以想要多少就改多少嘛,举个例子你就明白了就比如说你做了一个存储联系人信息的通讯录用一个数组来存,程序运行起来之后,增加几个联系人,但你为了不浪费内存增加一个你就去停下程序去修改源码嘛,显然这是扯淡。

其实第二点就是第一点,因为数组的元素参数必须是整形常量,所以当程序运行起来数组的长度只能是固定的,也就是说内存不是浪费了,就是内存不够用,而动态内存就可以随时分配内存。

3.A函数中定义的数组(局部数组),在A函数运行期间可以被其他函数使用,但A运行完毕之后,A函数中的数组将无法被其他函数使用,虽然全局数组可以做到这一点但是全局的变量尽量少用。

总结:动态内存的分配的出现可以解决以上问题,主要是可以实现内存的动态扩充和缩小。

动态内存的位于内存的堆区,由我们自己分配,和回收空间。
动态内存分配及动态顺序表的实现_第3张图片

二.动态内存分配函数(重点)

1.malloc函数(memory allocation)

malloc函数全称为memory allocation 意思为动态内存分配。
动态内存分配及动态顺序表的实现_第4张图片
直接一查这个函数给你解释的明明白白的
动态内存分配及动态顺序表的实现_第5张图片
总结:

参数:
size:开辟动态内存的大小单位为字节
如果参数 size 为0,malloc返回值可能是空指针,也可能不是,取决于编译器。
返回值:
1.如果开辟成功,则返回一个指向开辟好空间的指针。
2.如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。

3.开辟出来的内存若未初始化,则内存中的值是随机值。

malloc分配内存
动态内存分配及动态顺序表的实现_第6张图片
返回值记得做检查
动态内存分配及动态顺序表的实现_第7张图片
如果内存不够,开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查,不然得话后面解引用空指针会出问题。

打印一个数组:
动态内存分配及动态顺序表的实现_第8张图片

2.free函数

free–释放动态内存分配的空间。
动态内存分配及动态顺序表的实现_第9张图片
一目了然
动态内存分配及动态顺序表的实现_第10张图片
总结:

参数:

ptr:指向先前使用malloc、calloc或realloc分配的内存块的指针
1.如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
2.如果参数 ptr 是NULL指针,则函数不执行任何操作

注意:free只会释放ptr指向的动态内存,而不会改变ptr本身的值,就是说ptr仍然指向原来的位置(虽然是无效的)

动态分配的内存使用完后必须要释放,不然一个程序中你一直开辟动态内存而不回收会导致内存大量浪费。

为什么要释放动态内存

其实程序结束时操作系统或自动帮你回收动态分配的空间,既然是怎样我们为什么还要我们手动释放呢。

原因一:当程序很大时,如果你一直开辟空间而不回收,内存会一直被占用,程序结束前不能被释放。
原因二:如果程序一直不结束呢,这就是我们所谓的内存泄漏,内存一直被占用,有没有这种情况呢,比如说你电脑上的电脑管家是不是一直运行着保护你的电脑,如果电脑管家的程序代码存在内存泄漏,则会拖慢你电脑的运行速度。

释放分配的动态内存
动态内存分配及动态顺序表的实现_第11张图片
注意:free只会释放p指向的动态内存,而不会改变p指针本身的值,就是说p仍然指向原来的位置(虽然是无效的)

此时p指向一块无效的内存,如果解引用就是非法访问内存,p就变成的野指针,所有在释放内存之后要将p置为NULL.

3.calloc函数

calloc——分配动态内存空间,并初始化为零
动态内存分配及动态顺序表的实现_第12张图片
简简单单
动态内存分配及动态顺序表的实现_第13张图片
总结:

参数:
num:要分配的元素个数
size:每个元素的大小
1.函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0
2.与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
其他特点与malloc函数一样,参考malloc函数即可

使用方法:
动态内存分配及动态顺序表的实现_第14张图片

4.realloc函数(重点)

realloc——重新分配动态内存空间大小(可大可小)

动态内存分配及动态顺序表的实现_第15张图片
简简单单
动态内存分配及动态顺序表的实现_第16张图片
动态内存分配及动态顺序表的实现_第17张图片

总结:

参数:
ptr: 是要调整的内存地址
size: 调整之后大小
返回值为调整之后的内存起始位置。
1.如果返回一个新的地址,这个函数调整原内存空间大小的基础上,还会将原来内存中的数据拷贝到新 的空间
2.realloc调整失败,将返回一个空指针,并且参数ptr所指向的内存块不会被释放(它仍然有效,其内容不变)。
3.如果新内存大小更大,则新分配的部分的值是不确定的
4.如果ptr是一个空指针,realloc等价于malloc,分配一个大小为字节的新块,并返回一个指向其开头的指针

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

  • 情况1:原有空间之后有足够大的空间,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化,返回的地址与原地址(ptr)相同。
    动态内存分配及动态顺序表的实现_第18张图片

  • 情况2:原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址,且将原来内存中的数据拷贝到新的空间,且原来的旧空间被释放,但新分配的那一部分还是未初始化的,值不确定
    动态内存分配及动态顺序表的实现_第19张图片
    使用realloc重新分配空间时一定要先拿一个临时指针来接收返回值,不能直接用原来空间的指针来接收,若realloc 分配空间失败会返回一个NULL直接用原来空间的指针来接收会导致原来的空间直接找不到啦。
    动态内存分配及动态顺序表的实现_第20张图片
    如果ptr是一个空指针,realloc等价于malloc,分配一个大小为字节的新块,并返回一个指向其开头的指针

动态内存分配及动态顺序表的实现_第21张图片

三.动态内存分配迷之操作

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

动态内存分配及动态顺序表的实现_第22张图片
所以实验malloc函数最好要检查是否开辟内存成功

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

动态内存分配及动态顺序表的实现_第23张图片

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

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

动态内存分配及动态顺序表的实现_第24张图片

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

动态内存分配及动态顺序表的实现_第25张图片
动态内存分配及动态顺序表的实现_第26张图片

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

动态内存分配及动态顺序表的实现_第27张图片
当然我觉得没有人紧挨着释放两次,但是就怕你释放之后不记得又释放一次,最后的解决办法就是释放之后将指针置为NULL指针,free(NULL),不会有任何问题。

动态内存分配及动态顺序表的实现_第28张图片
这样即防止了p变成野指针,也可以防止对同一块动态内存重复释放

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

动态内存分配及动态顺序表的实现_第29张图片
图中可以看到程序运行后,疯狂吃电脑内存。

四.经典笔试题

  • 代码1

思考以下代码有什么问题**
动态内存分配及动态顺序表的实现_第30张图片

动态内存分配及动态顺序表的实现_第31张图片
改正:
思路一:传地址

动态内存分配及动态顺序表的实现_第32张图片
思路二:返回给str接收到的动态分配的地址
动态内存分配及动态顺序表的实现_第33张图片
两种方式记得后面要释放动态内存

  • 代码2:
    动态内存分配及动态顺序表的实现_第34张图片

动态内存分配及动态顺序表的实现_第35张图片
动态内存分配及动态顺序表的实现_第36张图片

  • 代码3
    动态内存分配及动态顺序表的实现_第37张图片

动态内存分配及动态顺序表的实现_第38张图片

五.揉性数组

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

动态内存分配及动态顺序表的实现_第39张图片
柔性数组的特点:

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

sizeof 返回的这种结构大小不包括柔性数组的内存

动态内存分配及动态顺序表的实现_第40张图片
柔性数组开辟空间
动态内存分配及动态顺序表的实现_第41张图片
使用

//柔性数组
int main()
{
	printf("%d\n",sizeof(struct S));
	struct S * tmp;
	int i=0;
    struct S *p=(struct S*)malloc(sizeof(struct S)+5*sizeof(int));
   if (p==NULL)
   {
	   return 0;
   }
    for(i=0;i<5;i++)
	{
		p->arr[i]=i;
	}
	tmp=(struct S*)realloc(p,sizeof(struct S)+10*sizeof(int));
	if (tmp !=NULL) 
	{
	     p=tmp;
		 tmp=NULL;
	}
	for(i=5;i<10;i++)
	{
		p->arr[i]=i;	
	}
	for(i=0;i<10;i++)
	{
		printf("%d ",p->arr[i]);
	}
	free(p);
    p=NULL;
	return 0;
}

其实柔性数组完全可以用一个结构体指针去指向一块动态内存开辟的空间代替,也是后面我们实现顺序表的方法

 int main()
 {
	 int i=0;
	 int *tmp;
    struct S s;
	struct S *ps=&s;
   ps->arr=(int *)malloc(5*sizeof(int));
   if (ps->arr ==NULL)
   {
	   return 0;
   }
   for(i=0;i<5;i++)
   {
	   ps->arr[i]=i;
   }
   tmp=(int *)realloc(ps->arr,10*sizeof(int));
	if (tmp !=NULL)
	{
		ps->arr=tmp;
		tmp=NULL;
	}
   for(i=5;i<10;i++)
	{
		ps->arr[i]=i;
	}
   for(i=0;i<10;i++)
   {
	   printf("%d ",ps->arr[i]);
   }
   free(ps->arr);
   ps->arr=NULL;
   return 0;
 }

动态内存分配及动态顺序表的实现_第42张图片
其实动态顺序表你把上面这个搞懂了那还不是简简单单。

六.动态顺序表

如果你认真看完了本文章
《结构体详解》
《指针从入门到熟练掌握》
又看完了上面两篇,动态顺序表还搞不定,直接来打我不解释

顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。

SquList.h

#ifndef __SEQLIST_H
#define __SEQLIST_H

#include 
#include
typedef  int   SQDataType;

typedef struct SeqList
{
	SQDataType *arr;//动态顺序表
       int  size;//当前表中元素个数
	   int capacity;//当前表容量
}SeqList;


//初始化顺序表
void SeqListInit(SeqList *ps);
//打印顺序表
void SeqListPrint(SeqList *ps);
//清除顺序表
void SeqListClear(SeqList *ps);

//尾插
void SeqListPushBack(SeqList *ps,SQDataType x);
//头插
void SeqListPushFront(SeqList *ps,SQDataType x);
//尾删
void SeqListPopBack(SeqList *ps);
//头删
void SeqListPopFront(SeqList *ps);
//任意位置插入一个元素
void SeqListInsert(SeqList *ps,int pos,SQDataType x);
//任意位置删除一个元素
void SeqListErase(SeqList *ps,int pos);

#endif /* __SEQLIST_H */

SquList.c

#include "SeqList.h"


//初始化顺序表
void SeqListInit(SeqList *ps)
{
	ps->arr=(SQDataType *)malloc(10*sizeof(SQDataType));
	ps->capacity=10;//初始化表容量为10元素
	ps->size=0;//当前表中有多少个元素
}

//打印顺序表
void SeqListPrint(SeqList *ps)
{
	int i=0;
	for(i=0;i<ps->size;i++)
	{
		printf("%d ",ps->arr[i]);
	}
}

//清除顺序表
void SeqListClear(SeqList *ps)
{
	ps->size=0;
}

//检测容量
static void CheckCapacity(SeqList *ps)
{
  if (ps->size == ps->capacity)
   {
      SQDataType *tmp;
	  //容量不够利用realloc函数,扩容至原来的两倍
	  tmp=(SQDataType *)realloc(ps->arr,2*ps->capacity*sizeof(SQDataType));
	  if (tmp != NULL)
	  {
		  ps->arr=tmp;
	  }
	  ps->capacity=2*ps->capacity;	//容量为原来的两倍
   }
}

//尾插
void SeqListPushBack(SeqList *ps,SQDataType x)
{
	//添加元素前都要检测容量
   CheckCapacity(ps);
   ps->arr[ps->size]=x;
   ps->size++;
}

//头插
void SeqListPushFront(SeqList *ps,SQDataType x)
{
	int i=0;
	CheckCapacity(ps);
	for(i=ps->size-1;i>=0;i--)
	{
		ps->arr[i+1]=ps->arr[i];
	}
	ps->arr[0]=x;
	ps->size++;
}

//尾删
void SeqListPopBack(SeqList *ps)
{
	if(ps->size<= 0)
	{
		return;
	}
	ps->size--;
}
//头删
void SeqListPopFront(SeqList *ps)
{
	int i=0;
   if(ps->size<= 0)
	{
		return;
	}
   for(i=0;i<ps->size-1;i++)
   {
	   ps->arr[i]=ps->arr[i+1];
   }
   ps->size--;
}

//任意位置插入一个元素
void SeqListInsert(SeqList *ps,int pos,SQDataType x)
{
	int i=0;
    CheckCapacity(ps);
	if(pos<0 || pos>ps->size)
	{
		return;
	}
  for(i=ps->size;i>pos;i--)
  {
	  ps->arr[i]=ps->arr[i-1];
  }
  ps->arr[pos]=x;
  ps->size++;
}

//任意位置删除一个元素
void SeqListErase(SeqList *ps,int pos)
{
	int i=0;
	if (pos<0 || ps->size > ps->size-1)
	{
		return;
	}
	for(i=pos;i<ps->size-1;i++)
	{
		ps->arr[i]=ps->arr[i+1];
	}
	ps->size--;
}

我这里讲一下任意位置插入一个元素,其他的函数实现都差不多。
动态内存分配及动态顺序表的实现_第43张图片

你可能感兴趣的:(玩转C语言,c语言,数据结构,算法)