动态内存管理(动态内存函数)

1. 为什么存在动态内存分配

内存的使用方式

  1. 创建变量(开辟一块独立的空间)
    • 局部变量,存在栈区。
    • 全局变量,放静态区。
  2. 创建数组(开辟一块连续的空间)
    • 局部数组-栈区。
    • 全局数组-静态区。

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

  1. 空间开辟的大小是固定的
  2. 数组在定义的时候,必须指定数组的长度,它所需要的内存在编译时分配。

假设我有一个50个坑位的数组int arr[50] = {0};,如果只用存30个东西进去,剩下20个位置明显就浪费掉了,如果有60则不够存。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了,这时候就只能试试动态内存开辟了,不是所有编译器都支持用变量改变数组元素个数
动态内存分配的好处就是要一块空间就去开辟一块空间,不会造成浪费或坑位不够

2. 动态内存函数

引用头文件<stdlib.h>

2.1 malloc

内存开辟函数

void* malloc (size_t size);

申请一块连续可用的存储空间,并返回一个指向这个空间的指针

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

举个栗子:
向内存申请10个整型的空间

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
        int* p = (int*)malloc(10*sizeof(int));//返回的是void*,要强转

        if(NULL == p)//如果开辟空间失败
        {
                //打印错误原因
                printf("%s\n",strerror(errno));
        }
        else
        {
                //可以正常使用空间
                int i;
                for(i = 0;i < 10;i++)
                {
                        *(p + i) = i;
                        printf("%d ",*(p + i));
                }
                putchar('\n');
        }
        return 0;
}
0 1 2 3 4 5 6 7 8 9 

开辟失败的情况

int* p = (int*)malloc(1000000000*sizeof(int));
if(NULL == p)//开辟空间失败
{
		printf("%s\n",strerror(errno));
}

程序执行结果Cannot allocate memory

2.2 free

释放动态开辟的空间

void free (void* ptr);

有借有还,再借不难,free函数是用来释放动态开辟的内存
malloc申请的空间不再使用的时候,就应该free给操作系统

  • 要释放哪块空间,就把哪块空间的地址交给free就完事了。
  • 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
  • 如果参数 ptr 是NULL指针,则函数什么事都不做。
  • 在释放内存空间后,所指向的指针要致空,不再使用它。

把p指向的空间还回去
在这里插入图片描述
代码如果还要继续写,但是这块空间不再使用时,自己借的空间应该自己主动free掉,而不是等着return 0帮你还。
注意:此时只是把p指向的那块空间还了回去,但是p还记得之前手里拿的地址,"不法分子"利用p还是能找到那块空间,所以主动点,动动发财的小手p = NULL

2.3 calloc

开辟一块空间,并将元素初始化成0。

void* calloc (size_t num, size_t size);

  • void* calloc (元素个数, 每个元素的大小);
  • 为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
  • 与函数 malloc 的唯一区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
  • 开辟成功,返回地址,开辟失败,返回NULL

举个栗子:
开辟一个40个字节的空间

malloc(10 * sizeof(int));
calloc(10 , sizeof(int));

calloc开辟空间的同时全部初始化为0

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

int main()
{

        int* p = (int*)calloc(10,sizeof(int));
        if(NULL == p)
        {
                printf("%s",strerror(errno));
        }
        else
        {
                int i;
                for(i = 0;i < 10;i++)
                {
                        printf("%d ",*(p + i));
                }
                putchar('\n');
        }

        free(p);//用完才释放
        p = NULL;

        return 0;
}
0 0 0 0 0 0 0 0 0 0 

和mallloc的区别也就只有malloc不初始化,calloc全部初始化为0而已。

2.4 realloc

调整动态开辟内存空间的大小
有时候觉得开辟的空间不够了,要增加一些,有时候又多了要减少一点,这时候就要用到realloc函数来调整开辟的空间。

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

  • ptr 是要调整的内存地址
  • size 调整之后新大小
  • 返回值为调整之后的内存起始位置。
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 的空间。
  • 如果追加空间失败,会返回NULL。

举个栗子

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

int main()
{
		int i;
	
		int* p = (int*)malloc(20);
		if(NULL == p)
		{
			printf("%s\n",strerror(errno));
		}
		else
		{
			printf("调整前:");
			for(i = 0;i < 5;i++)
			{
				*(p + i) = i;
				printf("%d ",*(p + i));
			}
			putchar('\n');
		}
		int* pp = (int*)realloc(p,40);//改p指向的空间为40字节
		if(NULL != pp)//追加空间如果失败会返回NULL,如果用p来接收,会导致原来p指向的20个字节的空间也找不到了
		{	//所以先用一个新的指针变量接收,来判断是不是NULL
			p = pp;
		}
		printf("调整后:");
		for(i = 5;i < 10;i++)
		{
			*(p + i) = i;
		}
		for(i = 0;i < 10;i++)
		{
			printf("%d ",*(p + i));
		}
		putchar('\n');	
		free(p);
		p = NULL;
		return 0;
}
调整前:0 1 2 3 4 
调整后:0 1 2 3 4 5 6 7 8 9

realloc在调整内存空间时存在两种情况

  • 情况1:原有空间之后足够大的空间,扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化,返回旧地址
  • 情况2:原有空间之后没有足够大的空间,没有足够的空间时总不能去抢别人的地盘吧,realloc会重新找一个内存区域开辟一块符合要求的空间,返回新地址,拷贝旧数据,free旧空间

不管情况1还是2,用完动态开辟的内存之后都应该主动free掉
在这里插入图片描述

利用realloc开辟动态空间

void * realloc (NULL, size_t size);

举个栗子:

int* p = (int*)realloc(NULL,40);//效果等于malloc(40)

3. 常见动态内存错误

3.1 对NULL指针的解引用操作

万一开辟空间失败了,返回来个NULL,对接收NULL的指针进行解引用就会导致程序崩溃。

		int* p = (int*)malloc(40);
        //万一malloc开辟空间失败了,p就被赋值为NULL

        int i;
        for(i = 0;i < 10;i++)
        {
                //这里就会变成对NULL指针的解引用
                *(p + i) = i;
        }
        free(p);
        p = NULL

所以在使用动态开辟的空间之前,一定要先判断指针是否为NULL。

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

		int* p = (int*)malloc(5*sizeof(int));
        if(NULL == p)
        {
                printf("%s\n",strerror(errno));
        }
        else
        {
                int i;
                for(i = 0;i < 10;i++)//这块空间只有20个字节,却要使用40个字节
                {
                        *(p + i) = i;
                        printf("%d ",*(p + i));
                }
                putchar('\n');
        }

        free(p);
        p = NULL;

程序直接崩溃
在这里插入图片描述

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

		int  a = 10;
        int* pa= &a;

        free(pa);
        pa = NULL;

程序照样崩溃
在这里插入图片描述

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

int* p = (int*)malloc(40);

        if(NULL == p)
        {
                return 0;
        }
        else
        {
                int i;
                for(i = 0;i < 10;i++)
                {
                        *p++ = i;
                }
        }
        //回收空间
        free(p);
        p = NULL;

直接崩溃

7fff475e7000-7fff475e9000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
已放弃(吐核)

因为p不再指向动态内存的起始地址
在这里插入图片描述

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

		int* p = (int*)malloc(40);
        if(NULL == p)
        {
                return 0;
        }
        else
        {
                int i;
                for(i = 0;i < 10;i++)
                {
                        *(p + i) = i;
                }
        }
        free(p);
        //....代码写着写着忘记p已经释放过一次了,于是
        free(p);

程序依然崩溃

7fff475e7000-7fff475e9000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
已放弃(吐核)

解决方法:在第二次释放之前,将p转换为空指针,再释放,就没有问题了,
所以free掉一块空间之后立刻将指针置为NULL是很有必要的。

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

只借不还,不停的开辟开辟压根不带还的

		 while(1)
        {
                malloc(1);//不停的开辟一个字节的空间
        }
已杀死

疯狂的开辟内存然后被计算机自动干掉了

4. 小试牛刀

4.1 题目1

运行Test后会发生什么?

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

1. 运行代码时会出现崩溃的情况。
2. 程序存在内存泄露。

在这里插入图片描述
改正方法1

void GetMemory(char **p)
{
        *p = (char *)malloc(100);
}

void Test(void)
{
        char *str = NULL;

        GetMemory(&str);
        strcpy(str, "hello world");
        printf("%s\n",str);
        free(str);
        str = NULL;
}
hello world

改正方法2
让p这个临时变量,在临死之前把它手里记住的地址交给str

char* GetMemory(char *p)
{
        p = (char *)malloc(100);
        return p;
}

void Test(void)
{
        char *str = NULL;

        str = GetMemory(str);
        strcpy(str, "hello world");
        printf("%s\n",str);
        free(str);
        str = NULL;
}

别忘了在开辟空间之后,str还是有可能为NULL,记得判断

4.2 题目2

问题出在哪?

#include <stdio.h>
char *GetMemory(void)
{
        char p[] = "hello world";
        return p;
}
void Test(void)
{
        char *str = NULL;
        str = GetMemory();
        printf("%s\n",str);
}
int main()
{
        Test();
        return 0;
}

非法访问内存,输出了个随机值

警告:函数返回局部变量的地址 [-Wreturn-local-addr]
  return p;
  ^
 ��P�

因为数组p是局部变量,放在栈区,出了p所在的函数,p的内容会被销毁,这块空间还回去了,再拿着p的地址找回去,找到的就不知道是啥了。栈空间上的地址不要随便返回

解决方法1:
加个static,延长局部变量的生命周期,把p放到静态区,让p出了函数之后不被销毁。

static char p[] = "hello world";

解决方法2
让hello world变成一个常量字符串,放在只读数据区

 char* p = "hello world";

4.3 题目3

Test运行之后发生什么?

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

可以正常打印,但是str用完之后没有主动free,存在内存泄露

4.4 题目4

运行Test后会发生什么?

void Test(void)
{
        char *str = (char *) malloc(100);
        strcpy(str, "hello");
        free(str);//free释放str指向的空间之后,并不会把str置为NULL
        if(str != NULL)//那么这个判断就没有意义了
        {
                strcpy(str, "world");//还将world拷到原来的那块空间属于非法访问
                printf(str);
        }
}

打印出world

但是,存着hello的那块空间已经被free掉,这块空间不再属于str,但是str任然记得这块空间的地址,将world拷到str属于非法访问内存,(酒店退房之后,但是还记得房间号,硬要用这个房间,非法访问了)

实际考察free掉str后,str成了个野指针,并不会变成空指针,那么if里的判断条件就没意义了

void Test(void)
{
        char *str = (char *) malloc(100);
        strcpy(str, "hello");
        free(str);
        str = NULL;
        if(str != NULL)//将str置为NULL后,这个判断才有意义
        {
                strcpy(str, "world");//还将world拷到原来的那块空间属于非法访问
                printf(str);
        }
}

5. 柔性数组

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

举个栗子:

typedef struct student
{
        int a;
        int b[];//未知大小的-柔性数组成员-数组的大小是可以调整的
}stu;

编译是能通过的

有些编译器可能无法编译,可以改成

typedef struct student
{
        int a;
        int b[0];//未知大小的-柔性数组成员-数组的大小是可以调整的
}stu;

5.1 柔性数组的特点

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

举个栗子

typedef struct student
{
        int a;
        int b[0];//柔型数组成员
}stu;
int main()
{
        stu s;
        printf("s = %d\n",sizeof(s));
        return 0;
}
s = 4

可以看到,结构体的大小并没有包含柔性数组的大小.

5.2 柔性数组的使用

给柔性数组成员开辟5个整型的空间:

typedef struct student
{
        int a;
        int b[];
}stu;
int main()
{
        stu* ps = (stu*)malloc(sizeof(stu) + 5 * sizeof(int));
        return 0;
}

因为结构体大小是4字节,给结构体追加的20个字节就是给柔性数组开辟的字节。
此时总开辟空间大小就是(结构体大小+给柔性数组单独开辟的空间大小)
在这里插入图片描述
代码1

typedef struct student
{
        int a;
        int b[0];
}stu;
int main()
{
        stu* ps = (stu*)malloc(sizeof(stu) + 5 * sizeof(int));//给柔性数组开辟了5个整型的空间
        ps -> a = 100;
        int i;
        for(i = 0;i < 5;i++)
        {
                ps -> b[i] = i;//将前5个元素赋值0 1 2 3 4
        }
        stu* pa = realloc(ps,44);//将柔性数组的大小调整为10个整型
        if(NULL != pa)
        {
                ps = pa;
        }
        for(i = 5;i < 10;i++)
        {
                ps -> b[i]= i;
        }
        for(i = 0;i < 10;i++)
        {
                printf("%d ",ps -> b[i]);
        }
        printf("\na = %d\n",ps -> a);
        free(ps);
        ps = NULL;
        return 0;
}
0 1 2 3 4 5 6 7 8 9 
a = 100

5.3 柔性数组的优势

上面的代码1也可以改成这个样子

代码2


typedef struct student
{
        int a;
        int* b;
}stu;

int main()
{
        stu* ps = (stu*)malloc(sizeof(stu));
        ps -> b = malloc(5 * sizeof(int));
        ps -> a = 100;

        int i;
        for(i = 0;i < 5;i++)
        {
                ps -> b[i] = i;
                printf("%d ",ps -> b[i]);
        }
        putchar('\n');

        //调整大小
        int* ptr = realloc(ps -> b,10*sizeof(int));
        if(NULL != ptr)
        {
                ps -> b = ptr;
        }
        for(i = 5;i < 10;i++)
        {
                ps -> b[i] = i;
        }
        for(i = 0;i < 10;i++)
        {
                printf("%d ",ps -> b[i]);
        }
        printf("\na = %d\n",ps -> a);

        free(ps -> b);
        ps -> b = NULL;
        free(ps);
        ps = NULL;
        return 0;
}
0 1 2 3 4 
0 1 2 3 4 5 6 7 8 9 
a = 100

代码1和代码2可以完成同样的功能,代码1(柔性数组)相对于方法2的优势。

  • 优势1:方便内存释放
    • 代码2中使用了两次malloc,所以就得进行两次free。
    • malloc用的越多,free也就用的越多。
    • free少了会导致内存泄露,free的先后顺序错了,也会导致出问题。
  • 优势2 有利于访问速度.
    • 连续的内存有益于提高访问速度,也有益于减少内存碎片。
    • 多次使用malloc会导致到处开辟空间,内存碎片多了后其他东西可能就不够位置放了。
    • 这里挖一个坑,那里挖一个坑,挤的别人只能在夹缝里里找空间存放东西
    • 在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值