目录
- 1.动态内存函数的介绍
- 1.malloc
- 2.free
- 2.calloc
- 4.realloc
- 2.常见的动态内存错误
- 3.C/C++程序的内存开辟
- 4.柔性数组
- 1.是什么?
- 2.柔性数组的特点
- 3.柔性数组的使用
- 4.柔性数组的优势
1.动态内存函数的介绍
1.malloc
- 函数原型:
void* malloc(size_t size)
- 功能:
malloc()
向内存申请一块连续可用的空间,并返回指向这块空间的指针
- 返回值:
- 如果开辟成功,则返回一个指向开辟好空间的指针
- 如果开辟失败,则返回一个NULL指针
- 因此malloc的返回值一定要做检查
- 返回值的类型是
void*
,所以**malloc()
并不知道开辟空间的类型**,具体在使用的时候使用者自己来决定(强制类型转换) - 如果参数
size == 0
,malloc()
的行为是标准是未定义的,取决于编译器
2.free
- 函数原型:
void free(void* ptr)
- 功能:
free()
用来释放动态开辟的内存 - 如果参数
ptr
指向的空间不是动态开辟的,那free()
的行为是未定义的 - 如果参数
ptr
是NULL
,则函数什么事都不做 - 一次完整的动态内存开辟的例子
int* p = (int*)malloc(10 * sizeof(int));
if(p == NULL)
{perror("main"); // main: xxxxreturn 0;
}// 使用
for(int i = 0; i < 10; i++)
{*(p + i) = i;
}free(p); // 回收空间
p = NULL; // 手动把p置为NULL
2.calloc
- 函数原型:
void* calloc(size_t num, size_t size)
- 功能:为
num
个大小为size
的元素开辟一块空间,并且把空间的每个字节初始化为0 - 与函数
malloc()
的区别只在于calloc()
会在返回地址之前把申请的空间的每个字节初始化为全0 - 如果对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务
4.realloc
-
函数原型:
void* realloc(void* ptr, size_t size)
-
功能:
realloc()
的出现让动态内存管理更加灵活- 有时会发现过去申请的空间太小了,有时候又会觉得申请的空间过大了,一定会对内存的大小做灵活的调整,那
realloc()
就可以做到对动态开辟内存大小的调整
-
参数:
ptr
:要调整的内存地址- 若
ptr == NULL
,则realloc()
作用同malloc()
- 若
size
:调整之后新大小
-
返回值:调整之后的内存起始位置
-
注意:这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
-
realloc()
在调整内存空间时,存在两种情况- 原有空间之后有足够大的空间
- 要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化
- 原有空间之后没有足够大的空间
- 扩展方法:
- 在堆空间上另找一个合适大小的连续空间来使用
- 这样函数返回的是一个新的内存地址
- 扩展方法:
- 由于上述的两种情况,
realloc()
的使用就要注意一些
- 原有空间之后有足够大的空间
-
例如:为了确保内存空间成功开辟,拿一个临时指针去接收新开辟的空间的地址,再赋值,这样保证了万一没有成功开辟新的地址,而丢失了原来的地址
int* p = (int*)calloc(10, sizeof(int));
if(p == NULL)
{perror("main");return 1;
}for(int i = 0; i < 10; i++) // 使用
{*(p + i) = i;
}// 这里需要p指向更大空间,realloc调整
int* ptr = (int*)realloc(p, 20 * sizeof(int));
if(ptr != NULL)
{p = ptr;
}free(p);
p = NULL;
2.常见的动态内存错误
- 对NULL指针的解引用操作
- 对动态开辟空间的越界访问
- 使用free释放非动态开辟的空间
- 使用free释放动态内存中的一部分
- 对同一块动态开辟的空间,多次释放
- 动态开辟的空间忘记释放- 内存泄漏 -> 比较严重
- 经典例子一:
str
传给GetMemory()
的时候是值传递,所以GetMemory()
的形参p
是str
的一份临时拷贝- 在
GetMemory()
内部动态申请空间的地址,存放在p
中,不会影响外边的str
,所以当GetMemory()
返回之后,str
依然是NULL
,所以strcpy()
会失败 - 当
GetMemory()
返回之后,形参p
销毁,使得动态开辟的100个字节存在内存泄漏无法释放
void GetMemory(char* p)
{p = (char *)malloc(100);
}void Test(void)
{char* str = NULL;GetMemory(str);strcpy(str, "hello world");
}int main()
{Test();return 0;
}// 改进一
char* GetMemory(char* p)
{p = (char *)malloc(100);return p;
}void Test(void)
{char* str = NULL;str = GetMemory(str);strcpy(str, "SnowK");free(str);str = NULL:
}// 改进二
void GetMemory(char** p)
{*p = (char *)malloc(100);
}void Test(void)
{char* str = NULL;GetMemory(&str);strcpy(str, "SnowK");free(str);str = NULL:
}
- 经典例子二:
GetMemory()
内部创建的数组是在栈区上创建的- 出了函数,
p[]
的空间就还给了操作系统 - 返回的地址是没有实际的意义,如果通过返回的地址去访问内存,就是非法访问内存
char* GetMemory()
{char p[] = "SnowK";return p;
}void Test()
{char* str = NULL;str = GetMemory();
}
3.C/C++程序的内存开辟
- C/C++程序内存分配的几个区域:
- 栈区(stack):
- 在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放
- 栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限
- 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等
- 堆区(heap):
- 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收
- 分配方式类似于链表
- 数据段(静态区)(static):
- 存放全局变量、静态数据程序
- 结束后由系统释放
- 代码段:存放函数体(类成员函数和全局函数)的二进制代码
- 实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁
- 但是被
static
修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁,所以生命周期变长
- 栈区(stack):
4.柔性数组
1.是什么?
struct s
{int n;int arr[]; // 大小是未知
}
2.柔性数组的特点
- 结构中的柔性数组成员前面必须至少一个其他成员
sizeof
返回的这种结构大小不包括柔性数组的内存- 包含柔性数组成员的结构用
malloc()
函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小
typedef struct st_type
{int i;int arr[0]; // 柔性数组成员
}type_a;printf("%d\n", sizeof(type_a)); // 输出的是4
3.柔性数组的使用
// 期望arr的大小是10个整形
struct S* ps = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(int));
ps->n = 10;for(int i = 0; i < 10; i++)
{ps->arr[i] = i;
}// 增加
struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 20 * sizeof(int));
if(ptr != NULL)
{ps = ptr;
}// 使用
// ...
// 释放
free(ps);
ps = NULL;
4.柔性数组的优势
- 方便内存释放
- 如果代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户
- 用户调用
free()
可以释放结构体,但是用户并不知道这个结构体内的成员也需要free()
,所以你不能指望用户来发现这个事 - 所以,如果把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次
free()
就可以把所有的内存也给释放掉
- 这样有利于访问速度
- 连续的内存有益于提高访问速度,也有益于减少内存碎片
- 其实,个人觉得也没高多少,反正也避免不了要用做偏移量的加法来寻址
- 连续的内存有益于提高访问速度,也有益于减少内存碎片