5. 动态内存管理
5.1 为什么存在动态内存分配?
已经掌握的内存开辟方式有:
int val = 20; // 在栈空间上开辟四个字节
char arr[10] = { 0 }; //在栈空间上开辟10字节的连续空间
以上两种开辟空间方式特点如下:
-
空间开辟大小是固定的
-
数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配
- 但有时并不能预先知道需要需要开辟的空间大小,此时就需要动态内存。
5.2 动态内存函数
动态内存函数用来管理(开辟和释放)动态内存的。存在于头文件
stdlib.h
可编程内存分为栈区、堆区、静态存储区三部分。
- 栈区(stack):函数内部局部变量、函数参数、函数返回值在栈区创建,函数执行结束,存储单元自动释放。栈区内存分配运算内置于处理器指令集中,效率高,但分配的内存容量有限。
- 堆区(heap):或称为动态内存分配区。程序运行时申请,使用结束后释放。如果不手动释放,在程序运行结束后将被操作系统回收。若不释放,会导致内存泄露。分配方式类似于链表。
- 静态存储区(static):主要存放静态数据,全局数据和常量。该内存在程序编译时已经分配,在程序的整个运行期间都存在。
- 更多—程序内存分配
5.2.1 malloc
// 原型
void* malloc (size_t size);
// 参数:size -- 需要开辟的动态内存大小 单位byte
// 功能:向内存申请一块连续可用的空间 并返回指向该空间的指针
- 如果开辟成功,则返回一个指向开辟号空间的指针
- 如果开辟失败,则返回一个
NULL
指针 因此malloc
的返回值一定要做检查 - 返回值的类型是
void*
,使用时需要强制类型转换为需要的类型 - 如果参数
size
为0
,malloc
的行为是标准未定义的,取决于编译器 - 使用
#include <stdio.h> #include <stdlib.h> int main() { int* p = (int*)malloc(3 * sizeof(int)); // 申请3个int大小的堆区内存空间 if (p == NULL) { perror("main"); } for (int i = 0; i < 3; i++) { *(p + i) = i; } free(p); p = NULL; return 0; }
5.2.2 calloc
// 原型
void* calloc (size_t num, size_t size);
// 参数:num 申请元素个数
// size 申请元素大小
// 功能:申请动态内存
- 函数的功能是为
num
个大小为size
的元素开辟一块空间,并且把空间的每个字节初始化为0
- 与函数
malloc
的区别只在于calloc
会返回地址之前把申请的空间的每个字节初始化为0
,如下图
- 使用
#include <stdio.h> #include <stdlib.h> int main() { int* p = (int*)calloc(3, sizeof(int)); // 申请3个int大小的堆区内存空间 if (p == NULL) { perror("main"); } for (int i = 0; i < 3; i++) { *(p + i) = i; } free(p); p = NULL; return 0; }
5.2.3 realloc
// 原型
void* realloc (void* ptr, size_t size);
// 参数:ptr -- 指向由malloc calloc realloc创建的动态内存块
// size -- 内存块新的大小 单位byte
// 返回值:调整之后的内存块地址
// 功能:调正ptr指向的内存块大小
-
realloc函数调整内存空间时存在3种情况
- 情况1:原有空间后面有够大的空间 — 直接在原有空间后面再开辟一块指定大小的空间
- 情况2:原有空间后面的空间不够大 — 在动态内存中寻找足够大的空间,并开辟,把原有空间的数据拷贝到新空间中,并将原堆区空间释放
- 情况3:在堆区空间并没有找到足够大的空间用来调整原动态空间的大小,此时函数返回
NULL
所以:realloc
函数返回的void*
空指针值,可能还是被调整的动态空间的指针,也可能是新的动态空间的地址,也可能是NULL
-
使用1
int* p = (int*)malloc(3 * sizeof(int)); // 申请3个int大小的堆区内存空间 // 当内存发现不够用时,使用realloc函数调整动态内粗空间的大小 p = realloc(p, 5 * sizeof(int)); // × // 能否直接使用 p 来接受该函数的返回值呢? // 答案:不能 // 如果调整空间大小失败,即在堆区并没有找到足够大的空闲空间 // 在该函数返回NULL,如果使用p接受返回值,此时 p=NULL // 导致 p 不能找到原来内存空间,本来 p 指向的内存空间的数据丢失 // 所以应该创建一个临时指针变量用来接受返回值,再判断是否为NULL // 如果为NULL,则调正空间失败,另作打算;如果不为NULL,再将临时指针变量复制给p // 以保证动态内存空间操作的一致性 // 代码应如下: int* ptr = realloc(p, 5 * sizeof(int)); if (ptr != NULL) { p = ptr; }
-
使用2
#include <stdio.h> #include <stdlib.h> int main() { int* p = (int*)realloc(NULL, 3 * sizeof(int)); // ptr=NULL 直接在堆区开辟12字节空间 // 等价于 // int* p = (int*)malloc(3 * sizeof(int)); return 0; }
5.2.4 free
// 原型
void free (void* ptr);
// 参数:需要释放的空间的地址
// 功能:动态内存的释放和回收
- 如果参数
ptr
指向的空间不是动态开辟的,free
函数的行为是未定义的 - 如果参数
ptr
是NULL
指针,则函数什么也不做
在使用
free
释放动态内存空间之后,需要再将指向动态内存的指针置为NULL
。原因如下:第一点,如果指针变量仍然指向那块内存空间,但是那块内存空间已经释放,对该指针变量操作必然会引发异常;第二点,置为NULL
之后可以避免多次释放动态内存空间,因为当free
函数的参数为NULL
时,什么也不做;第三点,free
函数释放内存空间,并不会将指针变量置为NULL
,况且传递的是指针变量,也不是指针变量的地址。
5.3 常见的动态内存错误
5.3.1 对NULL指针的解引用操作
int main()
{
int* p = (int*)malloc(4); // 开辟4byte动态空间 如果开辟动态内存失败
*p = 20; // 则对 p 解引用操作 会引发异常
free(p);
return 0;
}
5.3.2 对动态开辟空间的越界访问
int main()
{
int* p = (int*)malloc(4); // 开辟4byte动态空间
if (p == NULL)
{
perror("test");
}
*(p + 1) = 20; // p+1 已经在访问开辟的动态空间之后的内存 造成越界访问
free(p);
return 0;
}
5.3.3 对非动态开辟内存使用free释放
int main()
{
int a = 7; // a在栈区开辟空间 并不是动态内存
int* p = &a;
free(p);
return 0;
}
5.3.4 使用free释放一块动态开辟内存的一部分
int main()
{
int* p = (int*)malloc(8);
if (p == NULL)
{
perror("test");
}
p++;
free(p); // 此时p已经不再指向开辟内存空间的首地址
return 0;
}
5.3.5 对同一块动态内存多次释放
int main()
{
int* p = (int*)malloc(100);
if (p == NULL)
{
perror("test");
}
*p = 100;
free(p); // 第一次释放
free(p); // 第二次释放
return 0;
}
// free(p) 释放之后 将p置为NULL 可以避免多次释放
5.3.6 动态开辟内存忘记释放(内存泄漏)
#include <stdio.h>
#include <stdlib.h>
void test()
{
int* p = (int*)malloc(sizeof(int));
if (p==NULL)
{
perror("test");
}
*p = 100;
}
int main()
{
test();
//while (1); // 模拟服务器程序(程序上线持续运行)
return 0;
}
5.4 经典笔试题
指出代码段中存在的问题并修正。
5.4.1 练习1
#include <stdio.h>
#include <stdlib.h>
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world!");
printf(str);
}
int main()
{
Test();
return 0;
}
存在问题:① 第12行
GetMemory(str);
传参是值传递,GetMemory
函数内的p
只是str
的一份临时拷贝,并不会影响Test
函数内str
变量的值,所以GetMemory(str);
执行之后,str
值依然是NULL
,strcpy(str, "hello world!");
向空指针拷贝字符串则会引发异常。② 第六行p = (char*)malloc(100);
向堆区申请一块内存空间,但是并没有释放该空间,造成内存泄漏。
- 修改如下:
-
方法一
void GetMemory(char** p) { *p = (char*)malloc(100); // 修改str变量的值 } void Test(void) { char* str = NULL; GetMemory(&str); // 将str的地址传递给GetMemory函数 strcpy(str, "hello world!"); printf(str); free(str); // 释放向堆区申请的内存空间 str = NULL; } int main() { Test(); return 0; }
-
方法二
char* GetMemory(char* p) { p = (char*)malloc(100); return p; // 将向堆区申请的空间地址返回 } void Test(void) { char* str = NULL; str = GetMemory(str); // str为GetMemory函数内向堆区申请的地址空间 strcpy(str, "hello world!"); printf(str); free(str); // 释放向堆区申请的内存空间 str = NULL; } int main() { Test(); return 0; }
-
5.4.2 练习2
#include <stdio.h>
#include <stdlib.h>
char* GetMemory()
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
存在问题:
GetMemory
函数内在栈区开辟一块内存空间,用来存放字符串"hello world"
,并将字符串地址赋值给变量p
,但是函数返回之后,在堆区开辟的内存空间释放,尽管通过p
将那块空间地址返回给str
变量,但那块空间已被操作系统接管,不属于该程序,则造成非法访问。返回栈空间地址的问题。
5.4.3 练习3
#include <stdio.h>
#include <stdlib.h>
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
int main()
{
Test();
return 0;
}
存在问题:在
GetMemory
函数中*p = (char*)malloc(num);
向堆区申请的内存空间使用之后并没有释放,造成内存泄漏。
- 修改如下
void GetMemory(char** p, int num) { *p = (char*)malloc(num); } void Test(void) { char* str = NULL; GetMemory(&str, 100); strcpy(str, "hello"); printf(str); free(str); str = NULL; } int main() { Test(); return 0; }
5.4.4 练习4
#include <stdio.h>
#include <stdlib.h>
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
存在问题:第8行
free(str);
将char* str = (char*)malloc(100);
向堆区申请的内存空间释放,归还给操作系统,所以之后不能再访问str
指向的内存空间。但是free(str);
并不会将str
置为NULL
,所以if
语句中的条件判断为真,对str
进行操作,造成异常。
- 修改如下
#include <stdio.h> #include <stdlib.h> void Test(void) { char* str = (char*)malloc(100); strcpy(str, "hello"); free(str); str = NULL; // 将str置为NULL if语句判断为假 则不会对str再进行操作 if (str != NULL) { strcpy(str, "world"); printf(str); } } int main() { Test(); return 0; }
5.5 柔性数组(或伸缩性数组成员)
C99中,结构体中的最后一个元素允许是未知大小的数组,称为柔性数组成员。
例如:(有些编译器可能只支持写法1和写法2中的一种写法)
-
写法1
struct fa { int i; int a[0]; // 柔性数组成员 };
-
写法2
struct fa { int i; int a[]; // 柔性数组成员 };
5.5.1 柔性数组的特点
-
结构体中柔性数组成员前面必须有至少一个其他成员
-
sizeof
返回的这种结构大小不包括柔性数组的内存#include <stdio.h> struct S { int n; int a[]; // 柔性数组 }; int main() { printf("%d\n", sizeof(struct S)); // 4 return 0; } // 输出结果 // 4 // 结构体S的大小仅仅是结构体成员n的大小 而不包括a[]
-
包含柔性数组成员的结构用
malloc()
函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小
5.5.2 使用实例
#include <stdlib.h>
#include <stdio.h>
struct S
{
int n;
int arr[0]; // 柔性数组成员
};
int main()
{
// 期望arr是4个整形的数组
struct S* ps = (struct S*)malloc(sizeof(struct S) + sizeof(int) * 4);
if (ps==NULL)
{
perror("空间开辟失败");
}
ps->n = 4; // 存放数组arr的容量大小
for (int i = 0; i < 4; i++)
{
ps->arr[i] = i;
}
// 增加数组容量 8个整形的数组
struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + sizeof(int) * 8);
if (ptr == NULL)
{
perror("增容失败");
}
ps = ptr;
// 使用
ps->n = 8;
for (int i = 4; i < 8; i++)
{
ps->arr[i] = i;
}
// 打印数组数据
for (int i = 0; i < 8; i++)
{
printf("%d ", ps->arr[i]);
}
// 释放内存空间
free(ps);
ps = NULL;
return 0;
}
5.5.3 使用指针模拟柔性数组
#include <stdlib.h>
#include <stdio.h>
struct S
{
int n;
int* pa; // 数组指针
};
int main()
{
// 结构体开辟空间
struct S* ps = (struct S*)malloc(sizeof(struct S));
if (ps == NULL)
{
return 1;
}
// 为结构体中数组开辟空间,pa指向数组地址(堆区开辟的内存空间地址)
ps->pa = (int*)malloc(sizeof(int) * 3);
if (ps->pa == NULL)
{
return 1;
}
// 使用
// coding...
// 增加数组容量 存放6个整形
int* ptr = (int*)malloc(sizeof(int) * 6);
if (ptr == NULL)
{
return 1;
}
ps->pa = ptr;
// 使用
// coding...
// 释放内存
free(ps->pa); // 释放数组内存
ps->pa = NULL;
free(ps); // 释放结构体内存
ps = NULL;
return 0;
}
5.5.4 柔性数组的优势
使用指针模拟同样可以实现柔性数组相同的功能,那柔性数组有什么优势呢?
- 方便内存释放 — 使用柔性数组,给结构体分配内存,一次申请,一次释放。
- 有利于访问速度 — 柔性数组,堆区申请的内存空间是连续的,有益于提高访问速度,且减少内存碎片。(影响不大)
其他:
C语言结构体里的成员数组和指针 | 酷 壳 - CoolShell