int* ptr = new int(10); // 动态分配一个整数
delete ptr; // 释放内存
// ptr 现在成为悬挂指针,因为它指向被释放的内存
delete ptr;
ptr = nullptr; // 避免悬挂指针
int* ptr; // 未初始化的指针
*ptr = 5; // 未定义行为,对野指针解引用
int* ptr = new int(10); // 动态分配内存
delete ptr; // 释放内存
// 如果在此后没有将 ptr 置为 nullptr,它将变成野指针
int* ptr = nullptr; // 安全地初始化为nullptr
delete ptr;
ptr = nullptr; // 避免野指针
#include
void createMemoryLeak() {
int* leakyInt = new int(42); // 动态分配,没有相应的 delete
}
int main() {
createMemoryLeak(); // 调用会产生内存泄漏的函数
// ... 程序其他代码
return 0;
// 程序结束后,由于缺乏 delete,分配的内存被泄漏
}
#include
void safeFunction() {
auto safePtr = std::make_unique<int>(42); // 使用 unique_ptr,无需手动释放
}
Valgrind: 一个广泛使用的Linux下的内存检测工具,可以帮助发现内存泄漏以及其他内存相关错误。
AddressSanitizer(ASan): 一个快速的内存错误检测器,可以检测出包括内存泄漏在内的各种内存访问错误。
Visual Studio: 提供了内置的内存泄漏检测工具,在调试模式下运行程序时可以帮助发现内存泄漏。
#include
void bufferOverflowExample() {
char buffer[10];
for(int i = 0; i <= 10; i++) {
buffer[i] = 'a'; // i = 10 时会导致缓冲区溢出
}
}
int main() {
bufferOverflowExample(); // 调用函数,演示缓冲区溢出
return 0;
}
1、使用标准库容器: 例如 std::vector 和 std::string 来替代原始数组。这些容器会自动地管理内存,并提供边界检查操作。
2、边界检查: 如果必须使用原始数组,确保对所有的数组访问操作进行边界检查。
3、使用安全函数: 尽量使用安全的函数来代替旧的函数。比如使用 std::strncpy 代替 strcpy,使用 std::snprintf 代替 sprintf。
4、堆栈保护: 现代编译器通常提供选项以增加堆栈保护,可以帮助阻止缓冲区溢出攻击。
5、避免不安全的函数: 一些函数本身就存在安全隐患,比如 gets 函数,应当避免使用。
6、代码审查: 持续审查代码,查找可能的缓冲区溢出风险。
7、动态检测工具: 使用动态内存调试工具,如 Valgrind 或 AddressSanitizer,它们可以在开发过程中帮助发现缓冲区溢出。
8、静态代码分析: 许多现代 IDE 和静态代码分析工具可以扫描源代码,查找潜在的缓冲区溢出风险。
9、使用堆分配: 当分配大型数据结构时,使用堆 (通过 new 或 std::make_unique 等) 而非栈,以避免栈溢出。
10、开启编译器安全检查: 某些编译器提供编译时检查,可以帮助检测数组边界问题。
GCC/Clang 有 -fstack-protector 选项开启堆栈保护。
Visual Studio 提供 /GS 以增加安全检查。
指针 ptr 指向的是栈上分配的变量 i 的地址,而不是通过 new 关键字分配的堆内存。尝试用 delete 释放栈上的变量是不允许的,这会导致程序运行出现未定义行为。
int main() {
int i = 42;
int* ptr = &i;
// 错误:试图释放一个非动态分配的指针
delete ptr; // 未定义行为
return 0;
}
只有通过 new 分配的内存才应该用 delete 释放,通过 new[] 分配的内存应该用 delete[] 释放。类似地,malloc 分配的内存应该用 free 释放。
int main() {
// 正确:使用 new 分配动态内存
int* ptr = new int(42);
// 使用指针做一些事情...
// 正确:使用 delete 释放动态分配的内存
delete ptr;
return 0;
}
1、使用智能指针:智能指针如 std::unique_ptr 或 std::shared_ptr 可以自动管理内存的释放,这样就不需要手动调用 delete。
2、尽量避免裸指针:如果可以,尽量使用栈分配或者标准库容器类,比如 std::vector 或 std::string,这样就不需要直接处理内存分配和释放。
3、代码审查和自动化测试:通过代码审查和测试可以帮助检测和防止这类错误。
4、清晰的所有权管理:在设计软件时,清晰地定义哪部分代码负责分配和释放内存。
5、使用 RAII (Resource Acquisition Is Initialization) 原则:封装资源管理在对象内,利用构造函数分配和析构函数释放资源,保证资源的正确管理。
6、编写自动化测试:通过自动化测试来检测内存管理的问题,确保在释放之前内存是通过正确的方式分配的。
后越界访问发生在当你尝试访问一个数组或缓冲区结束之后的内存位置时。常见的情况是,访问数组时超出其已定义的长度范围。这种错误可能引起程序崩溃或行为异常,并且可以被恶意利用来攻击软件系统。
int arr[5] = {0, 1, 2, 3, 4};
// 越界访问:访问数组后面的元素
int value = arr[5]; // 错误,arr[5] 实际上是 arr 数组后的第一个位置
前越界访问发生在访问数组或缓冲区开始之前的内存位置时。这种情况不像后越界那么常见,但同样会导致未定义行为。
int arr[5] = {0, 1, 2, 3, 4};
// 越界访问:访问数组前面的元素
int value = *(arr - 1); // 错误,arr - 1 指向数组前面的位置
1、使用标准库容器:使用 std::vector, std::array, std::string 等 STL 容器,它们提供了边界检查的方法(如 at() 成员函数),如果越界,它们会抛出异常。
2、边界检查:总是在数组操作之前执行边界检查,确保索引是有效的。
3、使用迭代器和范围基 for 循环:尽量使用迭代器或者范围基 for 循环进行数组或容器的遍历,这样可以避免直接操作索引。
4、静态和动态分析工具:利用静态分析工具(诸如 Clang Static Analyzer 或者 cppcheck)和动态分析工具(比如 Valgrind 或者 AddressSanitizer)检测代码中可能存在的越界访问。
5、自动化测试和代码审查:编写测试用例以捕获边界条件,并进行代码审查以确保遍历操作的正确性。
6、避免裸指针操作:尽量减少指针算术操作,如果不得不使用,确保操作是安全的。
7、初始化和清零:对于本地数组,可在声明时对它们进行初始化或清零,以避免不小心使用未初始化的内存。
当你的代码调用可能抛出异常的函数或方法,而却没有捕获异常,可能会导致程序的非正常终止。
解决方法:使用 try-catch 块包围可能抛出异常的代码,并根据可能发生的错误类型来处理异常。
使用过宽泛的异常捕获(如捕获所有类型的异常)可能会掩盖问题的真正原因,并导致调试困难。
解决方法:尽量捕获特定的异常类型,这有利于更精确地处理错误并提供更有用的调试信息。
在异常处理过程中,如果没能正确释放资源(如动态分配的内存、文件句柄、锁等),将会导致资源泄露。
解决方法:利用 RAII(Resource Acquisition Is Initialization)原则管理资源,使用智能指针如 std::unique_ptr 和 std::shared_ptr 管理动态分配的内存,使用 std::lock_guard 和 std::unique_lock 管理锁,确保在栈展开(stack unwinding)时资源可以被自动释放。
在 catch 块中什么都不做(或者只打印错误消息)然后继续执行,有可能导致程序在一个不确定的状态下继续运行。
解决方法:在 catch 块中恰当地处理异常。如果不能处理,应该再次抛出或终止程序。
如果构造函数抛出异常,在没有完全构建该对象的情况下,析构函数不会被调用,可能会导致资源泄露。此外,在析构函数中抛出异常,如果同时有其它异常抛出,可能会导致 std::terminate 被调用。
解决方法:保证构造函数中的代码能够安全地处理异常,不要在析构函数中抛出异常。
一些 C++ 的库和接口使用返回值来表示成功或错误。忽略这些返回值可能会导致错误未被检测到。
解决方法:总是检查函数返回的错误代码,并根据错误代码进行相应的处理。
异常应该仅用于异常情况,它们不应该被用作执行普通流程控制,因为这样会降低程序性能,并且会让程序逻辑更难以理解。
解决方法:只针对确实异常的情况抛出和处理异常,使用其他方式(如条件判断)来处理正常逻辑。
在使用动态内存分配时,例如 new 操作符可能会抛出 std::bad_alloc。不处理这种异常可能会导致程序崩溃。
解决方法:对可能抛出 std::bad_alloc 的代码使用 try-catch 块并适当处理,或者使用 noexcept 的 new (std::nothrow) 形式。
C++11 之前,异常规格说明(如 throw(Type))用于表明函数可能抛出的异常类型。在 C++11 后,这个语法被 noexcept 取代。
解决方法:尽量避免使用异常规格说明,转而使用 noexcept 关键字,这能提供更明确的异常安全保证。
在 catch 代码块中抛出一个新的异常,而没有释放捕获的异常,将导致异常递归,可能消耗过多资源甚至栈溢出。
解决方法:避免在 catch 块内抛出新的异常,如果必要,应该在之前将异常释放或转换为适当的类型。
通过积极地关注异常和错误处理,可以提高程序的可靠性、可维护性,并且减少潜在的安全风险。