32.内存分配-内存安全
32.1 知识点
什么是内存安全
内存安全(Memory Safety)指的是程序在访问和操作内存时,不会因为出现问题而导致程序报错或崩溃。为了保证内存安全,我们需要注意以下几点:
- 了解内存销毁时机
- 防止内存泄漏
- 防止越界访问
- 空指针检查
- 避免使用野指针
了解内存销毁时机
栈内存
栈内存是自动管理的。当函数调用结束时,局部变量的内存会自动销毁。一旦程序退出某个作用域(例如函数或代码块),该作用域内的局部变量的内存会自动释放。
需要注意的是,不要在作用域外使用栈上分配的内存。
堆内存
使用 new
分配的堆内存需要在不使用后通过 delete
或 delete[]
进行释放。
需要注意的是,控制堆内存对象的生命周期非常重要,在不使用时一定要删除,防止内存泄漏。
堆和栈的其他区别
内存空间大小不同
- 栈的大小通常是固定的(比如几兆),由操作系统决定,因此无法在其中分配大内存。
- 堆的大小取决于系统可用内存,理论上可以分配非常大的内存。
速度不同
- 栈的分配和释放速度快,因为是由系统直接管理。
- 堆的分配和释放速度较慢。
线程安全
- 栈是线程私有的,不同线程的栈彼此独立,是线程安全的。
- 堆是进程共享的,多个线程访问堆时需要保证线程安全。
如何应用
- 栈:适用于小规模、短生命周期的数据。
- 堆:适用于动态分配的大规模数据,或生命周期跨越函数调用的数据。
防止内存泄漏
内存泄漏是指不再被使用的内存仍然占用资源,常见于堆内存的使用中。以下两种情况容易导致内存泄漏:
- 堆内存忘记释放,导致一直占用内存。
- 丢失对已分配堆内存的记录,导致内存无法释放。
以下代码分配了内存但未释放:
int* p = new int(10);
// 未释放内存,导致内存泄漏
更严重的情况是指针指向了新的内存,而未释放原有的内存,让原本指向堆内存的指针在没有释放之前指向了别的内容 原来分配new int(10)不好找到他让他释放了:
int* p = new int(10);
p = nullptr; // 丢失了原本分配的内存记录
p = new int(11); // 再次分配新内存
防止越界访问
为了避免越界访问,需要限制数组或指针的访问范围。必须确保程序访问的内存区域在分配范围之内。
空指针检查
空指针检查可以有效避免对无效内存的访问。良好的编程习惯包括:
- 在声明指针但未初始化时,将其赋值为
nullptr
。 - 在释放指向堆内存的指针后,将其赋值为
nullptr
。
int* p = nullptr;
p = new int(10);
delete p;
p = nullptr;
if (p != nullptr)
{
// 使用指针前的空指针检查
}
避免使用野指针
野指针是指向已释放内存空间的指针。为避免使用野指针,应确保指针指向的内存不再使用时,将其置为 nullptr
。
int* p = new int(10);
delete p; // 释放内存
p = nullptr; // 避免成为野指针
32.2 知识点代码
Lesson32_内存分配_内存安全.cpp
#include <iostream>
using namespace std;
int main()
{
std::cout << "内存安全\n";
#pragma region 知识点一 什么是内存安全
//内存安全(Memory Safety)指的是程序在访问和操作内存时不会因为出现程序问题
//导致程序报错、崩溃的情况。
//想要保证内存安全,我们主要保证以下几点即可
//1.了解内存销毁时机
//2.防止内存泄漏
//3.防止越界访问
//4.空指针检查
//5.避免使用野指针
#pragma endregion
#pragma region 知识点二 了解内存销毁时机
//1.栈内存
// 在函数调用结束时,局部变量的内存会自动销毁。
// 栈内存是自动管理的,一旦程序退出某个作用域(例如函数或代码块)
// 该作用域内的局部变量的内存会自动释放
// 因此我们只需注意不要在作用域外使用作用域内的在栈上分配的内存
//
//2.堆内存
// 使用 new 分配的堆内存需要在不使用后通过 delete 或 delete[] 进行释放
// 因此我们只需要注意控制堆内存对象的生命周期,不用时一定要删除
//堆和栈的其他区别:
//1.内存空间大小不同
// 栈的大小通常是固定的(比如几兆),是由操作系统决定的。
// 因此我们不能在其中分配大内存
// 堆的大小取决于系统可用的内存,理论上可以分配很大的内存
// 因此我们可以在其中分配大内存
//2.速度不同
// 栈的分配和释放速度快,因为是由系统直接管理
// 堆的分配和释放速度较慢
//3.线程安全
// 栈是线程私有的,不同线程的栈彼此独立,是线程安全的
// 堆是进程共享的,多线程访问时需要保证线程安全
//如何应用
//栈:适用于小规模,短生命周期的数据
//堆:适用于动态分配的大规模数据,或生命周期跨越函数调用的数据
#pragma endregion
#pragma region 知识点三 防止内存泄漏
//内存泄漏主要指不再被使用的内容一直占用内存资源
//在使用堆内存时很容易造成这样的结果
//1.堆内存忘记释放(一直占用堆内存)
//2.丢失对已分配堆内存的记录(无法释放,一直占用堆内存)
//出现以上类似的情况就会造成内存泄漏
//当内存达到一定的瓶颈是就会崩溃或报错,程序无法正常运行
//比如以下代码分配了内存但是没有释放
int* p = new int(10);
//排查问题时其实还是可以找到p让他释放的
// 以下是更严重的情况 p指向了新的内存
// 让原本指向堆内存的指针在没有释放之前指向了别的内容 原来分配new int(10)不好找到他让他释放了
p = nullptr;
p = new int(11);
#pragma endregion
#pragma region 知识点四 防止越界访问
//我们需要限制数组或指针的访问范围
//要避免访问超出分配范围的内存
#pragma endregion
#pragma region 知识点五 空指针检查
//养成好习惯
// 1.在声明指针没有初始化时对它赋值为nullptr
// 2.对于指向堆内存的指针,释放后,同样对它赋值为nullptr
//这样在使用指针之前判断它是否为nullptr空指针再决定如何使用它
int* p = nullptr;
p = new int(10);
delete p;
p = nullptr;
if (p != nullptr)
{
}
#pragma endregion
#pragma region 知识点六 避免使用野指针
//避免指针指向已释放的内存空间
//确定指针指向的内存不能用时,一定置空
#pragma endregion
}
32.3 练习题
堆和栈的区别是什么?
1. 分配方式
- 栈:内存由编译器自动分配和释放。
- 堆:内存由程序员使用
new
分配,需要显式调用delete
来释放内存。
2. 生命周期
- 栈:栈的生命周期由作用域决定,超出作用域后,栈内存自动释放。
- 堆:堆的生命周期由程序员控制,内存不会自动回收,可能会一直存在直到程序终止。
3. 内存大小
- 栈:栈的大小通常有限,较小,取决于操作系统或编译器设置。
- 堆:堆的大小通常取决于系统的可用内存,理论上可以分配非常大的内存。
4. 分配速度
- 栈:栈的分配速度非常快,因为系统只需移动栈指针即可。
- 堆:堆的分配速度较慢,因为需要操作系统的内存管理器找到合适的内存块并处理内存碎片。
5. 线程安全
- 栈:栈是线程私有的,每个线程都有独立的栈,因此是线程安全的。
- 堆:堆是进程共享的,多个线程可能同时访问堆上的内存,因此需要额外确保线程安全。
6. 使用场景
- 栈:用于临时变量、函数参数、小型固定大小的内存、短时间存在的数据。
- 堆:用于动态分配的对象或数组、长时间存在的数据、需要灵活调整大小的数据。
在开发时,你认为应该优先使用栈还是堆?
优先使用栈内存,因为栈的内存管理简单、高效、安全,适合存储小型临时变量。
堆适合需要动态分配的大型数据或长生命周期数据,但要注意内存泄漏和释放问题。
什么是内存泄漏?
内存泄漏是指程序中动态分配的内存未被释放,导致这部分内存无法再被使用或回收,
最终可能耗尽系统内存,影响程序性能或导致程序崩溃。(一般主要指堆内存)
常见情况:
- 堆上分配的内存没有释放。
- 分配的堆内存没有有效的指针指向,程序员无法再找到这块内存。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com