C语言重中之重!动态内存的所有问题你都能在这篇文章中找到答案

在这里插入图片描述

引入

typedef struct Con
{
	char name[20];
	char sex[10];
	int age;
	char tel[30];
	char addr[100];
}Con;

int main()
{
	Con con[100];
	return 0;
}

例子:一个固定开辟了100个元素的通讯录中的人员结构体数组,那么当我们只有10个人的时候,那么就有90个元素的空间被浪费了,如果我们有110个人的时候,数组就不够了。因此,我们为了内存能够得到有效的利用,能够适配具事件的不同情况,不至于 浪费,也不至于不够,能够有效的节省空间,所以需要在堆区动态开辟内存,以供我们更加灵活高效的使用内存
在这里插入图片描述

动态内存函数

malloc和free

函数原型:void* malloc (size_t size);
size - 我们需要开辟的动态空间大小,如果size为0,malloc的行为未定义,完全取决于编译器
返回值 - 因为返回类型是void* 所以常常搭配强制类型转换,如果返回成功,那么得到开辟的动态空间的首地址,如果开辟失败,返回NULL
所以在使用malloc函数时,需要判空NULL(判成功),防止在开辟失败后让变量得到NULL,还在进行后续各类操作(解引用、赋值…)。

#include<errno.h>errno
#include<string.h>strerror
#include<stdlib.h>malloc
#include<stdio.h>printf
int main()
{
    int* p = (int*)malloc(40);
    if(p == NULL)
    {
        printf("%s\n", strerror(errno));//打印错误信息
        return 1;
    }
    return 0;
}

这是一种判空方法,这种方法很完善,能够知道错误原因,而如果直接用assert§的话,只能知道在哪一行出现的问题,但是为了使用printf("%s\n", strerror(errno));这一行代码,需要用到两个不常用的头文件,所以简洁的判空的处理方法是用perror函数,此函数的头文件是<stdio.h>,不需要额外的头文件

在堆区申请空间都需要自行归还给操作系统,malloc,以及后面的realloc,calloc函数都是在堆区申请的空间,因此都需要自行归还,都需要使用free函数
函数原型:void free (void* ptr);
ptr - 动态开辟的内存的首地址。如果ptr为NULL,free函数什么也不做,如果ptr不是动态开辟的,那么free的行为未定义,取决于编译器

free函数仅仅是释放掉ptr指针所指向开辟的动态内存,也就是说ptr释放后,ptr指向的空间还存在,但是此时该空间就不是我们能够使用的空间了,也就是变成野指针了,为了防止非法访问,我们就需要将野指针置空:ptr = NULL
当然,在程序结束后,编译器会自动帮助我们归还动态开辟的内存,但这样依靠编译器的方式非常不妥

#include <stdio.h>
#include<stdlib.h>
int main()
{
	int* ptr = NULL;
	ptr = (int*)malloc(10 * sizeof(int));
	if (ptr == NULL)
	{
		perror("malloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(ptr + i) = i;
	}
	free(ptr);//释放ptr所指向的动态内存
	ptr = NULL;
	return 0;
}

在我们调试的时候,想要看到某个指针后面每一个元素的情况,只需要在监视窗口输入p,10(10为元素总个数)
在这里插入图片描述
malloc失败案例

//x86(32位)上跑代码,x64(64位)上INT_MAX还不够大,还能开辟成功
#include<stdlib.h>
int main()
{
       int* p = (int*)malloc(INT_MAX);//21亿大小的一个数字
       if (p == NULL)
       {
               perror("malloc");//malloc: Not enough space
               return 1;
       }
       free(p);
       p = NULL;
       return 0;
}

calloc

其函数原型:void* calloc (size_t num, size_t size);
函数的功能是给num个大小为size的元素开辟空间,并且把空间的每个字节初始化为0。
与函数malloc的区别只在于calloc会在返回之前把申请的空间的每个字节初始化为全0,也正因如此,malloc的效率更高,但当有初始化的需求,用calloc也是不错的选择。

#include <stdio.h>
#include<stdlib.h>
int main()
{
	int* ptr = NULL;
	ptr = (int*)calloc(10, sizeof(int));
	if (ptr == NULL)
	{
		perror("malloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(ptr + i));//0 0 0 0 0 0 0 0 0 0 
	}
	free(ptr);//释放ptr所指向的动态内存
	ptr = NULL;
	return 0;
}

realloc

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

ptr - 是要调整的内存地址,如果ptr是NULL,那么realloc就相当于是malloc
size - 调整之后新大小,可比原来空间大,也可以比原来空间小
返回值为调整之后的内存起始位置,如果调整空间失败,会返回NULL,因此在使用realloc时,先用一个临时的指针变量tmp去接收realloc的返回值,如果不为NULL,再把tmp赋值给ptr
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

realloc在调整内存空间的是存在两种情况:
情况1:扩大空间,原有空间之后有足够大的空间,此时会保留原来的数据,在原有内存的后面直接追加空间。
情况2:扩大空间,原有空间之后没有足够多的空间时,扩展的方法是在堆空间上另找一个合适大小的连续空间,并把原来的旧数据拷贝到新空间上去,然后realloc函数内会把旧地址的动态空间释放掉,这样函数返回的是一个新的内存地址。
情况3:缩小空间,缩小原有空间能够访问到的空间大小,保留缩小后能够访问到的原有数据。

#include<stdio.h>
#include<stdlib.h>
int main()
{
    int* p = (int*)malloc(5 * sizeof(int));
    if ( p == NULL)
	{
         perror("malloc");
	     return 1;
    }
    //使用
	int i = 0;
    for (i = 0; i < 5; i++)
	{
	* (p + i) = 1;
	}
    //不够了,增加5个整形的空间
	int* ptr = realloc(p, 10 * sizeof(int));
    if (ptr != NULL)
    {
        //当动态申请空间成功后    
        p = ptr;//我们不知道是否用到了新空间,所以再将ptr赋值给p
        //然后就可以用p正常使用新的动态空间了
        ptr = NULL;//这里ptr和p指向了同一块空间,可以使用ptr,但是大多数情况,只需要一个指针,所以一般就立刻将ptr置空,如果需要使用ptr,就需要记得在free(p)这块动态内存后,将p和ptr都给置空以防止野指针。
	}
    //使用动态内存
	//...
	free(p);
    p = NULL;
    //ptr = NULL;
    return 0;
 }
int main()
{
    int* p = (int*)realloc(NULL, 40);
    //当第一个参数是NULL,那么realloc就相当于malloc
    if(p != NULL)
    {
        //使用动态内存
    }
    free(p);
    p = NULL;
    return 0;
}

动态内存常见使用错误

一:malloc的空间没有判成功,对NULL指针的解引用操作

int main()
{
    int* p = (int*)malloc(100);//没有assert或者if判成功,会报警告:对NULL指针的解引用操作
    int i = 0;
    for(i = 0; i < 20; i++)
    {
        *(p + i) = 0;
    }
    return 0;
}

二:把空间大小当作个数,越界访问

int main()
{
    int* p = (int*)malloc(100);
    if(p == NULL)
    {
        return 1;
    }
    int i = 0;
    for(i = 0 ; i < 100; i++)
    {
        *(p + i) = 0;
    }
    return 0;
}

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

int main()
{
    int a = 10;//a存放在栈区
    int* p = &a;
    free(p);//不能free栈区空间
	p = NULL;
	return 0;
}

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

void test1()
{
    int* p = (int*)malloc(100);
    p++;
    free(p);//此时p不是动态开辟的内存的起始位置,所以不同free
}

void test2()
{
    int* p = (int*)malloc(100);
    if(p == NULL)
    {
    	perror("malloc");
        return 1;
    }
    int i = 0;
    for(i = 0; i< 25; i++)
    {
        *p = i;
        p++;
        //更好的方式:for(i = 0; i < 5; i++){ *(p + i) = 1;}
    }
    free(p);
    p = NULL;
    return 0;
}

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

int main()
{
    int* p = (int*)malloc(100);
    if(p == NULL)
    {
        return 1;
    }
    free(p);
    p = NULL;
    free(p);//这样从语法上是没有问题的,因为free函数当参数为NULL时,什么也不做,但如果p没有置空,多次free,程序会崩。
}

六:动态开辟内存忘记释放

void test()
{
    int* p = (int*)malloc(100);
    //使用
}
//如果test函数中没有释放空间,那出了函数就无法找到这块动态内存
int main()
{
    test();
    return 0;
}

malloc 判成功 free 置空 一个不能少

int* test()
{
    int* p = (int*)malloc(100);
    if(p == NULL)
    {
        return 1;
    }
    //使用
    return p;
}
//test中有malloc操作,当因为具体需求,不能在test中free时,那么就用返回值的方式,取得malloc开辟的动态地址。并且记得注释提示程序员主函数中需要free动态内存。
int main()
{
    int* ptr = test();
    free(ptr);
    ptr = NULL;
}
void GetMemory(char* p)
{
    int* p = (int*)malloc(100);
    if(p == NULL)
    {
        return;
    }
    //使用
    //if分支中未释放,发生内存泄露
    if(1)
        return;
    free(p);
    p = NULL;
}

内存泄露的影响?比如某些app重启可以解决卡顿问题,但是开机后使用该app一段时间又开始卡了,其实有时就是内存泄露,使得cpu占用率越来越高。
内存泄露只会因为动态内存分配malloc、calloc、realloc发生(C语言范围内)

经典笔试题

1、传值、释放问题

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

这个GetMemory函数是传值调用,并且没有返回值,所以无法在Test中改变str的值,并且p的生命周期在离开GetMemory函数时就结束了,也就是str还是NULL,我们无法对0x00000000位置进行(写入)解引用操作,除此之外,没有释放,所以还发生了内存泄露。
在这里插入图片描述
所以可以采用返回值,传址调用的方式,并且在Test函数中进行free、置空。

2、返回局部变量或临时变量的地址(返回了栈空间地址)

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

p是char数组类型,生命周期只在GetMemory函数中,离开函数后,p指向的空间已经归还给操作系统了,此时返回得到的p的地址其实是野指针,是非法访问内存的问题

此题的修改方式是将char p[] = "hello world;"改成char* p = "hello world;此时p存储的就是常量字符串"hello world"的地址了,常量字符串的地址是在静态区的,生命周期是整个程序,因此离开了GetMemory函数后,p指针指向的地址还能够正常访问。
ps:常量字符串是无法进行修改的
为了理解这里的char* p = "hello"char p[] = "hello",我举一个例子
在这里插入图片描述

int* test()
{
    int a = 10;
    return &a;
}
int main()
{
    int* p = test();
    printf("%d", *p);
    return 0;
}

在这里插入图片描述
这里能够打印出10只是巧合,如果在printf语句前后有其他的语句,那么printf打印p的值会发生改变。(函数的栈帧被销毁了,但是内容没有被修改)
在这里插入图片描述
这里p并未改变,但是打印的值会发生变化。(栈帧问题)

3、内存泄露

void GetMemory(char **p, int num)
{
    *p = (char *)malloc(num);
    //没有free和置空,内存泄露
}
void Test(void)
{
    char *str = NULL;
    GetMemory(&str, 100);//传址正确
    strcpy(str, "hello");//没问题
    printf(str);//打印没问题
}
int main()
{
    Test();
    return 0;
}

4、在free后使用动态空间

void Test(void)
{
    char *str = (char *) malloc(100);
    strcpy(str, "hello");
    free(str);
    //在释放后,str还是指向的那块空间,内容可能还未改变。
    if(str != NULL)
    {
        //str此时变成了野指针,发生非法访问
        strcpy(str, "world");
        printf(str);
    }
}

栈区堆区静态区的简单讲解:
栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS(操作系统)回收 。分配方式类似于链表。
数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
代码段:存放函数体(类成员函数和全局函数)的二进制代码以及只读常量(常量字符串)。
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁。因此static修饰的局部变量的生命周期变长了

柔性数组

结构体中的最后一个成员允许是未知大小的数组,这就叫做『柔性数组』成员(C99标准),柔性数组的大小通过函数中动态内存开辟的大小来确定
在这里插入图片描述
结构体中可以用[0]这种方式代表大小位置,但是函数中的数组大小不能用这种方式表示
特点:
1、结构中的柔性数组成员前面必须至少一个其他成员(第二点即为第一点的原因)
2、sizeof 返回的这种结构体大小不包括柔性数组的内存在这里插入图片描述
3、包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小
在这里插入图片描述
如果后面对于这个数组大小需要改变,那么就可以用realloc对柔性数组大小进行改变
在这里插入图片描述
柔性数组可以使用多次动态内存的方式达到效果
在这里插入图片描述
这种方式会多次malloc,会导致出现很多内存碎片,让代码的空间利用率低,多次free,也需要考虑free的顺序,因此柔性数组有其存在的价值(1、方便内存释放 2、有利于提高访问速度)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

失去梦想的小草

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值