菜鸟笔记
提升您的技术认知

动态内存管理

一、动态内存函数的介绍

1.1malloc函数

malloc函数是我们最常用最简单的一个动态空间开辟函数。

其函数原型如下所示:

该函数接收一个无符号整型变量size,表示开辟的字节数,返回的是一个void*类型的指针。

尽然返回的是指针类型,那我们就得用指针来接收。我们需要开辟什么类型的空间,就强制类型转换为什么类型。

使用该类型的函数,必然要包含头文件,其头文件为 ,后面介绍的函数的头文件均是这个,就不再一一赘述了。

下面为具体使用案例:

例1:

该例子中还存在众多错误。

我们先了解一下malloc函数在使用时的一些注意情况。

首先,malloc函数向内存空间申请的是一块连续可用空间,并返回这块空间的起始地址。如果开辟成功,返回的是有效使用地址,如果失败,返回的是一个NULL指针。

在上面的例子中我们并无法确定a指针指向的是否为有效的空间地址,我们对其进行解引用操作就有可能出错。所以我们应该先检查在使用。

其次,即便是动态开辟的空间,也不能进行越界访问内存未分配给你的空间地址。

上面的 i 增加到10时,会访问未分配的空间的内容。

返回的是void*类型,我们应该强行转换为自己需要的类型。

如果malloc函数在使用时,向内存申请size为0的空间大小,其行为是标准未定义的,取决于编译器。

有了上面的了解,例1就该改为这种情况:

释放空间的作用我们将在下文中进行讲解。

当然我们在使用时,还可以使用perror函数来打印出错的信息对我们进行提示,当指针为空时,直接报错打印信息,方便我们查找改错。

上面例1的结果:

我们发现,malloc函数并不会对我们申请的空间进行初始化,当我们需要初始化时我们可以使用memset函数自行处理。 

1.2calloc函数

C语言还为我们准备了一个函数用来开辟空间,就是calloc。

其函数原型如下:

calloc函数和malloc函数一样,返回的都是void*类型的指针变量,需要具体使用者来强制类型转换在使用。

calloc函数和malloc函数不同的在于,calloc函数含有两个参数,参数1为无符号整型num,表示需要开辟的变量个数,参数2为无符号整型size,表示每个元素的大小。

也就是说,calloc函数向内存申请一块能容纳num个元素,每个元素宽度为size字节的空间,并且把每个字节都初始化为0。 

其余的用法均和malloc类似,开辟成功返回起始地址,失败则返回NULL指针。

例2:用calloc函数开辟空间并将其赋值为‘a’

运行后结果:

这里我们便可以很形象的看到,calloc函数不仅申请了空间,还初始化为0。相当于实现了malloc函数和memset函数合在一起使用。 

1.3realloc函数

realloc函数能够让我们更方便的管理一块内存空间的大小。前面介绍的malloc和calloc函数开辟的空间是固定的,当我们用着用着发现空间不够了,我想在扩容一部分空间的时候,realloc函数就登场了。

该函数能够对我们先前申请的空间进行灵活的调整,其函数原型如下:

函数返回类型依然为void*类型,指向的是增容成功后起始空间的地址。

需要注意的是,该函数的两个参数中有一个是指针,该指针为指向需扩容的空间的起始地址,而无符号整型的size则为增容后的整体字节数(原先的字节数+新增的字节数)。

当需要扩容的指针为NULL指针时,realloc函数和malloc函数一样。

注意:该函数调整空间的方式有两种 

(1)原有空间后面有足够的空间,realloc函数会在后面自动接上一段空间,原先空间的数据不会变化。 

(2)原有空间后面没有足够大的空间,realloc函数会在堆栈上找一块能够容纳整体空间的大小的空间,然后将原来空间中的元素复制到新空间,并返回指向新空间的地址。

正因为如此,我们在使用realloc函数的时候需要格外注意空指针问题。

例3:使用realloc函数新增一块空间

乍一看,例3没什么大问题,当然,在后面我们使用了free(p)进行释放。其实,例3有一个隐患,那就是如果realloc函数开辟空间失败了怎么办,有人会说,我判断了呀,但是,此时的p指向的是空指针,那还意味着,p原先指向的空间也丢了,找不到了。

所以我们应该先用临时变量进行判断周转,然后在赋值给p,避免上述情况发生。

例3的正确书写: 

1.4free函数

在上面的例子中,我们一直在向内存中申请空间,那这些空间用完之后呢?我们需不需要对其回收呢?答案是肯定的,我们申请的空间一定要记得释放掉,不然会造成严重的后果。

如果我们开辟的空间都不释放,慢慢的整个内存都会被我们占用完毕,导致后面的数据无法存放,这就是内存泄漏问题。当然,在程序运行结束后,空间会自动收回。

释放空间的函数为free()函数。其函数原型为下图所示:

free函数返回类型为空,参数为需要释放的空间的地址。

使用free函数时我们要注意,一定得是动态内存管理函数开辟出来的空间才能使用free进行释放,如果ptr指针所指向的地址并不是动态内存开辟出来的,那free(ptr)的行为是未定义的。

如果ptr是NULL,那么free函数什么也不会做。

注意:使用free函数后,一定要把释放完成的指针置为NULL,避免照成野指针使用问题。

 二、使用动态内存的常见错误总结

2.1错误总结:

(1)对空指针进行解引用

这个错误的出现意味着我们在进行动态内存开辟时,默认认为开辟出来的地址是有效地址。就拿例1来说,如果我们用malloc函数开辟INT_MAX个字节的空间,malloc函数就会开辟失败,然后返回一个空指针,如果我们没有进行判断直接使用,程序就会崩溃。当然我们可以加上错误信息打印来提示我们。

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

这个错误在讲解例1的时候就提到过,当下标 i 对malloc函数开辟的空间访问时,可以正常访问,一旦 i 超过了开辟空间的最大元素个数,那我们访问的数据是未知的,照成一些未知的错误。

(3)对非动态开辟内存使用free函数释放

free函数只能对动态开辟的内存进行释放,当你对一个不是开辟出来的空间进行释放,必然会引起错误。

(4)使用free释放动态内存开辟空间的一部分

什么意思呢?就拿下面的代码来说,我们用a接收了malloc函数开辟出来的空间的起始地址,但是我们在初始化的时候将地址更改了,此时的a不在指向这块内存的起始地址,而我们却对其进行了释放,这必然会引起错误。

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

这个代码中,我们连续对p进行释放,第一次释放并没有错误,但是第二次释放时,p虽然还是开辟出来的空间的起始地址,但是所指向的空间已经被系统收回,p此时就是一个野指针,进行释放,必然会报错。

(6)内存泄漏

如果我们一直开辟空间而忘记对其进行释放操作,就会导致程序只要在运行,空间就不会释放,我们内存的空间就会一直占用,最终导致空间不够用,造成内存泄露问题。 

2.2易错题分析:

例1:

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

根据上面的代码,能否打印出hello world呢?

答案是否定的,该代码无法打印出来。

原因就是我们调用GetMemory函数的时候传过去的是指针str的一份临时拷贝,因此函数中p接收的指向内存开辟空间的地址并不影响str,虽然malloc开辟出来了空间,但是ptr依然是NULL,当函数调用完成后,p变量自动销毁,但是程序没有结束,malloc开辟的空间并没有被释放,造成内存泄漏问题。当我们回到主函数中,使用strcpy函数进行拷贝,但是str指向的依然是空指针,strcpy并不会拷贝成功,所以printf函数无法打印出字符。

我们应该进行传址调用。

 例2:

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

该代码能否打印出hello world呢?

答案是打印不出来,最终结果是随机值。

为什么呢?

str通过GetMemory函数来获得空间,函数将"hello world"存于局部变量字符数组p中,返回的是指向数组的起始地址,但是一旦调用结束后,char p[ ]自动销毁,此时的str虽然指向起始地址,但是后面的内容已经被系统收回,str为野指针,指向的内容并不知道是啥,所以会打印出来随机值。

可以更改为如下版本:

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

 该问题比较明显,就是未对malloc开辟的空间进行释放,有造成内存泄漏的风险。只需要在使用完后用free进行释放就可。

例4:

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

虽然这个代码在某些编译器上能够成功打印出来world,但并不代表它没问题。

该代码实际上对野指针进行了访问操作。

str所指向的空间被free释放掉了,但是str还记得空间的起始地址,str当然不是NULL,所以进入if语句直接开始解引用,访问一个系统未分配的空间,造成野指针问题。

所以在使用free函数释放空间后,一定要将释放的地址置为NULL,避免再次访问。

三、柔性数组

3.1什么是柔性数组?

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

这两种写法都表示柔性数组,因编译器而已,总会有一种能在编译器下运行。

3.2柔性数组的特点

通俗的来讲,柔性数组就是一个没有指定大小的数组成员。那么问题来了,没有大小?那s1这个结构体的总体大小是多少?

答案是4,也就是说大小是柔性数组成员前面那个成员的大小。

所以柔性数组有以下特点:

(1)结构中的柔性数组成员前面必须至少有一个其他成员,柔性数组是结构中的最后一个成员。

(2)sizeof计算结构大小时,返回的是不包含柔性数组成员的总体大小。

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

3.3柔性数组的使用

例1:

我们使用malloc为struct s1类型的指针变量ps创建空间,并且为柔性数组arr开辟出来10个整型的空间,让其可以作为正常数组使用。

看到这里,有些同学有问题了?

我不用柔性数组也可以这样用啊

我写成这样的代码不行吗?

 例2:

这个代码没有使用柔性数组,一样达到了上述代码的功能。

确实如此,两个代码实现的功能是一样的。那为什么还要有柔性数组呢?下面我们来看看柔性数组的优势。

3.4柔性数组的优势

在我们使用柔性数组实现例1的功能时,只需要向内存空间申请一次空间。我们只需要释放一次即可。如果在函数体内部运行,使用例2开辟出的空间需要使用两次进行释放,先释放总体为结构体开辟出来的空间,然后还要释放为结构体变量开辟出来的空间,但是用户并不知道,他所看到的可能只是一个结构体指针,然后对其释放了,这就导致内存泄漏,还不一定查找出原因。而对于例1来说,用户轻易的就能释放掉空间。

柔性数组可以提高访问速度,连续的内存有益于提高访问速度,也有益于减少内存碎片。

什么意思呢?

例1使用一次malloc开辟出的空间是连续的,直接使用即可。

例2使用两次malloc开辟的空间并非连续的,而是在内存中寻找的两块空间,容易造成碎片化的空间浪费。

 

lili