c++内存相关知识点整理

一、内存管理

1. new 和 delete 运算符

  • new:用于在堆上分配内存,并返回指向该内存的指针。可以分配单个对象或数组。
  • delete:用于释放之前通过 new 分配的内存。

2. new 和 delete 的变体

  • new 和 delete:默认分配和释放内存。
  • new[] 和 delete[]:用于数组的分配和释放。
  • new(std::nothrow) 和 deletenew(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 异常。应该适当处理这种异常。

示例

下面是一个简单的示例,展示了如何使用 newdelete 进行动态内存管理:

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 的成员函数,避免使用 strcpystrcat 等可能引起缓冲区溢出的函数。
  • 使用智能指针:使用 std::unique_ptr 和 std::shared_ptr 管理内存,减少手动管理内存的风险。
  • 使用容器:使用 C++ 标准库中的容器,如 std::vector 和 std::string,它们通常更安全地处理内存分配和边界检查。

三、内存泄漏

内存泄漏是指程序在运行过程中分配了内存但未能正确释放,导致这部分内存无法再被程序使用,最终可能导致程序消耗越来越多的内存资源。在 C++ 中,内存泄漏通常与动态内存分配相关,即使用 new 关键字分配的内存没有被适时地使用 delete 释放。

内存泄漏的原因

  1. 忘记释放内存:这是最常见的内存泄漏原因。当你使用 new 分配内存后,如果没有正确地使用 delete 来释放它,这部分内存就会成为泄漏。
  2. 多重释放:虽然这通常不会直接导致内存泄漏,但如果一个指针指向的内存被释放了两次,第二次释放会导致未定义行为,可能掩盖了真正的内存泄漏。
  3. 野指针:当一个指针指向的内存被释放后,但指针没有被设置为 nullptr,那么这个指针就变成了野指针。如果后续代码尝试再次释放野指针指向的内存,或者通过野指针访问内存,都可能导致未定义行为。
  4. 循环引用:当使用 std::shared_ptr 时,如果两个或多个对象相互持有对方的 shared_ptr,可能会形成循环引用,导致这些对象永远不会被删除,从而导致内存泄漏。
  5. 异常安全问题:如果在分配内存后抛出了异常,而没有适当地处理异常和释放内存,也会导致内存泄漏。

如何避免内存泄漏

  1. 使用智能指针:智能指针如 std::unique_ptr 和 std::shared_ptr 可以自动管理内存生命周期。std::unique_ptr 保证内存会在其生命周期结束时被释放,而 std::shared_ptr 则会在最后一个引用计数变为零时释放内存。
  2. 及时释放内存:如果你使用 new 分配内存,请确保每一块分配的内存都被正确释放,并且不要忘记将指针设置为 nullptr。
  3. 使用 RAII:资源获取即初始化(Resource Acquisition Is Initialization)是一种编程模式,其中资源(如内存)在对象构造时获取,在对象析构时释放。这确保了即使在异常情况下也能正确释放资源。
  4. 避免循环引用:当使用 std::shared_ptr 时,要注意循环引用的问题,可以使用 std::weak_ptr 来打破循环引用。
  5. 使用工具检测内存泄漏:利用内存检测工具,如 Valgrind 或 AddressSanitizer,可以帮助识别内存泄漏。
  6. 代码审查和单元测试:定期进行代码审查可以帮助发现潜在的内存管理问题。编写单元测试也可以帮助验证内存管理逻辑的正确性。

四、内存分区

在 C++ 中,程序的内存可以分为几个不同的区域,每个区域都有其特定的用途和生命周期。理解这些内存区域对于有效地管理内存非常重要。下面是 C++ 中的主要内存区域:

1. 栈区(Stack)

栈区是由编译器自动分配和释放的区域,用于存储函数的局部变量和函数调用的临时数据。栈上的内存分配和释放速度快,但空间有限。

  • 特点:自动分配和释放。生命周期与函数调用相关。存储局部变量、函数参数等。快速分配和释放。

2. 堆区(Heap)

堆区是由程序员手动分配和释放的区域,用于存储动态分配的对象。使用 newnew[] 进行分配,使用 deletedelete[] 进行释放。

  • 特点:手动分配和释放。存储动态分配的对象。生命周期由程序员控制。分配和释放速度较慢。空间相对较大。

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++的知识,整理不易,多多关注谢谢

全部评论

相关推荐

2 13 评论
分享
牛客网
牛客企业服务