前面我们学习了:
数组
函数
指针
结构体
之前创建变量时,通常都是直接写:
int a;
int arr[10];
struct Student s;这些变量的大小通常在程序编译时就已经确定。
但是有时候我们并不知道程序运行时到底需要多少内存。
例如:
用户输入需要保存多少个整数?
学生到底有多少人?
文件有多大?
字符串到底有多长?
这时候就需要使用:
动态内存分配。
一、什么是动态内存
普通变量:
int a;程序运行时,会自动为 a 分配内存。
而动态内存是:
程序运行
↓
需要多少内存
↓
运行时申请
↓
使用
↓
不需要了
↓
释放
这就是:
动态内存分配。
C 语言主要使用下面几个函数:
malloc
calloc
realloc
free
二、栈内存和堆内存
学习动态内存之前,先认识两个概念。
栈(Stack)
例如:
int a = 10;
void test() {
int x = 20;
}这种普通局部变量通常使用栈空间。
可以简单理解为:
进入函数
↓
创建局部变量
↓
使用
↓
函数结束
↓
自动释放
例如:
void test() {
int x = 10;
}test() 结束后,x 就不再存在。
三、堆(Heap)
动态申请的内存通常来自堆。
例如:
int *p = malloc(sizeof(int));可以理解成:
程序运行
↓
malloc
↓
从堆中申请内存
↓
返回地址
↓
p 保存地址
这块内存不会因为普通函数结束就自动释放。
需要手动:
free(p);四、为什么需要动态内存
假设我们需要保存 100 个整数。
最简单的方法:
int arr[100];但是如果用户运行程序后输入:
需要 100000 个整数
那么程序就不能继续使用:
int arr[100];因为大小已经写死了。
可以使用动态内存:
int n;
scanf("%d", &n);
int *arr = malloc(sizeof(int) * n);此时:
n = 10
↓
申请 10 个 int
n = 100
↓
申请 100 个 int
n = 100000
↓
申请 100000 个 int
这就是动态内存的意义。
五、malloc
malloc 是最常用的动态内存分配函数。
需要:
#include <stdlib.h>基本格式:
malloc(字节数);例如:
int *p = malloc(sizeof(int));这里申请了足够存放一个 int 的内存。
六、malloc 返回什么
这是最重要的地方之一。
malloc(sizeof(int))返回的是:
这块内存的地址。
所以可以:
int *p = malloc(sizeof(int));也就是:
malloc
↓
申请内存
↓
得到地址
↓
p 保存地址
可以理解成:
p
↓
┌──────────┐
│ int 内存 │
└──────────┘
七、malloc + sizeof
不要直接假设 int 一定是 4 字节。
推荐:
int *p = malloc(sizeof(int));而不是:
int *p = malloc(4);因为:
sizeof(int)会自动得到当前平台的 int 大小。
八、申请多个元素
例如申请 10 个 int:
int *p = malloc(sizeof(int) * 10);可以理解成:
p
↓
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
│int │int │int │int │int │int │int │int │int │int │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘
这和数组:
int arr[10];在使用上有很多相似之处。
例如:
p[0] = 10;
p[1] = 20;
p[2] = 30;九、动态数组
一个完整例子:
#include <stdio.h>
#include <stdlib.h>
int main() {
int n;
printf("请输入数量: ");
scanf("%d", &n);
int *arr = malloc(sizeof(int) * n);
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
}
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
free(arr);
return 0;
}例如输入:
5
输出:
1 2 3 4 5
这里:
int *arr = malloc(sizeof(int) * n);就创建了一个运行时决定大小的“数组”。
十、malloc 申请失败
malloc 不一定总能成功。
如果申请失败,它会返回:
NULL例如:
int *p = malloc(sizeof(int) * 100);
if (p == NULL) {
printf("内存申请失败\n");
return 1;
}因此推荐:
int *p = malloc(sizeof(int));
if (p == NULL) {
printf("malloc failed\n");
return 1;
}十一、NULL 是什么
NULL 可以理解为:
空指针。
例如:
int *p = NULL;表示:
p 目前没有指向有效对象
所以在使用 malloc 后,经常写:
if (p == NULL) {
...
}检查申请是否成功。
十二、使用动态内存
例如:
int *p = malloc(sizeof(int));
if (p == NULL) {
return 1;
}
*p = 100;
printf("%d\n", *p);
free(p);这里把之前学的指针全部串起来了。
十三、malloc 的完整流程
动态内存最重要的流程:
申请
↓
malloc
↓
检查 NULL
↓
使用
↓
free
例如:
int *p = malloc(sizeof(int));
if (p == NULL) {
return 1;
}
*p = 100;
printf("%d\n", *p);
free(p);可以记成:
malloc → 使用 → free
十四、free
申请了动态内存以后,使用结束就需要释放:
free(p);例如:
int *p = malloc(sizeof(int));
*p = 100;
printf("%d\n", *p);
free(p);free() 的作用:
释放之前动态申请的那块内存。
十五、为什么需要 free
假设:
void test() {
int *p = malloc(sizeof(int) * 1000);
}函数结束以后:
p 这个指针变量消失
但是:
malloc 得到的堆内存
仍然可能没有被释放。
这就产生了:
内存泄漏。
十六、内存泄漏
例如:
int *p = malloc(sizeof(int));
p = NULL;这里原来的地址丢掉了。
变成:
malloc
↓
一块内存
↓
p 指向它
p = NULL
原来的地址丢失
↓
没人能找到这块内存
这就是典型的内存泄漏。
正确方式:
int *p = malloc(sizeof(int));
free(p);
p = NULL;十七、free 后指针会怎样
看:
int *p = malloc(sizeof(int));
*p = 100;
free(p);注意:
free(p)
释放的是:
p 指向的内存。
并不是让:
p
这个指针变量自动变成 NULL。
所以 p 可能仍然保存着原来的地址。
十八、野指针 / 悬空指针
例如:
int *p = malloc(sizeof(int));
*p = 100;
free(p);
printf("%d\n", *p);这是错误的。
因为:
p
↓
原来的内存
↓
已经 free
此时 p 还保存着地址,但那块内存已经不能再使用。
这种情况通常称为:
悬空指针(dangling pointer)
也常被归入大家常说的“野指针”问题。
十九、free 后置 NULL
一个常见的安全习惯:
free(p);
p = NULL;这样:
free
↓
释放内存
p = NULL
↓
不再指向旧地址
之后可以检查:
if (p == NULL) {
printf("p 没有指向有效内存\n");
}二十、不能重复 free
例如:
int *p = malloc(sizeof(int));
free(p);
free(p);这是错误的。
同一块动态内存不能随意释放两次。
这种问题叫:
double free
更安全的写法:
free(p);
p = NULL;第二次如果:
free(p);因为 p 已经是
NULL,通常不会再次释放刚才那块内存。
不过更重要的是:
管理好每块内存的所有权和生命周期。
二十一、动态内存与数组
例如:
int *arr = malloc(sizeof(int) * 5);可以像数组一样:
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;也可以使用指针:
*(arr + 0) = 10;
*(arr + 1) = 20;
*(arr + 2) = 30;这和之前学习的数组、指针完全联系起来了。
因为:
arr[i]本质上就是:
*(arr + i)二十二、malloc + for
例如动态创建一个数组:
int n = 5;
int *arr = malloc(sizeof(int) * n);
if (arr == NULL) {
return 1;
}
for (int i = 0; i < n; i++) {
arr[i] = (i + 1) * 10;
}
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
free(arr);输出:
10 20 30 40 50
二十三、calloc
除了 malloc,还有:
calloc基本格式:
calloc(元素数量, 每个元素大小);例如:
int *arr = calloc(5, sizeof(int));表示:
5 个 int
和:
malloc(sizeof(int) * 5)类似。
一个重要区别是:
calloc 分配的内存会被初始化为零。
例如:
int *arr = calloc(5, sizeof(int));通常可以直接得到:
0 0 0 0 0
二十四、malloc 和 calloc
可以简单记:
malloc
↓
申请内存
↓
内容未初始化
calloc
↓
申请内存
↓
初始化为 0
例如:
int *a = malloc(sizeof(int) * 5);里面原本是什么值,不应该依赖。
而:
int *b = calloc(5, sizeof(int));得到的元素会初始化为 0。
二十五、realloc
如果已经申请了一块动态内存,但是后来发现:
不够用了
可以使用:
realloc例如:
int *p = malloc(sizeof(int) * 5);
p = realloc(p, sizeof(int) * 10);原本:
5 个 int
变成:
10 个 int
二十六、realloc 的作用
可以理解成:
malloc
↓
申请 5 个元素
使用
发现不够
↓
realloc
↓
调整空间大小
它可以:
扩大
也可以:
缩小
动态数组经常会使用它。
二十七、realloc 的一个重要问题
不要直接习惯性写:
p = realloc(p, new_size);因为如果 realloc 失败:
realloc 返回 NULL
那么原来的地址可能就丢失了。
更安全的写法:
int *temp = realloc(p, new_size);
if (temp == NULL) {
printf("realloc failed\n");
} else {
p = temp;
}也就是:
旧指针 p
↓
realloc
↓
临时指针 temp
↓
检查成功
↓
p = temp
这是一种很重要的动态内存编程习惯。
二十八、realloc 示例
#include <stdio.h>
#include <stdlib.h>
int main() {
int *arr = malloc(sizeof(int) * 5);
if (arr == NULL) {
return 1;
}
for (int i = 0; i < 5; i++) {
arr[i] = i + 1;
}
int *temp = realloc(arr, sizeof(int) * 10);
if (temp == NULL) {
free(arr);
return 1;
}
arr = temp;
for (int i = 5; i < 10; i++) {
arr[i] = i + 1;
}
for (int i = 0; i < 10; i++) {
printf("%d ", arr[i]);
}
free(arr);
return 0;
}输出:
1 2 3 4 5 6 7 8 9 10
二十九、动态内存和结构体
现在把上一章的结构体接进来。
例如:
struct Student {
char name[50];
int age;
double score;
};可以动态申请一个:
struct Student *p;然后:
p = malloc(sizeof(struct Student));这样就在堆上申请了一个 Student 大小的空间。
三十、结构体 + malloc
完整例子:
#include <stdio.h>
#include <stdlib.h>
struct Student {
char name[50];
int age;
double score;
};
int main() {
struct Student *p = malloc(sizeof(struct Student));
if (p == NULL) {
return 1;
}
p->age = 18;
p->score = 95.5;
printf("age = %d\n", p->age);
printf("score = %.2f\n", p->score);
free(p);
p = NULL;
return 0;
}这里同时使用了:
结构体
+
结构体指针
+
malloc
+
free
+
->
三十一、动态创建结构体数组
还可以一次申请多个结构体:
struct Student *students =
malloc(sizeof(struct Student) * 100);表示:
100 个 Student
然后:
students[0].age = 18;
students[1].age = 19;
students[2].age = 20;或者:
(students + 0)->age = 18;
(students + 1)->age = 19;这里又和之前的:
数组
指针
结构体
全部连接起来了。
三十二、typedef + malloc
如果之前使用了:
typedef struct {
char name[50];
int age;
double score;
} Student;那么可以写:
Student *p = malloc(sizeof(Student));而不是:
struct Student *p = malloc(sizeof(struct Student));例如:
Student *p = malloc(sizeof(Student));
if (p == NULL) {
return 1;
}
p->age = 18;
free(p);
p = NULL;三十三、动态字符串
动态内存不只可以保存数字。
例如字符串:
char *str = malloc(sizeof(char) * 100);然后:
scanf("%99s", str);使用结束:
free(str);也可以写:
char *str = malloc(100);因为 char 通常占 1 字节。
但更加推荐明确表达意图:
char *str = malloc(sizeof(char) * 100);三十四、动态二维数组
简单情况下,也可以申请一整块连续内存:
int rows = 3;
int cols = 4;
int *arr = malloc(sizeof(int) * rows * cols);访问:
arr[i * cols + j]例如:
arr[0 * cols + 0]
arr[0 * cols + 1]
arr[1 * cols + 0]可以把二维数据放在一维的连续内存里。
后面学习更复杂的数据结构时,这种思想会非常常见。
三十五、sizeof 与动态内存
例如:
int *p = malloc(sizeof(int) * 10);注意:
sizeof(p)和:
sizeof(*p)不是一回事。
p 是指针:
sizeof(p)得到的是:
指针本身的大小。
而:
sizeof(*p)得到的是:
p 指向的数据类型大小。
例如:
int *p;那么:
sizeof(p)是指针大小。
而:
sizeof(*p)是 int 的大小。
因此动态数组申请时:
malloc(sizeof(*p) * n);也是一种很实用的写法。
三十六、动态内存和普通数组的区别
普通数组:
int arr[10];特点:
大小通常在定义时确定
生命周期由作用域等规则管理
不需要手动 free
动态数组:
int *arr = malloc(sizeof(int) * n);特点:
大小可以运行时决定
生命周期由程序员管理
需要 free
可以简单理解:
普通数组
↓
编译/定义时决定大小
动态内存
↓
运行时决定大小
三十七、常见错误
1. 没有检查 malloc
不推荐:
int *p = malloc(sizeof(int));
*p = 10;更安全:
int *p = malloc(sizeof(int));
if (p == NULL) {
return 1;
}
*p = 10;2. 使用未初始化的 malloc 内存
例如:
int *p = malloc(sizeof(int));
printf("%d\n", *p);不能依赖这里是什么值。
应该先写入:
*p = 10;再读取。
3. 忘记 free
例如:
int *p = malloc(sizeof(int));
*p = 10;如果不再使用,却没有:
free(p);就可能导致内存泄漏。
4. free 后继续使用
错误:
free(p);
printf("%d\n", *p);释放后不要继续访问。
5. 重复 free
错误:
free(p);
free(p);不要重复释放同一块内存。
6. 越界
例如:
int *p = malloc(sizeof(int) * 5);合法:
p[0]
p[1]
p[2]
p[3]
p[4]错误:
p[5]因为你只申请了 5 个元素。
三十八、动态内存的生命周期
可以把动态内存理解成:
申请
↓
malloc / calloc
↓
检查
↓
使用
↓
realloc(需要调整时)
↓
继续使用
↓
free
↓
结束
例如:
int *p = malloc(sizeof(int) * 10);
if (p == NULL) {
return 1;
}
/* 使用 p */
int *temp = realloc(p, sizeof(int) * 20);
if (temp != NULL) {
p = temp;
}
/* 继续使用 p */
free(p);
p = NULL;三十九、一个完整的小程序
实现:
用户输入人数
↓
动态创建学生数组
↓
输入学生信息
↓
输出学生信息
↓
释放内存
代码:
#include <stdio.h>
#include <stdlib.h>
typedef struct {
char name[50];
int age;
double score;
} Student;
int main() {
int n;
printf("请输入学生数量: ");
scanf("%d", &n);
Student *students =
malloc(sizeof(Student) * n);
if (students == NULL) {
printf("内存申请失败\n");
return 1;
}
for (int i = 0; i < n; i++) {
printf("请输入第 %d 个学生的信息:\n", i + 1);
printf("姓名: ");
scanf("%49s", students[i].name);
printf("年龄: ");
scanf("%d", &students[i].age);
printf("成绩: ");
scanf("%lf", &students[i].score);
}
printf("\n学生信息:\n");
for (int i = 0; i < n; i++) {
printf(
"%s %d %.2f\n",
students[i].name,
students[i].age,
students[i].score
);
}
free(students);
students = NULL;
return 0;
}这个例子已经把:
数组
指针
结构体
函数
动态内存
开始真正组合起来了。
四十、本章核心知识
这一章最需要掌握的不是所有函数,而是下面这条思路:
指针保存地址
↓
malloc 在运行时申请内存
↓
指针指向这块内存
↓
通过指针使用内存
↓
不需要了
↓
free 释放内存
最核心的代码:
int *p = malloc(sizeof(int));
if (p == NULL) {
return 1;
}
*p = 100;
printf("%d\n", *p);
free(p);
p = NULL;以及动态数组:
int *arr = malloc(sizeof(int) * n);四十一、练习
练习 1
使用 malloc 动态申请一个 int:
输入一个数字
↓
保存到动态内存
↓
输出
↓
free
练习 2
让用户输入 n:
n = ?
然后动态申请:
n 个 int
使用 for 输入所有数字,再全部输出。
练习 3
使用:
calloc动态创建 10 个 int,观察它们初始值。
练习 4
使用:
realloc先创建 5 个整数,再扩展成 10 个整数。
练习 5
写一个:
void double_array(int *p, int n)动态申请数组后:
1 2 3 4 5
调用函数变成:
2 4 6 8 10
练习 6
使用:
typedef struct定义 Student。
然后动态申请一个学生:
Student *p = malloc(sizeof(Student));通过:
p->age修改年龄。
最后:
free(p);练习 7
动态创建 n 个学生:
Student *students =
malloc(sizeof(Student) * n);输入并输出全部学生信息。
这是本章最重要的综合练习之一。
Comments NOTHING