动态内存管理

为什么要有动态内存分配

我们已经掌握的内存开辟⽅式有:
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
但是上述的开辟空间的⽅式有两个特点:
空间开辟⼤⼩是固定的。
数组在申明的时候,必须指定数组的⻓度,数组空间⼀旦确定了⼤⼩不能调整
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间⼤⼩在程序运⾏的时候才能知
道,那数组的编译时开辟空间的⽅式就不能满⾜了。
C语⾔引⼊了动态内存开辟,让程序员⾃⼰可以申请和释放空间,就⽐较灵活了。

c/c++中程序内存区域划分(简化版)

按以前我们学的观念,我们可以将内存区域分为栈区,堆区,静态区。(但其实其内存区域分布复杂多了,这个只是简化版,之后会讲其更细致的内存区域划分版本)

函数使用时分配的内存在栈区,局部变量分配内存也在栈区。函数参数就是在函数内部创造的变量,所以其内存就是在函数分配的内存里,也在栈区。静态函数分配也是在栈区(被static修饰,只能在本文件使用)

全局变量在静态区,静态变量也在静态区(被static修饰的变量),分为静态局部变量和静态全局变量,静态局部变量生命周期跟程序一样长,作用域不变还是只能在其括号里使用;而静态全局变量只能在本文件使用。

 堆区中存放的是动态内存,由我们自己去控制分配的。而之后要讲的四个函数malloc,free,calloc,realloc都是跟动态内存有关的函数。 (变量都是在栈区或静态区分配的,不是在堆区

 动态内存管理_第1张图片

 malloc,free,calloc,realloc

 malloc

C语⾔提供了⼀个动态内存开辟的函数:
 void* malloc (size_t size);
这个函数向内存申请⼀块连续可⽤的空间,并返回指向这块空间的指针。
如果开辟成功,则返回⼀个指向开辟好空间的指针。
如果开辟失败,则返回⼀个 NULL 指针,因此malloc的返回值⼀定要做检查。(当申请的内存过大就可能会造成开辟失败)
返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使⽤的时候使⽤者⾃
⼰来决定。自己去添加。
如果参数 size 为0,malloc的⾏为是标准是未定义的,取决于编译器。(对于当参数为0时的情况根本没有用到过,所以我们研究这种情况是毫无意义的)

malloc申请的内存在堆区中分配的,为动态内存。 

 free

C语⾔提供了另外⼀个函数free,专⻔是⽤来做动态内存的释放和回收的,函数原型如下:
 void free (void* ptr);
free函数单纯就是⽤来释放动态开辟的内存。
 如果参数 ptr 是NULL指针,则函数什么事都不做。(也不会报错)
malloc和free都声明在 stdlib.h 头⽂件中。
对于free有以下几大误区:
1.只能对动态内存(在栈区)释放,对非动态内存释放会报错

2.其中的参数ptr指向的必须为动态内存的起始位置,不能指向动态内存的中间位置,否则会报错

3.别对同一块内存多次释放

4.动态内存开辟后一定要释放,否则会发生内存泄露的问题。

5.对于ptr作用于free后,动态内存被释放,但ptr的值依然不会变(变为野指针),所以因为他变为了野指针,我们此时应该及时将其变为空指针(NULL,NULL被使用需要加头文件stdio.h)

还要额外说一些点。

1.对于被释放的内存我们不能再访问了,再访问属于非法访问系统报错。只能访问被申请的内存。(数组越界访问属于这种);

2.当一个内存被释放后,其存的值并不会被改变,只是其不能再访问了。

对于一个动态内存要被释放掉只有两种方式

1.用free函数释放

2.程序结束 动态内存就自动被释放掉

这是因为动态内存的作用域和生命周期都是一整个程序。所以分配后的动态内存其能用在程序的任何一个地方,并且只有当程序结束时才会被释放。 

所以这很容易造成内存泄露问题,导致内存积累,程序运行过慢,解决方法就是当我们用完开辟后的动态内存后就及时释放掉以免造成这种问题

内存泄漏是指程序中已动态分配的堆区内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。  

calloc

C语⾔还提供了⼀个函数叫 calloc calloc 函数也⽤来动态内存分配。原型如下:
void* calloc (size_t num, size_t size);
函数的功能是为 num 个⼤⼩为 size 的元素开辟⼀块空间(相当于开辟num*size的大小空间),并且把空间的每个字节初始化为0。
与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全
0。而malloc并不会对里面的区域初始化,所以malloc在开辟完后空间内的值都是随机值。

calloc开辟内存失败同样会返回NULL。 (内存不够就会开辟失败)

realloc

realloc函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太⼩了,有时候我们⼜会觉得申请的空间过⼤了,那为了合理的时候内存,我们⼀定会对内存的⼤⼩做灵活的调整。那 realloc 函数就可以做到对动态开辟内存⼤
⼩的调整。
函数原型如下:
void* realloc (void* ptr, size_t size);

 ptr 为要调整的动态内存地址(所以在实施realloc之前我们还需要开辟一块内存空间)

size 为调整之后内存空间新⼤⼩(可以变大可以变小)

 返回值为调整之后的内存起始位置。
当空间开辟失败后,会返回NULL指针(没有能够开辟的空间)
当开辟成功后
如果你是要将其空间变大,分为两种:
在原有空间之后没有足够大的空间(该空间后面也存在开辟空间,其中间隔不够大),就会在堆区中再去找一个合适大小的连续空间,并将其原来内存中的数据拷贝到新的空间(扩大中多的空间依旧还是随机值),之前的旧空间则会释放。返回值则是新空间的首位地址
如果后面有足够大空间,就较简单了,就原有内存之后直接追加空间,原来空间的数据不发⽣变化(原本的开辟空间并不会改变,追加的空间并不会被初始化), 返回值则为原空间首位地址

 

如果想将其空间变小,那就更简单,开辟的空间会缩小,里面的多余空间则会被释放。(原本的开辟空间并不会改变)

 由于上述的将空间变大时的两种不同情况,realloc函数的使⽤就要注意⼀些。


#include 
#include 
int main()
{
 int *ptr = (int*)malloc(100);
 if(ptr != NULL)
 {
 //业务处理
 }
 else
 {
 return 1; 
 }
 //扩展容量
 
 //代码1 - 直接将realloc的返回值放到ptr中
 ptr = (int*)realloc(ptr, 1000);//这样可以吗?(如果申请失败会如何?)
 //失败的话指向原空间的地址也会变NULL,我们就找不到原空间,它会变为一个隐患,所以代码1不行
 

//代码2 - 先将realloc函数的返回值放在p中,不为NULL,在放ptr中
 int*p = NULL;
 p = realloc(ptr, 1000);
 if(p != NULL)
{
 ptr = p;
//业务处理
 }

 free(ptr);
 return 0;

对于realloc还规定,当其为realloc(NULL,数字)时相当于malloc(数字),其也可以开辟内存空间跟malloc一模一样(同样并不会初始化其内存的值) 

常见的动态内存的错误

 对NULL指针的解引用操作

void test()
 {
 int *p = (int *)malloc(INT_MAX/4);
 *p = 20;//如果p的值是NULL,就会有问题
 free(p);
 }

 因为malloc开辟失败的话会返回NULL,这时对其解引用会发生系统错误,所以我们需对其进行条件的区分。如下

int main()
{
	int* p = (int*)malloc( 1000000000000000000);
	if (p != NULL)
		*p = 20;//如果p的值是NULL,就会有问题
	else
		perror("malloc");
	free(p);
}

 这样才是严谨的,防止系统发生错误。

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

void test()
 {
 int i = 0;
 int *p = (int *)malloc(10*sizeof(int));
 if(NULL == p)
 {
 exit(EXIT_FAILURE);
 }
 for(i=0; i<=10; i++)
 {
 *(p+i) = i;//当i是10的时候越界访问
 }
 free(p);
 }

当i=10时访问了未开辟的空间,从而非法访问未开辟的内存,造成系统错误。

 对⾮动态开辟内存使⽤free释放

这个在讲free函数时也讲过 ,其不能释放非动态开辟内存,否则系统发生错误

void test()
{
 int a = 10;
 int *p = &a;
 free(p);//ok?
 }

动态内存管理_第2张图片

使⽤free释放⼀块动态开辟内存的⼀部分

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

之前在讲free时就讲过这个点,free参数中的指针必须为动态内存的起始位置,不能在动态内存其他位置上否则会导致系统发生错误。

 动态内存管理_第3张图片

 对同一块内存多次释放

void test()
 {
 int *p = (int *)malloc(100);
 free(p);
 free(p);//重复释放
 }

在讲free时我们就讲过对同一块内存不能多次释放,否则系统会崩溃

动态内存管理_第4张图片

解决方法就是把该指针在执行完后变为NULL,这样就算你手误再次执行该操作,因为之前讲过free(NULL)时什么都不会发生,系统也不会崩溃,所以这样就能防止发生错误系统崩溃

动态内存管理_第5张图片

 动态开辟的内存忘记释放(内存泄露)

 之前在讲free时就讲过内存泄露问题,在使用完该动态内存之后一定要释放,否则会造成内存泄露问题。

现在再次说下。

void test()
 {
 int *p = (int *)malloc(100);
 if(NULL != p)
 {
 *p = 20;
 }
 }
int main()
 {
 test();
 while(1);
 }
忘记释放不再使⽤的动态开辟的空间会造成内存泄漏。切记:动态开辟的空间⼀定要释放,并且正确释放。

动态内存经典笔试题分析  

 题目一

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

这题错哪了,,大家可能觉得printf(str)是错的 ,但其实它是没问题的,这是一种特殊形式。

错的是其他地方。 

printf的特殊形式

当printf(“%s”,地址)时其实也可以变为printf(地址),其可以达到一样的效果。注意如果要用printf(地址)形式写则该地址必须是char*或者const char*形式。  (注意我们只要知道这种情况就行,在代码中看到有人这样用就能知道这代码是干什么的,不至于都不清楚,一般我们自己都不会用这种形式的(少炫点技))

 错的原因

1.注意其传递是值传递,所以str在getmemory后依旧是NULL,从而导致strcpy时发生错误,代码错误。

2.此外还有个小问题,malloc开辟的内存没释放从而导致内存泄露 

所以我们应该修改为如下代码才能达到想要的效果 

  char* GetMemory(void)
{
   char *	p = (char*)malloc(100);
	return p;
}
  int main()
  {
	  char* str = NULL;
	  str = GetMemory();
	  strcpy(str, "hello world");
	  printf(str);
	  free(str);
	  str = NULL;
  }

动态内存管理_第6张图片

题目二 

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

动态内存管理_第7张图片

为什么出现这种状况,究其原因是str变成了野指针。

在getmemory中创建了数组p,而后使用完该函数后就销毁了该函数开辟的栈帧(空间)。使str接受的地址变为未开辟的空间,该指针变为野指针。从而在后续用printf函数时其开辟的空间肯定会与getmemory之前开辟的空间有重叠,其printf函数在使用时可能就会重置到数组p所在的空间,其中的值就会被改变,从而打印str时出现上述这种情况。

如果出现这种相似的情况但其结果依然是正常打印出想要的结果,那你完全是运气好,其新函数开辟的空间刚好没在这地址上面或者其新函数在这地址上面开辟但是其要重置的部分刚好不在这地址上,从而就没被修改,打印出正常的结果。这种纯看运气,下次修改下代码可能就会导致打印出的结果发生错误,所以切记不要使用野指针(要及时发现野指针)。

如果要修改就是用static修饰数组,使其在静态区开辟空间,其空间生命周期就跟程序一样长,只有程序结束其才被销毁。 此时它就能实现应有的效果。

动态内存管理_第8张图片

 题目三

void GetMemory(char **p, int num)
 {
 *p = (char *)malloc(num);
 }
void Test(void)
 {
 char *str = NULL;
 GetMemory(&str, 100);
 strcpy(str, "hello");
 printf(str);
 }
唯一的问题就是没释放动态内存,造成内存泄露,很简单,就不解释了

题目四 

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

该题跟第二题很像,都是将str变为野指针,只不过该函数最终能打印出正常结果world,然而它打印出来不要以为代码正确,这只是运气好,printf函数开辟空间后重置的地方没影响到打印正常结果。即使打印出正常结果,代码依然是错误的。

柔性数组 

柔性数组的认识 

 也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。

在C99 中,结构体中的最后⼀个元素允许是未知⼤⼩的数组,它叫做『柔性数组』成员。
​
​
struct cat
{
 int  i;
int  arr[0];//结构体中的柔性数组成员
}

​

​

 有些编译器会报错无法编译所以我们也可以改成

​
struct cat
{
 int  i;
int  arr[];//结构体中的柔性数组成员
}

​

所以这就是柔性数组成员的结构体的声明

 柔性数组的特点

 结构中的柔性数组成员前⾯必须⾄少有⼀个其他成员。
 sizeof 返回的这种结构⼤⼩不包括柔性数组的内存。(在sizeof参与运算时就假设没有柔性数组从而得出结果)
typedef struct st_type
{
 int i;
 int a[0];//柔性数组成员
}type_a;
int main()
{
 printf("%d\n", sizeof(type_a));//输出的是4
 return 0;
}
 包含着柔性数组成员的结构体我们是在堆区分配内存的,并且分配的内存应该⼤于结构体的⼤
⼩(sizeof算出的),以适应柔性数组的预期⼤⼩。
之所以不在栈区或静态区分配内存给该结构体,因为其创建好后的结构体内存大小是可以变化的,为动态,而在栈区或静态区创建好后的分配的内存都是固定的,所以它绝对不能在栈区或静态区中被分配内存比如创建一个该类型的结构体变量,其变量储存在栈区或静态区,它们内存分配都是固定的,接受不了这样一个动态的结构体,所以不能用含有柔性数组的结构体创建变量)。 (变量不会储存在堆区里)
而堆区内存是动态的,由我们自己控制,所以对于该类型结构体我们都是在堆区分配内存(用malloc,calloc,realloc等函数)。

 柔性数组的使用

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

int main()
{
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);
 return 0;
}

 这就是柔性数组的使用,如上图,含柔性数组的结构体的就是在堆区中分配的。(用了malloc函数)

柔性数组的优势

//代码2
#include 
#include 
typedef struct st_type
{
 int i;
 int *p_a;
}type_a;
int main()
{
 type_a *p = (type_a *)malloc(sizeof(type_a));
 p->i = 100;
 p->p_a = (int *)malloc(p->i*sizeof(int));
 
 //业务处理
 for(i=0; i<100; i++)
 {
 p->p_a[i] = i;
 }
 
 //释放空间
 free(p->p_a);
 p->p_a = NULL;
 free(p);
p = NULL;
 return 0;
}

 像该代码不用柔性数组同样可以实现相同的效果。

但是用柔性数组有两个好处:

第⼀个好处是:⽅便内存释放
如果我们的代码是在⼀个给别⼈⽤的函数中,你在⾥⾯像代码二一样做了⼆次内存分配,并把整个结构体返回给⽤⼾。⽤⼾调⽤free可以释放结构体,但是⽤⼾并不知道这个结构体内的成员也需要free,所以你不能指望⽤⼾来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返回给⽤⼾⼀个结构体指针,⽤⼾做⼀次free就可以把所有的内存也给释放掉。否则如果用户忘了只用了一次free,就会发生内存泄露。
第⼆个好处是:这样有利于访问速度.
连续的内存有益于提⾼访问速度,也有益于减少内存碎⽚。(第一个代码(含有柔性数组)是直接一整个数据,而第二个代码有两个数据,其中会有内存碎片,空间利用率变低,访问速度也就变得相对第一个更慢)(其实,我个⼈觉得也没多⾼了,反正你跑不了要⽤做偏移量的加法来寻址)

扩展阅读 

扩展阅读:这个文章很好,推荐看下,让你对其有更清楚的认知:

C语言结构体里的成员数组和指针 | 酷 壳 - CoolShell

 总结c/c++中程序内存区域划分(没简化的版本)

 之前讲的内存区域划分为了便于我们理解,都是简化后的版本。没简化的内存区域划分比这复杂多了,现在我们讲讲没简化的内存区域划分。(它们属于c/c++中的内存区域划分)

 动态内存管理_第9张图片

内存分为如上六大空间:

内核空间相当于我们的禁区,用户代码不能读写在其上面,我们写代码时是用不到内核空间的(它是给系统操作系统自己用的,)

栈区我们之前就讲过了,这里就不讲了,它的内存分配是由高地址到低地址分配的。

内存映射段我们现在学的太少了,就先不讲了。 

堆区我们之前也讲过了,这里也不讲了,它的内存分配是由低地址到高地址分配的。

数据段就是静态区,我们也讲清楚了。 

代码段存放的是可执行代码和只读常量。我们写的代码经过编译和链接最终形成二进制指令,这些二进制指令就是我们可执行代码,它们总要存放吧,于是就存放在代码段。 而只读常量就比如我们的常量字符串(“adsds”)和常量数字(如40),它们也存放在代码段中,这些只读常量只能被读取使用,不能被修改。

 所以这就是c/c++的内存区域划分。 

总结 

所以现在我们就把动态内存管理这一篇章给讲完了,下篇文章我将给大家介绍文件操作这一篇章。

感谢大家的支持❤️(*/ω\*)!!!

你可能感兴趣的:(c语言知识点专栏,算法)