C语言的动态内存管理

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

因为通过普通创建变量的方式开辟空间,空间太死板了,空间开辟过大会造成浪费,空间开辟过小会导致溢出,固定的内存大小好像总是不够灵活。

C语言给了我们一种方法,可以根据我们的需求开辟空间,给我们C语言使用者更多的灵活性。

2.动态内存函数的介绍

我们可以把可以我们可以调用的内存区分为三种区域

栈区:用来存放局部变量、函数形式参数(会被销毁的一些量);

堆区:用来动态内存分配(堆区上空间的维护) ,使用堆区上的空间需要malloc calloc realloc free函数。

静态区:用来存放静态变量、全局变量。

2.0 malloc和free
#include <stdlib.h>
void* malloc(size_t size);

在内存的堆区上向内存申请一个大小为size个字节连续空间

如果开辟成功返回一个储存这个空间首地址的指针,类型是void*

如果开辟失败,则返回一个NULL指针,因此对malloc函数必须做返回的检测

如果参数size为0,malloc的行为是标准未定义的,取决于编译器。

但是返回void*我们不能直接使用,因此还需要强制类型转换。

int* p=(int*)malloc(40);
if(p==NULL)
{
    return -1
}
//走到这里了 开辟成功了
free(p);//把p所指的空间还给操作系统
//free不会让p值发生变化,通过p还是可以找到这个空间,很危险
//这样的话p成了一个野指针
//因此还要把p变成空指针
p=NULL;
//如果p指向的空间不是动态开辟的,那么free的行为是未定义的
//如果p是空指针,则free函数什么也不做。
#include <stdlib.h>
int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		return -1;
	}
	for (int i = 0; i < 10; i++)
	{
		p[i] = i;
	}
	free(p);
	p = NULL;
	return 0;
}
2.1 calloc

malloc和calloc都是用来动态内存分配的。

malloc函数只负责开辟在堆区申请空间并且返回起始地址,不初始化内存空间。

calloc函数在堆区上申请空间,并且初始化为0,返回起始地址

void* calloc(size_t num,size_t size);

num表示需要开辟的元素个数,size表示每个元素的大小(单位字节)。

想初始化 用calloc 不想初始化 用malloc

2.2 realloc
void* realloc(void* pastp,size_t size);

传入需要调整大小的内存空间的首地址,传入需要调整成的大小

调整空间时会遇到两种情况

如果原空间后面的连续空间足够,就会把后面的空间也给p 返回源空间的首地址;

如果原空间后面的连续空间不够,就会再找一块空闲空间,足够大的空间,然后把原空间中的数据都粘过去,然后会free掉原本的连续空间,最后返回新空间的首地址。

realloc如果找不到对应内存空间大小,就会返回NULL。

p=realloc(p,20*sizeof(int));
//不能这么写,如果调整空间失败了 realloc返回NULL 然后把p变成了NULL
//那块空间的地址也丢了= =
//最好有个中间指针来承接一下 然后判断一下是否为空再决定是否给p
2.4 返回描述错误的字符串的函数 strerror
char* strerror(int errnum);

参数:errnum错误号,通常是errno

返回值:返回一个指向错误字符串的指针,该错误字符串描述了错误errno
需要头文件<string.h> <errno.h>

实例:

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

int main ()
{
   FILE *fp;

   fp = fopen("file.txt","r");
   if( fp == NULL ) 
   {
      printf("Error: %s\n", strerror(errno));
   }
   
  return(0);
   //输出:Error: No such file or directory
//errno是头文件<errno.h>定义的一个全局变量 strerror函数会把出现错误导致全局变量变化为的值赋值到一个字符串上。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
    int* p=(int*)malloc(4000000000000000);
    if(p==NULL)
    {
        printf("%s\n",strerror(errno));
        return -1;
    }
    else
    {
        for(int i=0;i<100;i++)
        {
            p[i]=i;
            printf("%d\t",p[i]);
        }
    }
    free(p);
    p=NULL;
    return 0;
}
3.常见的动态内存错误
  • 开辟空间失败直接解引用空指针导致的错误。

    • 解决方法:if(p==NULL){return -1;}
  • 对动态开辟空间的越界访问

    • 解决方法:注意空间大小单位是字节数。
  • 对非动态开辟的内存用free。

    • 你这样做会把程序挂掉的
  • 使用free释放一块动态开辟内存的一部分

    • 经常出现在p跑到的不是首地址的地方。
  • 对同一块动态内存多次释放

    • 解决方法free完就把指针变量赋NULL
  • 动态开辟内存忘记释放(内存泄漏)

    • 在堆区申请的空间,有两种回收的方式
    • 1.主动free;
    • 2.程序退出的时候,申请的空间也会回收(如果我们运行的程序一直运行呢?比如服务器程序,空间不释放,开辟了空间用完了不还回去,导致内存逐渐不够了,这种情况我们叫做内存泄漏)。
4.经典例题
4.0 例1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}

运行一下程序会崩溃

本题的关键在于**用指针接受一个指针变量,其实相当于值传递,**字符指针str先初始化为NULL,然后运行GetMemory函数,传了一个str这个指针给p,是值传递,p是str的临时拷贝,当malloc开辟的空间起始地址赋给局部变量p时,不会影响str,str依然为NULL指针,当str是NULL指针时,strcpy想把"hello world"拷贝拷贝到str指向的空间时程序崩溃,因为NULL指针指向的空间是不能访问的。

这里还存在内存泄露,我们没有主动freemalloc开辟的空间,存在内存泄露的风险,并且问题严重的是离开Getmemory函数后,p销毁了,这块空间的起始地址我们就找不到了,这块空间都来不及回收了。

我们传指针能够通过指针修改变量值,是传你需要修改的变量的指针,然后通过*运算通过地址找到这个变量修改这个值,这里就应该传str的地址,也就是一个二级指针。

//修改方法一:既然要改str的值,就传它的指针喽。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void GetMemory(char** p)
{
	*p = (char*)malloc(100);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str);
	strcpy(str, "hello world");
	printf(str);
    free(str);
    str=NULL;
}
int main()
{
	Test();
	return 0;
}

通过返回值接收,不过这样设计看起来很蠢,其实不需要传参了都。

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

pps:

printf(str)printf("hello world");//传的是h的地址
//printf接收的是字符串的指针,所以我们可以直接传一个字符指针给printf去打印
4.1 例2:
char *GetMemory(void)
{
 char p[] = "hello world";
//改为static p 这个就是静态本地变量了 离开空间不会被销毁 就ok了
//局部变量是在栈区创建的 具有作用域
//离开这个作用域这块空间会销毁 这块空间不属于当前程序了 使用权限不属于当前程序了
//当这块空间不属于程序的时候,鬼知道里头现在里头还有什么
//有没有被别人用,完全可能打印出来的结果是随机的
//这种情况叫非法访问这个空间
 return p;
}
void Test(void)
{
 char *str = NULL;
 str = GetMemory();//这里的str就相当于一个野指针
 printf(str);
}
//程序运行打印的是随机值
//为什么呢?

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=NULL;

这类问题统一称为返回栈空间地址的问题

野指针—未被初始化的指针。

悬空指针:指向的空间使用权限已经被收回。

个人理解:所谓野指针会报错的原因就是指向了我们不确定是否有使用权限的内存空间

4.2 例3
void Test(void)
{
 char *str = (char *) malloc(100);
 strcpy(str, "hello");
 free(str);
 if(str != NULL)
 {
 strcpy(str, "world");
 printf(str);
 }
}//str free完了没初始化 是个野指针,调用了我们已经没有使用权限的空间。
5.C/C++程序开辟方式

数据段也叫静态区。

操作系统的内存进程会细讲。

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行 结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限,所以有时候会报栈溢出。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、 返回地址等。

  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 ,分配方式类似于链表。

  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。static修饰局部变量使它从本来在栈区移动到了静态区

  4. 代码段:存放函数体(类成员函数(c++)和全局函数)的二进制代码和只读常量

  5. 内核空间:我们不能访问的一片区域。

6.柔性数组

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

typedef struct st_type//前面至少包含一个其他成员
{
 int i;
 int a[0];//柔性数组成员
}type_a;
//或
typedef struct st_type
{
 int i;
 int a[];//柔性数组成员
}type_a;
6.0 柔性数组的特点
  • 结构中的柔性数组成员前面必须至少一个其他成员

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

  • 包含柔性数组成员的结构用malloc ()函数和realloc进行内存的动态分配,并且分配的内存应该大于结构的大 小,以适应柔性数组的预期大小。

6.1 柔性数组的使用
#include <stdio.h>
#include <stdlib.h>
int main()
{
	typedef struct  {
		int i;
		int a[];
	}meta;
	meta* p = NULL;
	meta* ptr=(meta*)malloc(sizeof(meta) + 10 * sizeof(int));
	//sizeof含有柔性数组的结构体得到的字节数是本来这个结构体的大小(ps有对齐)
	if (ptr == NULL)
	{
		return -1;
	}
	p = ptr;
	p->i = 10;
	for (int i = 0; i < 10; i++)
	{
		p->a[i] = i;
		printf("%d\t", p->a[i]);
	}
	//如果内存不够了 要重新申请空间
	meta* pp = (meta*)realloc(p,sizeof(meta) + 20 * sizeof(int));
	if (pp == NULL)
	{
		printf("申请内存失败!\n");
		return -1;
	}
	p = pp;
    //销毁
    free(p);
    p=NULL;
	return 0;
}
6.2 柔性数组的优势

或许使用int*类型变量作为结构体成员也能做到?

#include <stdio.h>
#include <stdlib.h>
int main()
{
	typedef struct {
		int i;
		int* a;
	}parr;
	parr* zancun = (parr*)malloc(sizeof(parr));
	if (zancun == NULL)
	{
		printf("申请内存失败\n");
		return -1;
	}
	parr* p = zancun;
	int* zancun2 = (int*)malloc(sizeof(int) * 10);
	if (zancun2 == NULL)
	{
		printf("申请内存失败\n");
		return -1;
	}
	p->a = zancun2;
	p->i = 10;
	for (int i = 0; i < 10; i++)
	{
		p->a[i] = i;
		printf("%d\t", p->a[i]);
	}
	//扩容
	int* zancun3 = (int*)realloc(p->a,sizeof(int) * 20);
	if (zancun3 == NULL)
	{
		printf("扩容失败\n");
		return -1;
	}
	p->a = zancun3;
	printf("扩容成功!");
	//销毁
	free(p->a);
	free(p);
	p = NULL;
	zancun = NULL;
	zancun2 = NULL;
	zancun3 = NULL;

	return 0;
}

但是对比两个代码,我们不难发现一些问题

明显使用柔性数组要更加的方便,只用free一次,使用int*则需要free两次。

隐形优势是柔性数组的内存空间都是连续开辟的,连续的内存有利于提高访问速度。

并且如果我们要定义很多的柔型数组 每个都有两块空间 一块是放结构体的 一块是放这个指针指向的连续空间作为数组的,这样会导致有很多看起来其实没用的闲置空间,容易导致内存碎片的出现。

内存碎片即"碎片的内存"描述一个系统中所有不可用的空闲内存,这些碎片之所以不能被使用,是因为负责动态分配内存的分配 算法 使得这些空闲的内存无法使用,这一问题的发生,原因在于这些空闲内存以小且不连续方式出现在不同的位置。. 因此这个问题的或大或小取决于内存管理算法的实现上。.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值