深入探究C语言内存分配系列函数
深入探究C语言内存分配系列函数
目录
操作系统负责管理内存的分配和释放。当运行一个程序时,操作系统会为它分配一块内存空间;当程序结束时,操作系统会回收这块内存。
内存是什么?
内存是计算机中用来存储和访问数据的硬件设备。内存像是一个“临时工作台”,计算机在运行程序时,所有的数据和指令都需要先加载到内存中,才能被 CPU 快速访问和处理。
了解更多
内存分配的三种方式
- 静态存储区分配
- 静态存储区中的变量在程序启动时由程序分配内存,在程序结束时释放内存,无需动态管理。
- 生命周期随程序的结束而结束。
如:全局变量,静态变量,常量。
- 栈空间分配
- 栈空间是管理函数调用和局部变量的一种内存区域。它的分配和释放是由编译器自动管理的,遵循“后进先出”的原则。
- 栈空间的特点:分配和释放速度非常快,但容量有限
- 函数调用结束后自动释放。
- 堆空间分配
动态内存开辟,malloc
、calloc
、realloc
。
- 堆空间:程序运行时用于动态分配内存的一种内存区域。
- 堆空间的分配和释放由程序员手动管理的。
- 堆空间的特点:容量较大,但分配和释放速度较慢。
为什么要学习动态分配的系列函数?
在之前学习数组时,存在一个问题就是:要在函数开头先声明一个很大的数组,然后仅仅使用它的一小部分
后面发现C99及之后才可以用变量做数组定义的大小
(C99前我们需要在函数的最前面的区域对所有变量进行声明)
#include<stdio.h>
int main(void) {
int arr[1000] = { 0 };/*c99之前需要申请很大的一块空间*/
int N = 0;
int i = 0;
printf("请输入数组的大小\n");
scanf("%d", &N);
printf("请输入%d个数\n", N);
for (i = 0; i < N; i++)
scanf("%d", &arr[i]);
return 0;
}
#include<stdio.h>
int main(void) {
int N = 0;
int i = 0;
printf("请输入数组的大小\n");
scanf("%d", &N);
int arr[N] = { 0 };/*c99之后可以用变量定义数组的大小*/
printf("请输入%d个数\n", N);
for (i = 0; i < N; i++)
scanf("%d", &arr[i]);
return 0;
}
#include<stdio.h>
#include<stdlib.h>
int main(void) {
int N = 0;
int i = 0;
printf("请输入数组的大小\n");
scanf("%d", &N);
int* arr = (int*)malloc(sizeof(int) * N);/*c99之前可以用动态申请*/
printf("请输入%d个数\n", N);
for (i = 0; i < N; i++)
scanf("%d", &arr[i]);
return 0;
}
第一种显然造成了内存的浪费就不说了,但是你会说,我的IDE就是C99以上,我完全可以就用第二种变长数组实现。
不急,再仔细研究下
- 第二种(变长数组)
- 需要数组大小确定(N确定)
- 需要数组大小较小(内存分配方式:栈空间分配,由前置知识,容易栈溢出)
- 兼容性不好(仅支持C99及之后的编译器)
- 代码更简洁,自动分配和释放
- 可以初始化
- 第三种(动态内存分配)
- 不需要数组大小确定
- 适合数组较大的情况(内存分配方式:堆空间分配,由前置知识)
- 兼容性好,适合跨平台开发
- 需要手动管理内存,避免内存泄漏。(开辟完了要及时释放)
所以用那种方式需要结合业务需求,二三种方式都需要了解
四大内存分配函数
前置知识
void*
特点:- 无类型指针,不关联任何数据类型
- 不能直接解引用
- 在使用
void*
之前,通常需要将其转换为具体的指针类型。
- 关于内存泄漏
我们一次程序中可以申请的内存是有限的。- 如果只是平时写简单的程序,写完就关闭,退出去了,这时忘记了free的话,不会对任何人造成影响,因为操作系统有清除曾使用的内存的机制
- 如果是一个持续运行的服务器就不可以不
free()
了!万一堆区中所有的空间都被你申请了呢
malloc
与free
malloc
memery allocted:内存分配
- 头文件:
stdlib
- 原型:void* malloc(size_t size)
返回的是无类型 所以需要根据实际 强制类型转换 - 不初始化
- 返回值:
- 成功时,返回指向新分配内存的指针。为避免内存泄漏,必须用
free()
或realloc()
解分配返回的指针。 - 失败时,返回空指针
(NULL)
- 成功时,返回指向新分配内存的指针。为避免内存泄漏,必须用
- 参数:
size -
要分配的字节数
sizeof(数据类型)*N//N是通过变量、用户输入、文件读取等方式确定。
//(你可能觉得咋没对呢,之前不是说动态分配不需要数组大小确定吗)
//等会我们看realloc
- 使用前检查是否成功(是否为NULL),使用后需要释放
free
我们在声明一个指针时,一般把它初始化为0,也就是NULL。
这样做的好处是,如果我们在后面的程序中没有让这个指针指向一块具体的空间,这个指针不会是野指针,方便我们用来判断。比如if(p != NULL)
我们还知道,当malloc失败时返回的是 NULL
所以我们一开始写上free是好习惯,因为我们不知道我们会不会用到我们声明的指针,也不知道malloc能不能成功
这时候,free空指针就是有意义的了
- 头文件:
stdlib
- 原型:
void* free(void* ptr)
- 释放问题
- 只能释放由
malloc
、calloc
或realloc
分配的内存 - 不能重复释放,同一块内存只能释放一次
- 释放后应将指针置为 NULL
- 只能释放由
释放内存后,指针仍然指向原来的地址,但该地址已经无效。为了避免悬空指针(野指针),建议将指针置为 NULL。
#include <stdio.h>
#include <stdlib.h>
int main() {
int n;
printf("请输入数组的大小: ");
scanf("%d", &n);
// 分配内存
int* arr = (int*)malloc(n * sizeof(int));
if (arr == NULL) {
printf("内存分配失败\n");
return 1;
}//检查是否分配成功
// 输入数据
printf("请输入 %d 个整数: ", n);
for (int i = 0; i < n; i++) {
scanf("%d", &arr[i]);
}
// 输出数据
printf("您输入的数组是: ");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// 释放内存
free(arr);
arr = NULL; // 避免野指针
if(arr == NULL){
printf("已释放");
}//检验是否释放成功
return 0;
}
realloc
reallocate:重新分配内存
- 头文件:
stdlib
- 原型:
void* realloc(void* ptr, size_t size);
(void* ptr
是指向之前分配的内存块的指针) - 返回值:
- 成功时,返回指向新分配内存的指针。 (可能与
ptr
不同) - 失败时,返回
(NULL)
且原来的内存块保持不变
- 成功时,返回指向新分配内存的指针。 (可能与
- 扩展内存:
- 新的大小大于原来的大小,realloc 会尝试扩展内存块。
- 如果当前内存块后面有足够的连续空间,
realloc
会直接扩展内存块。 - 如果没有足够的连续空间,
realloc
会分配一个新的内存块,并将原来的数据复制到新内存块中,然后释放原来的内存块。
- 如果当前内存块后面有足够的连续空间,
- 新的大小大于原来的大小,realloc 会尝试扩展内存块。
- 缩小内存:
新的大小小于原来的大小,realloc 会缩小内存块。
多余的内存会被释放,数据会被保留。 - 释放内存:
如果size 为 0
,realloc 会释放原来的内存块(类似于free
),并返回NULL
。 - 分配新内存:
如果ptr
为NULL
,realloc
===malloc
。
#include <stdio.h>
#include <stdlib.h>
int main() {
int* arr = (int*)malloc(5 * sizeof(int));
if (arr == NULL) {
printf("内存分配失败\n");
return 1;
}
for (int i = 0; i < 5; i++) {
arr[i] = i + 1;
}
// 调整内存大小
int* new_arr = (int*)realloc(arr, 6 * sizeof(int));
if (new_arr == NULL) {
printf("内存重新分配失败\n");
free(arr); // 释放原来的内存
return 1;
}
// 初始化新增的内存
arr[5] = 6;
// 打印数组
printf("数组内容: ");
for (int i = 0; i < 6; i++) {
printf("%d ", new_arr[i]);
}
printf("\n");
// 释放内存
// free(new_arr);
// new_arr = NULL;等价于
int* newArr = (int*)realloc(arr, 0);
//检查是否成功释放
if(newArr == NULL){
printf("内存已释放");
}
return 0;
}
calloc
contiguous allocation:连续分配内存
- 头文件:
stdlib
- 原型:
void* calloc(size_t num, size_t size);
- 参数:
- num:需要分配的元素个数。
- size:每个元素的字节数
(int*)calloc(n, sizeof(int));
- 初始化所有字节为0
calloc
与malloc
类似,但它在分配内存的同时会将内存块中的所有字节初始化为 0。 - 性能稍慢(需要初始化内存)
#include <stdio.h>
#include <stdlib.h>
int main() {
int n = 5;
// 分配并初始化数组
int* arr = (int*)calloc(n, sizeof(int));
if (arr == NULL) {
printf("内存分配失败\n");
return 1;
}
// 打印数组(初始值为 0)
printf("数组内容: ");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// 释放内存
free(arr);
arr = NULL;
return 0;
}
Deepseek的总结
假设你有一个仓库,里面有很多货架。你需要存放一些箱子(数据)。
- malloc:
你告诉管理员:“我需要 5 个货架。”
管理员找到 5 个空闲的货架,并把它们的编号给你。
货架上的东西可能是乱七八糟的(未初始化)。 - calloc:
你告诉管理员:“我需要 5 个货架,并把它们清空。”
管理员找到 5 个空闲的货架,清空它们,并把编号给你。
货架上的东西是干净的(初始化为 0)。 - realloc:
假设你之前申请了 5 个货架,但现在需要 10 个货架。
你告诉管理员:“我需要把原来的 5 个货架扩展到 10 个。”
管理员检查原来的货架后面是否有足够的空闲空间:- 如果有,直接扩展。
- 如果没有,找一个更大的区域,把原来的箱子搬过去,并释放原来的货架。