在学习和使用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必须成对调用,程序中每出现一次开辟就必须有一个释放与其匹配,否则一定会发生错误。
一段问题代码:
void GetMemory(char* p)//p是str的一份临时拷贝!!!
{
p = (char*)malloc(100);
}//结束时,p销毁,开辟的空间未被free,且无法找到,造成内存泄漏
void Test(void)
{
char* str = NULL;
GetMemory(str);//传值调用
strcpy(str, "hello world");//str还是NULL,崩溃
printf(str);
}
//1.程序崩溃
//2.内存泄漏
int main()
{
Test();
return 0;
}
四.多次释放内存
这种情况与上面的情况恰好相反。如果一个指针是空指针,那么你对它进行释放10次也不会出现问题(但没人会这样做),但若其不是空指针,第二次释放时就会出现问题。避免造成这种问题的方式有两个:1.尽量做到谁使用谁释放;2.释放后立即将指针设置为NULL
一个使用动态内存的良好案例:
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
return 0;
}
else
{
//使用
}
free(p);
p=NULL;
return 0;
}
五.使用了已经被释放的内存
有三种情况:
(1)程序中的调用关系过于复杂,实在难以分清某个内存块是否被释放。此时应该对程序进行梳理,或者直接重新设计。
(2)return了已经被销毁的内存。注意不要返回栈内存的指针,因为函数调用结束时栈内存会自动销毁。
(3)释放内存后没有将指针置空,导致产生“野指针”。用free释放内存后,应立即将指针置为空指针。
一段问题代码:
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}//虽然返回了字符数组的地址,但该地址的空间已被销毁
//char* GetMemory(void)
//{
//char* p = "hello,world";//p指向一个字符串常量,字符串常量存储在静态区,所以不会被销毁
//return p;
//}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
另一段问题代码:
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");//非法访问动态内存
printf(str);
}
}
六.不完全释放内存
内存空间只能整块地申请和释放,对于操作系统来说,部分释放内存是不允许的,即不允许“缩容”的情况。在使用free()
释放内存空间时,只能从某块内存空间的起始地址进行释放,此时释放的是曾经申请的整块空间。若从内存块中间进行内存释放,就会出现问题。
另外,给出v.s.编译器下free()
报错“触发断点……”问题的经验:当free()
出现上述错误时,一般有三种情况:
- free野指针
- 从内存块中间进行free(不完全释放)
- 之前操作内存空间时有越界。在v.s.中free会检查之前的内存使用情况,如果该块空间之前被越界访问,free会进行报错
此外,realloc()
在实际使用中一般用来调整内存空间大小使其增大,但若调整的目标空间大小比原空间小,此时不对原来的内存空间大小进行操作,此时应限制“多余”内存空间的使用权限,但是使用这部分内存空间会不会出问题(报错)取决于编译器。
柔性数组
是什么
C99标准中引入了柔性数组的概念。结构体中最后一个成员变量可以是一个未知大小的数组,该数组被称为柔性数组。平常说的长度为0的数组即是指的柔性数组。
struct S
{
int n;
int arr[0];//这是一个柔性数组
}
注:计算上面结构体S的大小时不包括arr[]的大小
我们可以通过动态内存操作开辟和调整柔性数组的大小:
struct S* ps=(struct S*)malloc(sizeof(struct S)+sizeof(数组大小));
if(ps != NULL)
{
//使用
}
/...
.../
free(ps);
ps=NULL;
调整大小:
struct S* ptr = realloc(ps, 数组大小);//调整柔性数组大小
if(ptr != NULL)
{
ps=ptr;
}
/...
.../
free(ps);
ps=NULL;
柔性数组的特点
柔性数组具有以下特点:
(1)柔性数组成员前面必须有其他成员
(2)sizeof()返回具有柔性数组成员的结构体大小时不包括柔性数组的大小
(3)使用malloc对含有柔性数组成员的结构体进行动态内存分配,且分配的空间大小应该大于结构体的大小,以适应柔性数组的大小
为什么需要柔性数组?
在柔性数组引入之前,我们大可在结构体中定义一个指针变量,再使用这个指针变量维护我们想要的动态内存空间,可以达到和柔性数组一样的效果。
struct S
{
char c;
int* parr;//
};
int main()
{
struct S* p = (struct S*)malloc(sizeof(struct S));//为结构体S开辟空间
if (p != NULL)
{
p->parr = (int*)malloc(10 * sizeof(int));//用结构体中的指针维护空间
if (p->parr != NULL)
{
//使用
}
}
free(p->parr);//注意释放顺序
p->parr = NULL;
free(p);
p = NULL;
return 0;
}
那么柔性数组的存在是多余的吗?其实并不是。如果稍微思考就会发现,上述两种方案的内存结构不尽相同。使用柔性数组时,数组的空间和结构体其他成员的空间都被结构体统一维护;而第二种方案则是在结构体之外又开辟了一块新的内存空间。使用柔性数组只需要进行一次malloc和free操作即可完全释放这块空间,而第二种方案至少需要free两次。
柔性数组具有以下优势:
(1)柔性数组方便开辟和释放空间,减少了易错性。
(2)增加了内存利用率。由于柔性数组的使用一般只需要malloc一次,减少了内存碎片,从而增加内存利用率。
(3)提高了内存访问速度。使用柔性数组时的空间是连续的,提高了内存访问的命中率,从而一定程度提高了内存访问速度。