一、动态内存函数的介绍
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