c++内存相关知识点整理
一、内存管理
1. new 和 delete 运算符
- new:用于在堆上分配内存,并返回指向该内存的指针。可以分配单个对象或数组。
- delete:用于释放之前通过 new 分配的内存。
2. new 和 delete 的变体
- new 和 delete:默认分配和释放内存。
- new[] 和 delete[]:用于数组的分配和释放。
- new(std::nothrow) 和 delete:
new(std::nothrow)
试图分配内存,如果失败则返回nullptr
而不是抛出异常。
3. 构造函数和析构函数
- 构造函数:在通过
new
分配内存后,对象的构造函数会被调用,用于初始化对象。 - 析构函数:在通过
delete
释放内存前,对象的析构函数会被调用,用于清理对象。
4. 内存管理的安全性
- 智能指针:C++11 引入了智能指针,如 std::unique_ptr 和 std::shared_ptr,它们负责自动管理动态分配的内存。这有助于避免内存泄漏和悬挂指针等问题。std::unique_ptr:独占所有权的智能指针,当 unique_ptr 被销毁时,它所拥有的对象也会被自动删除。std::shared_ptr:共享所有权的智能指针,当最后一个指向该对象的 shared_ptr 被销毁时,对象会被删除。
5. 内存管理的注意事项
- 悬挂指针:释放内存后继续使用指向该内存的指针会导致未定义行为。
- 内存泄漏:忘记释放分配的内存会导致内存泄漏,尤其是在循环中频繁分配和释放内存的情况下。
- 多重释放:多次释放同一块内存也会导致未定义行为。
- 异常安全:在使用
new
分配内存时,如果内存分配失败,new
会抛出std::bad_alloc
异常。应该适当处理这种异常。
示例
下面是一个简单的示例,展示了如何使用 new
和 delete
进行动态内存管理:
1#include <iostream> 2#include <new> // 包含 new(std::nothrow) 3 4class MyClass { 5public: 6 MyClass(int value) : value(value) { 7 std::cout << "MyClass constructor called." << std::endl; 8 } 9 10 ~MyClass() { 11 std::cout << "MyClass destructor called." << std::endl; 12 } 13 14 void printValue() const { 15 std::cout << "Value: " << value << std::endl; 16 } 17 18private: 19 int value; 20}; 21 22int main() { 23 // 分配一个 MyClass 对象 24 MyClass* myObj = new(std::nothrow) MyClass(42); 25 26 if (myObj != nullptr) { 27 myObj->printValue(); // 输出 "Value: 42" 28 29 // 删除 MyClass 对象 30 delete myObj; 31 } else { 32 std::cerr << "Memory allocation failed." << std::endl; 33 } 34 35 return 0; 36}
二、内存溢出
1. 栈溢出(Stack Overflow)
栈溢出发生在程序栈空间不足时。在 C++ 中,函数调用时会为局部变量和函数参数分配栈空间。如果栈空间分配过多或递归调用过深,可能会导致栈溢出。
原因
- 深度递归:如果递归函数的终止条件不正确或缺失,递归调用可能会无限进行下去,导致栈溢出。
- 大局部变量:如果局部变量占用的空间过大,可能会耗尽栈空间。
- 栈分配的数组过大:如果在栈上分配的数组过大,也可能导致栈溢出。
解决方法
- 优化递归函数:确保递归函数有正确的终止条件,考虑使用迭代替代递归。
- 减少局部变量的大小:尽量减少局部变量的数量和大小。
- 使用堆内存:对于大型数组或数据结构,考虑使用动态内存分配(如
new
和std::vector
)而不是栈分配。
2. 堆溢出(Heap Overflow)
堆溢出是指程序试图访问超出分配给它的堆内存范围之外的内存。这通常发生在使用 new
分配内存时,如果程序试图访问超出分配内存范围的地址,就可能发生堆溢出。
原因
- 越界访问:如果程序试图读取或写入超出分配给它的堆内存范围的地址。
- 缓冲区溢出:如果向一个固定大小的缓冲区写入超过其容量的数据,可能导致相邻内存区域被覆盖。
解决方法
- 检查边界:确保对数组或缓冲区的访问不会超出其范围。
- 使用安全的库函数:使用安全的字符串处理函数,如
std::string
的成员函数,避免使用strcpy
,strcat
等可能引起缓冲区溢出的函数。 - 使用智能指针:使用
std::unique_ptr
和std::shared_ptr
管理内存,减少手动管理内存的风险。 - 使用容器:使用 C++ 标准库中的容器,如
std::vector
和std::string
,它们通常更安全地处理内存分配和边界检查。
三、内存泄漏
内存泄漏是指程序在运行过程中分配了内存但未能正确释放,导致这部分内存无法再被程序使用,最终可能导致程序消耗越来越多的内存资源。在 C++ 中,内存泄漏通常与动态内存分配相关,即使用 new
关键字分配的内存没有被适时地使用 delete
释放。
内存泄漏的原因
- 忘记释放内存:这是最常见的内存泄漏原因。当你使用 new 分配内存后,如果没有正确地使用 delete 来释放它,这部分内存就会成为泄漏。
- 多重释放:虽然这通常不会直接导致内存泄漏,但如果一个指针指向的内存被释放了两次,第二次释放会导致未定义行为,可能掩盖了真正的内存泄漏。
- 野指针:当一个指针指向的内存被释放后,但指针没有被设置为 nullptr,那么这个指针就变成了野指针。如果后续代码尝试再次释放野指针指向的内存,或者通过野指针访问内存,都可能导致未定义行为。
- 循环引用:当使用 std::shared_ptr 时,如果两个或多个对象相互持有对方的 shared_ptr,可能会形成循环引用,导致这些对象永远不会被删除,从而导致内存泄漏。
- 异常安全问题:如果在分配内存后抛出了异常,而没有适当地处理异常和释放内存,也会导致内存泄漏。
如何避免内存泄漏
- 使用智能指针:智能指针如 std::unique_ptr 和 std::shared_ptr 可以自动管理内存生命周期。std::unique_ptr 保证内存会在其生命周期结束时被释放,而 std::shared_ptr 则会在最后一个引用计数变为零时释放内存。
- 及时释放内存:如果你使用 new 分配内存,请确保每一块分配的内存都被正确释放,并且不要忘记将指针设置为 nullptr。
- 使用 RAII:资源获取即初始化(Resource Acquisition Is Initialization)是一种编程模式,其中资源(如内存)在对象构造时获取,在对象析构时释放。这确保了即使在异常情况下也能正确释放资源。
- 避免循环引用:当使用 std::shared_ptr 时,要注意循环引用的问题,可以使用 std::weak_ptr 来打破循环引用。
- 使用工具检测内存泄漏:利用内存检测工具,如 Valgrind 或 AddressSanitizer,可以帮助识别内存泄漏。
- 代码审查和单元测试:定期进行代码审查可以帮助发现潜在的内存管理问题。编写单元测试也可以帮助验证内存管理逻辑的正确性。
四、内存分区
在 C++ 中,程序的内存可以分为几个不同的区域,每个区域都有其特定的用途和生命周期。理解这些内存区域对于有效地管理内存非常重要。下面是 C++ 中的主要内存区域:
1. 栈区(Stack)
栈区是由编译器自动分配和释放的区域,用于存储函数的局部变量和函数调用的临时数据。栈上的内存分配和释放速度快,但空间有限。
- 特点:自动分配和释放。生命周期与函数调用相关。存储局部变量、函数参数等。快速分配和释放。
2. 堆区(Heap)
堆区是由程序员手动分配和释放的区域,用于存储动态分配的对象。使用 new
和 new[]
进行分配,使用 delete
和 delete[]
进行释放。
- 特点:手动分配和释放。存储动态分配的对象。生命周期由程序员控制。分配和释放速度较慢。空间相对较大。
3. 数据区(Data Segment)
数据区用于存储全局变量和静态变量,以及初始化的静态数据。
- 特点:在程序启动时初始化。生命周期与程序相同。存储全局变量、静态变量。初始化的静态数据存储在此区域。
4. 代码区(Code Segment)
代码区用于存储程序的指令集,即函数的机器码。
- 特点:存储程序的指令集。只读区域。生命周期与程序相同。
5. BSS 区(Block Started by Symbol)
BSS 区用于存储未初始化的全局变量和静态变量。
- 特点:未初始化的全局变量、静态变量存储在此区域。默认初始化为零。生命周期与程序相同。
示例:
#include <iostream> #include <cstring> // for memset // 全局变量 int globalVar = 100; // 存储在数据段 int* globalPtr; // 存储在数据段 // 函数 void useStackAndHeap() { int stackVar = 10; // 存储在栈区 int* heapVar = new int(20); // 存储在堆区 // 使用栈区和堆区的变量 std::cout << "Stack variable: " << stackVar << std::endl; std::cout << "Heap variable: " << *heapVar << std::endl; delete heapVar; // 释放堆区的内存 } int main() { // 使用全局变量 std::cout << "Global variable: " << globalVar << std::endl; // 分配全局指针并使用堆区 globalPtr = new int[100]; memset(globalPtr, 0, 100 * sizeof(int)); // 初始化为零 // 使用全局指针 std::cout << "First element of global array: " << globalPtr[0] << std::endl; delete[] globalPtr; // 释放全局指针指向的堆区内存 useStackAndHeap(); // 使用栈区和堆区 return 0; }#c++##c++面试##c++后端##c++学习##c++开发#
c++知识库 文章被收录于专栏
不定时更新一些学习c++的知识,整理不易,多多关注谢谢