在学习和使用C语言的过程中经常要编写管理内存的程序,往往提心吊胆。若是不想踩雷,唯一的办法就是深入理解内存管理,发现所有的陷阱并排除他们。
内存的使用方式
内存主要有三种分配方式:
(1)在栈(Stack)上创建。可以在栈区创建数个局部变量或者局部数组。函数结束执行时这些内存被自动释放。
(2)从静态区(Static)分配。在静态区创建全局变量,static修饰的变量和常量字符串都在静态区存储。这些内存在程序的整个运行期间都存在。
(3)从堆区(Heap)分配。又称动态内存分配。使用动态内存可以非常灵活,但问题也最多。
动态内存
·使用库函数进行动态内存管理
在C语言中使用malloc、calloc、realloc和free等库函数进行动态内存管理。
malloc
void* malloc(size_t size);
malloc用于开辟一块动态内存空间。参数为开辟的内存大小,单位是字节,返回所开辟内存空间的地址。
使用时需要注意以下几点:
- 使用malloc可能出现开辟内存失败的情况,此时返回值为NULL
- malloc的返回值是void*类型,用指针接收malloc开辟的空间时,注意进行类型转换
- 对于size为0的情况,C语言标准未规定,可能会造成危险,进行不要进行此操作
- 使用malloc开辟的空间中初始是随机值
calloc
void* calloc (size_t num,size_t size);
calloc用于开辟一块动态内存空间。参数为要开辟空间中元素的个数和元素大小,单位是字节,返回值为所开辟的内存空间的地址。其与malloc不同的地方除参数列表外,也会将所开辟的空间中的值自动初始化为0。
使用时需要注意:
- 可能出现开辟内存失败情况,此时返回值为NULL
- 用指针接收所开辟的空间时,注意类型转换
realloc
void* realloc(void* mem,size_t size);
realloc用于调整已开辟内存空间的大小。参数为已开辟内存的地址和新的空间大小,单位是字节。
realloc的内存调整
realloc的内存调整有两种情况:
(一) 进行内存增加调整时,若原来内存后面的空间足够,则直接在原来内存空间的后面进行内存的追加,此时返回原来内存的指针。
(二) 若原来内存后面的空间不足,则另外开辟一块新的空间,将原来空间内的数据拷贝到新空间中,释放原来的空间,返回新空间的地址。
即realloc的返回值有多种可能。在实际使用过程中,也可能会发生调整内存失败的情况,此时realloc的返回值为NULL。若直接使用原来空间的指针(假设为p)进行接收,且此时开辟空间失败返回NULL,则原来空间的指针就会被赋值为空指针,造成原来空间的遗失。因此应该先使用另外一个指针变量(假设为ptr)接收realloc的返回值,接着对ptr进行判断,若ptr不为空指针,则可以放心得将这块调整后的空间交给p。
free
void free( void *memblock );
free用于主动释放一块动态内存空间。参数为指向一块内存的指针,返回值为空。
注意:
(1)程序结束时,动态内存自动回收,但最好用free主动进行内存释放。
(2)free只是将相应的内存进行释放,此时该内存的指针依旧指向被释放的内存,即造成了一个“野指针”。为了保证安全,在释放内存时应彻底将内存和指向该内存的指针切断联系,即将指针设置为空指针。
(3)如果参数为NULL,则free不进行任何操作。
(4)free只针对动态开辟的空间。
常见的动态内存错误及其对策
一.使用了未分配成功的内存
这个问题常见于开辟/调整动态内存失败但仍使用的情况。在使用一块内存之前,最好先检查指针是否为NULL,用if(p == NULL)
或if(p != NULL)
进行防错处理。
二.操作越过了内存的边界
例如在操作数组时,经常发生数组下标多1或少1的情况。特别是在for循环中,循环次数的错误容易造成内存的越界。
三.忘记释放内存,导致内存泄漏
含有这种错误的函数或语句,每被调用或执行一次都会吃掉一块内存空间。刚开始你可能不会发现,但终有一次会发生错误:内存不足。
动态内存的开辟和释放必须成对存在,malloc/free必须成对调用,程序中每出现一次开辟就必须有一个释放与其匹配,否则一定会发生错误。
一段问题代码:
四.多次释放内存
这种情况与上面的情况恰好相反。如果一个指针是空指针,那么你对它进行释放10次也不会出现问题(但没人会这样做),但若其不是空指针,第二次释放时就会出现问题。避免造成这种问题的方式有两个:1.尽量做到谁使用谁释放;2.释放后立即将指针设置为NULL
一个使用动态内存的良好案例:
五.使用了已经被释放的内存
有三种情况:
(1)程序中的调用关系过于复杂,实在难以分清某个内存块是否被释放。此时应该对程序进行梳理,或者直接重新设计。
(2)return了已经被销毁的内存。注意不要返回栈内存的指针,因为函数调用结束时栈内存会自动销毁。
(3)释放内存后没有将指针置空,导致产生“野指针”。用free释放内存后,应立即将指针置为空指针。
一段问题代码:
另一段问题代码:
柔性数组
是什么
C99标准中引入了柔性数组的概念。结构体中最后一个成员变量可以是一个未知大小的数组,该数组被称为柔性数组。平常说的长度为0的数组即是指的柔性数组。
注:计算上面结构体S的大小时不包括arr[]的大小
我们可以通过动态内存操作开辟和调整柔性数组的大小:
调整大小:
柔性数组的特点
柔性数组具有以下特点:
(1)柔性数组成员前面必须有其他成员
(2)sizeof()返回具有柔性数组成员的结构体大小时不包括柔性数组的大小
(3)使用malloc对含有柔性数组成员的结构体进行动态内存分配,且分配的空间大小应该大于结构体的大小,以适应柔性数组的大小
为什么需要柔性数组?
在柔性数组引入之前,我们大可在结构体中定义一个指针变量,再使用这个指针变量维护我们想要的动态内存空间,可以达到和柔性数组一样的效果。
那么柔性数组的存在是多余的吗?答案并不是。如果稍微思考就会发现,上述两种方案的内存结构不尽相同。使用柔性数组时,数组的空间和结构体其他成员的空间都被结构体统一维护;而第二种方案则是在结构体之外又开辟了一块新的内存空间。使用柔性数组只需要进行一次malloc和free操作即可完全释放这块空间,而第二种方案至少需要free两次。
柔性数组具有以下优势:
(1)柔性数组方便开辟和释放空间,减少了易错性。
(2)增加了内存利用率。由于柔性数组的使用一般只需要malloc一次,减少了内存碎片,从而增加内存利用率。
(3)提高了内存访问速度。使用柔性数组时的空间是连续的,提高了内存访问的命中率,从而一定程度提高了内存访问速度。