动态内存管理:教你在堆上创建变量,避开栈的销毁打击

在学习malloc之前,我们开辟的所有数据的存储基本上都是存放在栈上面的。

int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间

以上开辟空间的方法还是非常经典的,但是其实我们使用起来还是很难受的,有时候我们处理一些问题的时候没法提前知道我这个数组到底需要多大,而且本身C语言并不支持变长数组,所以说很难让数组的大小实现自适应,通常还要手动改。

存放在栈上的数据还有一点非常让人难受,那就是出函数就被销毁了。

总之……就是非常蛋疼。


如果你对C语言的内存分配还有一些不了解,这里有一些概述。

 1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结
束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是
分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返
回地址等。
2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分
配方式类似于链表。
3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。



所以,动态内存的开辟还是非常有用且方便的,接下来则是对动态内存开辟的一些记述。

开拓者:malloc

malloc 和 free

 C语言提供了一个动态内存开辟的函数:

扫描二维码关注公众号,回复: 16002804 查看本文章
void* malloc (size_t size);

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

这块被开辟的空间与我们正常开辟的空间的位置不一样,它是开辟在堆上的。

如果开辟成功,则返回一个指向开辟好空间的指针。
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。

int main ()
{
    int* ret = (int*)malloc(sizeof(int) * 4);
    if(ret == NULL )
    {
        perro("malloc fail!");
        exit(-1);
    }
    return 0;
}

 空指针的返回非常危险,我们使用上述的代码来防止这种情况的发生。
返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己
来决定。

int* ret = (int*)malloc(sizeof(int) * 4);

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

但是既然开辟在堆上的空间躲开了来自函数栈帧销毁的打击,那么我们开辟的内存的释放就得要我们自己来了,毕竟相当于自动转手动了,而且开辟的空间不还给人家也说不过去。

C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:

free

void free (void* ptr);

free函数用来释放动态开辟的内存。
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
如果参数 ptr 是NULL指针,则函数什么事都不做。
malloc和free都声明在 stdlib.h 头文件中。

 

int main ()
{
    int* ret = (int*)malloc(sizeof(int) * 4);
    if(ret == NULL )
    {
        perro("malloc fail!");
        exit(-1);
    }
    free(ret);
    ret = NULL;
    return 0;
}

在我们free掉malloc出来的空间之后,不要忘记将那个用于存放开辟空间的指针置空,这是一个好习惯。

假如我们不去释放会发生什么?

忘记释放不再使用的动态开辟的空间会造成内存泄漏!

内存泄漏的问题是非常严重的,因为一旦造成了内存泄漏,我们将无法再对那块空间做出任何改动了,那块空间就再也没有办法去找的到了。这种情况如果发生的多了将会使得整个程序崩溃,所以还需要非常小心。

calloc

C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。原型如下:

void* calloc (size_t num, size_t size);

 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
举个例子:

#include <stdio.h>
#include <stdlib.h>
int main()
{
    int *p = (int*)calloc(10, sizeof(int));
    if(NULL != p)
    {
        perro("malloc fail!");
        exit(-1);
    }
    free(p);
    p = NULL;
    return 0;
}

 我们拿到p的地址然后监视一下内存。


那么,calloc和malloc之间怎么去选择呢

需要初始化时使用calloc,不需要时使用malloc

方才提到的两个函数其实都是可以动态开辟空间的,我们只需要在传参的时候将变量传参即可,但接下来的这位更是重量级,它的业务范围就比较广了。

realoc

 用于调整被开辟的内存空间的大小

虽然我们可以通过控制变量的大小来动态开辟内存空间的大小,但是还是会发生在程序运行的期间开的大了或者开的小了的情况,realoc则可以很好的处理这个情况

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

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

这个函数进行调整的情况是有如下两种的:

情况1:原有空间之后有足够大的空间

当所需要扩容的内存后面容得下时,realoc会直接在所需扩容内存后面直接扩容

情况2:原有空间之后没有足够大的空间

为了避免在扩充内存的时候越界访问,realoc其实是找了个地儿能存下要求大小的地方新建一个空间,把原本需要扩充的空间拷贝过去,而原本的被扩充的老空间会被realoc释放掉

 

 由于上述的两种情况,realloc函数的使用就要注意一些

比方说如下情况:

int main()
{
	int* ptr = (int*)malloc(100);
	if (ptr != NULL)
	{
		//业务处理
	}
	else
	{
		exit(EXIT_FAILURE);
	}

	//扩展容量
	//代码1
	ptr = (int*)realloc(ptr, 1000);//这样可以吗?(如果申请失败会如何?)
}

我们可能乍一看感觉是对的,我们用realoc调整了ptr开辟空间的大小,然后更新ptr回去,看上去一点问题没有啊?

哪请问这种情况下假如说我们的ptr开辟空间失败了,这么多的数据开不出来会发生什么?

我们知道空间开辟失败的时候会返回空指针,空指针的麻烦可就大发了,这下一赋值,寄!ptr的值丢了!

所以我们要这样子改:

int* newptr = ptr;
newptr = (int*)realloc(ptr, 1000);
//用一个新指针去接收。

if(newptr == NULL)
{
    newptr = ptr;
}

如果开空了,至少ptr指向开辟的空间的信息不会丢失

常见的动态内存错误

 在堆上头进行操作还是要相当小心的,一不注意就可能会造成内存泄漏,一下归纳了一些常见的错误。

对NULL指针的解引用操作:

void test()
{
    int *p = (int *)malloc(INT_MAX/4);
    *p = 20;
    free(p);
}

看着没啥问题,但是没有检查开辟后的返回值,如果*p此时正好开辟失败是个空指针就很麻烦了。

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

void test()
{
    int i = 0;
    int *p = (int *)malloc(10*sizeof(int));

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

    free(p);
}

经典的计数失误问题。

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

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

 直接崩了。

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

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

少释放一个int大小的空间,造成内存泄漏

数组首元素地址:我呢?你不管我了吗?

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

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

这个就纯属没啥必要了,也崩了

不过free函数对NULL是不作为的,所以如果p置空了的这个程序是没什么问题的

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

这个问题前文已经提到过了,不再过多叙述。

几个经典的笔试题

 1.

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

请问运行Test 函数会有什么样的结果?

 崩了。

这里犯了个非常经典的错误,也就是关于函数栈帧销毁的问题,诚然我们在堆上面开辟空间可以避开空间销毁的打击,但是p本身依旧是个在Getmemory内部的形参,它只是str地址的拷贝,出了函数即销毁,所以啥都没了。

如果想用函数来开辟空间,可以这样修正:

 虽然其实没啥必要哈哈哈哈哈哈。


2.

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

这段代码会输出啥?

 还是非常经典的问题,char p【】这个数组在出了函数就已经被销毁了,还给操作系统了,就算str能找到原来的地址,里头存的也不是hello world 了。


3.

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);
    }
}

这段程序会打印啥?

拷贝完后free释放,malloc空间已经回收,但是str指针还记得起始地址, 还去拷贝使用str的空间,非法操作,造成野指针

所以当我们free完后应该马上把指针置为NULL。

柔性数组

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

struct S3
{
	int num;//4
	int arr[];//柔性数组成员
};
 
int main()
{
	printf("%d\n", sizeof(struct S3));//?
	return 0;
}

柔性数组必须得使用动态内存方式才能使用

我们答应整个结构体的大小,得到的是4个字节。

结构中的柔性数组成员前面必须至少一个其他成员。

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

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

struct S3
{
	int num;//4
	int arr[];//柔性数组成员
};
 
int main()
{
	struct S3* ps = (struct S3*)malloc(sizeof(struct S3)+40);

	return 0;
}

虽然说柔性数组它本身像个幽灵一样并不占用任何空间,但是我们在分配空间的时候依然需要专门为其增加空间。其实它本身不占用空间也是方便了我们使用,这样当整个结构体偏复杂时,我们也不需要专门的去计算数组的大小。

//代码1
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);

这样柔性数组成员a,相当于获得了100个整型元素的连续空间。

但我们可能会想,这样子用起来还是挺蛋疼的,我们就不能直接在结构体内部创建一个指针然后malloc出来当数组用吗?

比如这样子:

typedef struct st_type
{
    int i;
    int* pa;
}type_a;


int main()
{
    type_a* p = (type_a*)malloc(sizeof(type_a));
    p->i = 100;
    p->pa = (int*)malloc(p->i * sizeof(int));
    //业务处理
    for (int i = 0; i < 100; i++)
    {
        p->pa[i] = i;
    }
    //释放空间
    free(p->pa);
    p->pa = NULL;
    free(p);
    p = NULL;
}

这样子也是可行的,可是我们需要考虑到当我们面向用户编程的时候,柔性数组的优势就体现出来了

释放简单:

对于柔性数组的结构体来讲,我们用起来还算是方便的,释放的时候也非常方便,直接free掉整个结构体指针就可以了,不用考虑其他的

而用指针则蛋疼在我不仅需要释放结构体指针,还需要释放那个指针的空间。

这就很难受了,你不能指望着用户可以完全了解你实现的底层结构,让用户不仅释放结构体,还需要释放成员指针,这是不行的。

当然,柔性数组也可以提升一定的访问效率:

连续的内存有益于提高访问速度,也有益于减少内存碎片(提高内存利用率)。CPU在寄存器中拿取数据,根据局部性原理数据在被访问时,会把周围数据加载到寄存器中,当访问到后面数据时,这时候CPU访问数据时在寄存器中命中概率会高一些。

如果在寄存器中没有发现数据,CPU会去缓存->内存拿取,直到找到对应数据


感谢阅读!希望对你有点帮助!

猜你喜欢

转载自blog.csdn.net/m0_53607711/article/details/126886028