C语言入门(二十八):动态内存管理(2)

目录

[4. 常⻅的动态内存的错误](#4. 常⻅的动态内存的错误)

[4.1 对NULL指针的解引⽤操作](#4.1 对NULL指针的解引⽤操作)

[4.2 对动态开辟空间的越界访问](#4.2 对动态开辟空间的越界访问)

[4.3 对⾮动态开辟内存使⽤free释放](#4.3 对⾮动态开辟内存使⽤free释放)

[4.4 使⽤free释放⼀块动态开辟内存的⼀部分](#4.4 使⽤free释放⼀块动态开辟内存的⼀部分)

[4.5 对同⼀块动态内存多次释放](#4.5 对同⼀块动态内存多次释放)

[4.6 动态开辟内存忘记释放(内存泄漏)](#4.6 动态开辟内存忘记释放(内存泄漏))

[5. 柔性数组](#5. 柔性数组)

[5.1 柔性数组的特点:](#5.1 柔性数组的特点:)

[5.2 柔性数组的使⽤](#5.2 柔性数组的使⽤)

[6. 总结C/C++中程序内存区域划分](#6. 总结C/C++中程序内存区域划分)


4. 常⻅的动态内存的错误

4.1 对NULL指针的解引⽤操作

错误的写法:

cpp 复制代码
错误的写法
#include <stdio.h>
#include <stdlib.h>
#include <limits.h> 


int main()
{
	int* p = (int*)malloc(INT_MAX);
	*p = 20; //如果不去判断p是否为空指针,则会报错

	return 0;
}

解析:

错误1:没有检查malloc返回值

问题

  • malloc(INT_MAX) 尝试分配非常大的内存

  • INT_MAX 通常是 2147483647(约2GB)

  • 很可能分配失败,返回 NULL

  • 如果 pNULL*p = 20 会导致 程序崩溃

正确的做法

cpp 复制代码
int* p = (int*)malloc(INT_MAX);
if (p == NULL) 
{
    perror("malloc失败");
    return 1;  // 退出程序
}
*p = 20;  // ✅ 安全

问题2 :程序退出时,这2GB内存没有使用free去释放

1. 内存泄漏的后果

复制代码
程序开始
  ↓
malloc申请2GB ← 内存被占用
  ↓
使用内存
  ↓
程序结束 ← ❌ 内存没有归还系统!

4.2 对动态开辟空间的越界访问

错误的写法:

cpp 复制代码
错误的写法
void test()
{
	int i = 0;
	int* p = (int*)malloc(10 * sizeof(int));
	if (NULL == p)
	{
		exit(EXIT_FAILURE);

	}
	for (i = 0; i <= 10; i++)
	{
		*(p + i) = i; //当i=10的时候,会越界访问
	}
	free(p);
}
int main()
{
	test();
	return 0;
}

解析:
主要问题:数组越界访问:

cpp 复制代码
for (i = 0; i <= 10; i++)
{
    *(p + i) = i;
}

问题分析:

  1. 分配了10个intmalloc(10 * sizeof(int))

  2. 有效索引:0, 1, 2, 3, 4, 5, 6, 7, 8, 9(共10个)

  3. 循环条件i <= 10

i = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 → 正常

i = 10 → ❌ 越界访问

内存示意图:

cpp 复制代码
分配的内存(10个int = 40字节):
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ p[0]│ p[1]│ p[2]│ p[3]│ p[4]│ p[5]│ p[6]│ p[7]│ p[8]│ p[9]│
│  0  │  1  │  2  │  3  │  4  │  5  │  6  │  7  │  8  │  9  │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
  ↑                             ↑                             ↑
安全访问                    安全访问                  ❌ 越界访问
(p+0 到 p+9)                                      (p+10 不存在)

实际内存布局

cpp 复制代码
p+0  → 地址1000: 存0
p+1  → 地址1004: 存1
...
p+9  → 地址1036: 存9
p+10 → 地址1040: ❌ 这里不属于你分配的内存!

越界访问的后果:
可能情况1:覆盖其他数据

cpp 复制代码
// 假设p+10指向另一个变量的内存
*(p + 10) = 10;  // 可能修改了其他变量的值

可能情况2:程序崩溃(段错误)

cpp 复制代码
// 如果p+10指向受保护的内存区域
*(p + 10) = 10;  // 触发段错误,程序崩溃

可能情况3:引发安全漏洞

  • 缓冲区溢出攻击就是利用这种越界访问

正确的写法:

方法1:使用 < 而不是 <=

cpp 复制代码
for (i = 0; i < 10; i++)  // ✅ 正确:0~9
{
    *(p + i) = i;
}

方法2:使用变量名代替硬编码

cpp 复制代码
int size = 10;
int* p = (int*)malloc(size * sizeof(int));
for (i = 0; i < size; i++)  // ✅ 清晰明了
{
    p[i] = i;  // 也可以用数组表示法
}

**代码的其他注意事项:

  1. exit(EXIT_FAILURE) 的使用**
cpp 复制代码
if (NULL == p)
{
    exit(EXIT_FAILURE);  // 立即终止程序
}

注意exit() 会终止整个程序 ,而不仅仅是 test() 函数。

一句话总结:

主要问题是数组越界访问malloc 分配了10个int的空间(索引0~9),但循环条件 i <= 10 会访问到第11个元素(索引10),这是非法的内存访问。应该改为 i < 10

4.3 对⾮动态开辟内存使⽤free释放

错误的写法:

cpp 复制代码
错误的写法
int main()
{
	int a = 10;
	int* p = &a;
	free(p); //free只能释放动态所开辟的空间,如malloc所开辟的空间就可以使用
	p = NULL;

	return 0;
}

解析代码:
核心问题:错误的内存释放

cpp 复制代码
int a = 10;        // 栈内存变量
int* p = &a;       // p 指向栈内存地址
free(p);           // ❌ 尝试释放栈内存

1. 栈内存 (Stack Memory)

cpp 复制代码
int a = 10;  // 栈上分配

特性

  • 自动分配:进入函数时自动分配

  • 自动释放:函数结束时自动回收

  • 生命周期:与函数作用域绑定

  • 管理方式:编译器自动管理

2. 堆内存 (Heap Memory)

cpp 复制代码
int* p = malloc(sizeof(int));  // 堆上分配

特性

  • 手动分配:需要显式调用分配函数

  • 手动释放 :必须显式调用 free()

  • 生命周期 :直到调用 free() 为止

  • 管理方式:程序员手动管理

错误原因:

free() 函数内部会:

  1. 检查指针是否在堆内存范围内

  2. 检查指针是否指向某个内存块的起始位置

  3. 栈地址 &a 不满足这些条件

正确的写法:

cpp 复制代码
#include <stdlib.h>

int main() {
    int* p = malloc(sizeof(int));  // 在堆上分配
    if (p == NULL) return 1;
    
    *p = 10;        // 使用
    free(p);        // ✅ 正确释放
    p = NULL;       // ✅ 防止野指针
    
    return 0;
}

根本原因总结:

开始的代码的主要问题是混淆了内存管理模型

  1. 栈内存模型:自动管理(分配/释放)

    • 适用:局部变量、函数参数

    • 操作:自动进行,程序员不干预

  2. 堆内存模型:手动管理(分配/释放)

    • 适用:动态数据结构、大内存

    • 操作:malloc/free,程序员全权负责

4.4 使⽤free释放⼀块动态开辟内存的⼀部分

错误的写法:

cpp 复制代码
错误的写法
int main()
{
	int* p = (int*)malloc(100);
	if (p == NULL);
	{
		return 1;
	}
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*p = i + 1;
		p++;
	}
	free(p); //此时的p已经不是动态事情的空间的起始位置了,不能进行free
	p = NULL;

	return 0;
}

解析代码:
主要问题:指针移动导致错误的free

问题分析:

cpp 复制代码
for (i = 0; i < 5; i++)
{
    *p = i + 1;
    p++;           // ❌ 移动了原始指针!
}

free(p);           // ❌ 此时p不是起始地址!

内存变化图示:

cpp 复制代码
初始状态(malloc后):
p → [???][???][???][???][???]
     ↑
     起始地址 = 0x1000

循环中:
第1次:p[0]=1, p++ → p=0x1004
第2次:p[0]=2, p++ → p=0x1008  
第3次:p[0]=3, p++ → p=0x100C
第4次:p[0]=4, p++ → p=0x1010
第5次:p[0]=5, p++ → p=0x1014

结束循环后:
p → [ 1 ][ 2 ][ 3 ][ 4 ][ 5 ][???]...
                     ↑
                    p现在指向这里(0x1014)
                    不是起始地址!

结果free(p) 试图释放一个不是malloc返回的地址,导致崩溃。

正确的写法:

cpp 复制代码
int main()
{
    int* p = (int*)malloc(100);
    if (p == NULL)  // 去掉分号
    {
        return 1;
    }
    
    int i = 0;
    for (i = 0; i < 5; i++)
    {
        *(p + i) = i + 1;  // ✅ 不移动p,用p+i访问
        // 或者:p[i] = i + 1;  // 这样写更清楚
    }
    
    free(p); // ✅ p还是起始位置
    p = NULL;
    return 0;
}

4.5 对同⼀块动态内存多次释放

错误的写法:

cpp 复制代码
void test()
{
	int* p = (int*)malloc(100);
	free(p); //如果它们之间加了 p=NULL 可以避免重复释放导致的报错
	free(p); //重复释放,导致报错
}
int main()
{
	test();
	return 0;
}

解析代码:
问题分析:重复释放

内存状态变化:

cpp 复制代码
步骤1:malloc(100)
p → [分配的100字节内存块] ← 状态:已分配

步骤2:第一次 free(p)
p → [同一个内存块] ← 状态:已释放(还给系统)

步骤3:第二次 free(p)
p → [同一个内存块] ← 状态:❌ 已经是释放状态!
                    再次释放 → 崩溃

为什么会崩溃?

free() 的内部工作:

  1. 检查指针是否有效

  2. 查找内存管理记录

  3. 第一次free:标记为"可用"

  4. 第二次free:发现已经是"可用"状态

  5. 报错:double free or corruption

正确的写法:

解决方案:释放后立即置NULL

cpp 复制代码
void test()
{
    int* p = (int*)malloc(100);
    free(p);  // 第一次释放
    p = NULL; // ✅ 立即置NULL
    
    free(p);  // ✅ free(NULL) 是安全的,什么都不做
}

4.6 动态开辟内存忘记释放(内存泄漏)

错误的写法:

cpp 复制代码
void test()
{
	int* p = (int*)malloc(100);
	if (NULL != p)
	{
		*p = 20;
	}
}
int main()
{
	test();

	while (1); //一直循环,没有free去释放空间,导致内存泄漏
}

解析代码:

主要问题:内存泄漏

cpp 复制代码
void test()
{
    int* p = (int*)malloc(100);
    if (NULL != p)
    {
        *p = 20;
    }
    // ❌ 没有 free(p)!
    // p 是局部变量,函数结束时会销毁
    // 但 malloc 分配的 100 字节内存还在!
}

内存泄漏图示:

cpp 复制代码
调用 test() 时:
1. malloc(100) → 从堆分配100字节
2. p 指向这块内存
3. *p = 20 使用内存
4. 函数结束,p 被销毁(栈回收)
5. 但堆上的100字节还在占用! ← ❌ 内存泄漏
6. 没有指针指向这块内存了,无法释放

正确的写法:

cpp 复制代码
#include <stdlib.h>

void test()
{
    int* p = (int*)malloc(100);
    if (NULL != p)
    {
        *p = 20;
        free(p);  // ✅ 使用后立即释放
        p = NULL;
    }
}

int main()
{
    test();
    return 0;
}

内存泄漏的后果:
长期运行程序

cpp 复制代码
时间线:
0秒:程序启动
10秒:test()调用100次 → 泄漏10KB
1小时:test()调用10000次 → 泄漏1MB  
1天:test()调用百万次 → 泄漏100MB
几天后:内存耗尽,程序崩溃

5. 柔性数组

也许你从来没有听说过柔性数组(flexiblearray)这个概念,但是它确实是存在的。 C99中,结构中的最后⼀个元素允许是未知⼤⼩的数组,这就叫做『柔性数组』成员。

例如:

cpp 复制代码
struct a
{
	int i;
	char n;
	int a[];  //最后一个成员变量为数组,而且没有定义大小,此时为柔性数组
//	int a[0]; //也可以加个0进去,是一样的
};

5.1 柔性数组的特点:

  • 结构中的柔性数组成员前⾯必须⾄少⼀个其他成员。
  • sizeof返回的这种结构⼤⼩不包括柔性数组的内存。
  • 包含柔性数组成员的结构⽤malloc()函数进⾏内存的动态分配,并且分配的内存应该⼤于结构的⼤ ⼩,以适应柔性数组的预期⼤⼩。

例如:

cpp 复制代码
struct S
{
	int a; //占4个字节
	int arr[]; //不知道占多少个字节
};
int main()
{
	printf("%zd\n", sizeof(struct S)); //输出4
	return 0;
}

输出结果:

5.2 柔性数组的使⽤

cpp 复制代码
struct S
{
	int n;  //4个字节
	int arr[];
};
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	ps->n = 100;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		ps->arr[i] = i;
	}
	//调整空间
	struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 10 * sizeof(int));
	if (ptr != NULL)
	{
		ps = ptr;
	}
	//释放空间
	free(ps);
	ps = NULL;

	return 0;
}

代码解析:

第一步:定义柔性数组结构体

cpp 复制代码
struct S
{
    int n;        // 4个字节
    int arr[];    // 柔性数组,大小未知
};

知识点:柔性数组

  • int arr[]柔性数组成员

  • 它必须是结构体的最后一个成员

  • 不占用结构体大小sizeof(struct S) 只包含 n 的大小

  • 用于动态分配结构体+数组的连续内存

第二步:分配内存

cpp 复制代码
struct S* ps = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));

内存分配解析

  • sizeof(struct S) = 4字节(存储n)

  • 5 * sizeof(int) = 5 × 4 = 20字节(存储arr数组)

  • 总大小 = 4 + 20 = 24字节

  • 内存布局 :结构体和数组在连续的内存中

内存示意图

cpp 复制代码
分配的内存块(24字节):
┌──────────────┬────────────────────────────┐
│ 结构体部分    │      柔性数组部分           │
│ n (4字节)    │ arr[0]-arr[4] (20字节)     │
└──────────────┴────────────────────────────┘
ps→↑

第三步:使用内存

cpp 复制代码
ps->n = 100;            // 设置结构体成员
int i = 0;
for (i = 0; i < 5; i++)
{
    ps->arr[i] = i;     // 设置数组元素
}

内存状态变化

cpp 复制代码
执行前: [随机值][随机值][随机值][随机值][随机值]
执行后: [ 100 ][  0 ][  1 ][  2 ][  3 ][  4 ]
           ↑n    arr[0] arr[1] arr[2] arr[3] arr[4]

知识点:箭头运算符

  • ps->n 等价于 (*ps).n

  • 用于通过指针访问结构体成员

第四步:调整空间(扩容)

cpp 复制代码
struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 10 * sizeof(int));

realloc解析

  • 原大小:sizeof(struct S) + 5 * sizeof(int) = 4 + 20 = 24字节

  • 新大小:sizeof(struct S) + 10 * sizeof(int) = 4 + 40 = 44字节

  • 扩容20字节(增加5个int的空间)

realloc的两种可能

  1. 原地扩容 :后面有足够空间,ptr == ps

  2. 移动扩容 :分配新内存,复制数据,释放旧内存,ptr != ps

检查realloc结果

cpp 复制代码
if (ptr != NULL)
{
    ps = ptr;  // 更新指针
}

知识点:安全更新指针

  • 不直接 ps = realloc(ps, ...),防止失败时丢失原指针

  • 用临时变量 ptr 接收结果

  • 成功时才更新 ps

内存扩容后

cpp 复制代码
扩容前(24字节): [100][0][1][2][3][4]
扩容后(44字节): [100][0][1][2][3][4][?][?][?][?][?]
                                     ↑ 新增的5个int空间

6. 总结C/C++中程序内存区域划分

C/C++程序内存分配的⼏个区域:

  1. 栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时 这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内 存容量有限。栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。 《函数栈帧的创建和销毁》

  2. 堆区(heap):⼀般由程序员分配释放,若程序员不释放,程序结束时可能由OS(操作系统) 回收。分配⽅式类似于链表。

  3. 数据段(静态区):(static)存放全局变量、静态数据。程序结束后由系统释放。

  4. 代码段:存放函数体(类成员函数和全局函数)的⼆进制代码

以上就是我们动态内存管理的全部内容了,谢谢大家!!!!!!

相关推荐
矢鱼3 小时前
python中对应c++容器的结构
开发语言·c++·python·算法
Doris8933 小时前
【JS】JS进阶--编程思想、面向对象构造函数、原型、深浅拷贝、异常处理、this处理、防抖节流
开发语言·javascript·ecmascript
Clarence Liu3 小时前
golang 剖析 sync包
开发语言·golang
柒儿吖3 小时前
Perl在鸿蒙PC上的使用方法
开发语言·harmonyos·perl
m5655bj3 小时前
使用 C# 设置 Word 段落对齐样式
开发语言·c#·word
福尔摩斯张3 小时前
基于TCP的FTP文件传输系统设计与实现(超详细)
linux·开发语言·网络·网络协议·tcp/ip·udp
技术净胜3 小时前
MATLAB 环境搭建与认知实战教程:从下载安装到入门全解析教程
开发语言·matlab
爱吃大芒果3 小时前
Flutter 自定义 Widget 开发:从基础绘制到复杂交互
开发语言·javascript·flutter·华为·ecmascript·交互
帅得不敢出门3 小时前
MTK Android11 APP调用OTA升级
android·java·开发语言·framework