在学习和使用C语言的过程中经常要编写管理内存的程序,往往提心吊胆。若是不想踩雷,唯一的办法就是深入理解内存管理,发现所有的陷阱并排除他们。本文主要讨论动态内存的使用和管理。


内存的使用方式

内存主要有三种分配方式:

(1)在栈(Stack)上创建。可以在栈区创建数个局部变量或者局部数组。函数结束执行时这些内存被自动释放。

(2)从静态区(Static)分配。在静态区创建全局变量,static修饰的变量和常量字符串都在静态区存储。这些内存在程序的整个运行期间都存在。

(3)从堆区(Heap)分配。又称动态内存分配。使用动态内存可以非常灵活,但问题也最多。

C语言动态内存管理_动态内存


动态内存


·使用库函数进行动态内存管理

在C语言中使用malloccallocreallocfree等库函数进行动态内存管理。


malloc

void* malloc(size_t size);

malloc用于开辟一块动态内存空间。参数为开辟的内存大小,单位是字节,返回所开辟内存空间的地址。

使用时需要注意以下几点:

  1. 使用malloc可能出现开辟内存失败的情况,此时返回值为NULL
  2. malloc的返回值是void*类型,用指针接收malloc开辟的空间时,注意进行类型转换
  3. 对于size为0的情况,C语言标准未规定,可能会造成危险,尽量不要进行此操作
  4. 使用malloc开辟的空间中初始是随机值

calloc

void* calloc (size_t num,size_t size);

calloc用于开辟一块动态内存空间。参数为要开辟空间中元素的个数和元素大小,单位是字节,返回值为所开辟的内存空间的地址。其与malloc不同的地方除参数列表外,也会将所开辟的空间中的值自动初始化为0

使用时需要注意:

  1. 可能出现开辟内存失败情况,此时返回值为NULL
  2. 用指针接收所开辟的空间时,注意类型转换

realloc

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

realloc用于调整已开辟内存空间的大小。参数为已开辟内存的地址和新的空间大小,单位是字节。

realloc的内存调整

realloc的内存调整有两种情况

(一) 进行内存增加调整时,若原来内存后面的空间足够,则直接在原来内存空间的后面进行内存的追加,此时返回原来内存的指针

(二) 若原来内存后面的空间不足,则另外开辟一块新的空间,将原来空间内的数据拷贝到新空间中,释放原来的空间,返回新空间的地址

即realloc的返回值有多种可能。在实际使用过程中,也可能会发生调整内存失败的情况,此时realloc的返回值为NULL。若直接使用原来空间的指针(假设为p)进行接收,且此时开辟空间失败返回NULL,则原来空间的指针就会被赋值为空指针,造成原来空间的遗失。因此应该先使用另外一个指针变量(假设为ptr)接收realloc的返回值,接着对ptr进行判断,若ptr不为空指针,则可以放心得将这块调整后的空间交给p

C语言动态内存管理_柔性数组_02


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


柔性数组


是什么

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两次

C语言动态内存管理_柔性数组_03


柔性数组具有以下优势:

(1)柔性数组方便开辟和释放空间,减少了易错性。

(2)增加了内存利用率。由于柔性数组的使用一般只需要malloc一次,减少了内存碎片,从而增加内存利用率。

(3)提高了内存访问速度。使用柔性数组时的空间是连续的,提高了内存访问的命中率,从而一定程度提高了内存访问速度。