c++动态内存管理

发布时间:2024年01月05日

C++ 动态内存管理

在 C++ 中,动态内存管理是一个核心概念,它允许在运行时分配和释放内存。以下是 C++ 动态内存管理需要掌握的关键知识点:

1. newdelete 操作符

在 C++ 中,newdelete 是用于动态内存分配和释放的基本操作符。

new 操作符

new 用于在堆(也称为自由存储区)上动态分配内存,并返回指向新分配内存的指针。new 会调用对象的构造函数来初始化对象。

基本用法

int* ptr = new int; // 分配一个 int 的内存
*ptr = 5; // 初始化该 int

初始化

new 还支持直接初始化:

int* ptr = new int(5); // 分配内存并初始化为 5

异常

如果内存分配失败(例如由于内存不足),new 将抛出 std::bad_alloc 异常。

数组分配

对于数组的分配,new 提供了特殊的语法:

int* array = new int[10]; // 分配一个包含 10 个整数的数组

delete 操作符

delete 用于释放由 new 分配的内存,并调用对象的析构函数。

基本用法

delete ptr; // 释放内存并调用析构函数

数组释放

释放由 new 分配的数组需要使用 delete[]

delete[] array; // 释放数组内存

注意事项

  • 避免内存泄漏:每个 new 分配的内存都应该使用对应的 deletedelete[] 来释放。
  • 避免悬挂指针:释放内存后,指针变成悬挂指针。应该将指针设为 nullptr
  • 不要重复释放内存:对同一内存块调用两次 delete 会导致未定义行为。
  • 数组与非数组形式要匹配:用 new[] 分配的内存必须用 delete[] 释放,用 new 分配的内存必须用 delete 释放。

替代方案

在现代 C++ 开发中,建议使用智能指针(如 std::unique_ptrstd::shared_ptr)来自动管理动态分配的内存,以避免手动管理内存的复杂性和风险。智能指针在其析构函数中自动释放它们所拥有的内存,从而帮助防止内存泄漏和其他常见的内存管理错误。

2. 动态分配数组

在 C++ 中,动态分配数组涉及使用 newdelete 操作符来在堆上分配和释放连续的内存块,用于存储数组。这种机制允许在运行时确定数组的大小,提供了比静态数组更大的灵活性。

使用 new[] 分配数组

new[] 用于动态分配一个元素数量已知的数组。

基本用法

int* myArray = new int[10]; // 分配一个包含 10 个整数的数组

在这个例子中,new[] 分配了一个包含 10 个整数的数组,并返回一个指向数组第一个元素的指针。分配的数组在堆上,其生命周期直到使用 delete[] 显式释放为止。

初始化数组

使用 new[] 分配的数组默认进行值初始化。对于内置类型,如 int,这意味着数组元素被初始化为零。对于类类型的元素,将调用默认构造函数。

使用 delete[] 释放数组

使用 delete[] 来释放由 new[] 分配的数组内存。

基本用法

delete[] myArray; // 释放数组内存

在这个例子中,delete[] 释放了之前分配的数组内存。如果数组元素是类对象,delete[] 还将调用每个对象的析构函数。

注意事项

  • 匹配使用 new[]delete[]:用 new[] 分配的内存必须用 delete[] 释放。使用不匹配的 delete 形式会导致未定义行为。
  • 避免内存泄漏:每个通过 new[] 分配的数组都应该使用 delete[] 释放。
  • 处理数组指针:释放内存后,指针变成悬挂指针。为了安全,应将指针设为 nullptr
  • 数组大小不可更改:一旦数组被分配,其大小将固定,不能更改。如果需要可调整大小的数组,可以考虑使用 std::vector

替代方案

在现代 C++ 中,推荐使用 std::vector 或其他标准容器来替代手动管理的动态数组。标准容器提供了更安全、更方便的方式来处理动态分配的数组,自动管理内存,并提供了许多额外的功能,如自动调整大小和元素访问的边界检查。

3. 处理内存分配失败

在 C++ 中,动态内存分配可能会失败,尤其是在内存资源紧张的情况下。处理内存分配失败是动态内存管理的一个重要方面,确保程序在内存不足时能够可靠地运行。

new 的内存分配失败

当使用 new 进行内存分配时,如果系统无法满足分配请求,将抛出 std::bad_alloc 异常。这是 C++ 标准库中定义的一个异常类型,用来表示内存分配失败。

示例

try {
    int* myArray = new int[1000000000]; // 尝试分配大量内存
} catch (const std::bad_alloc& e) {
    std::cerr << "Memory allocation failed: " << e.what() << std::endl;
    // 处理内存分配失败的情况
}

在这个例子中,如果内存分配失败,程序会捕获 std::bad_alloc 异常,并通过 catch 块来处理这种情况。

new (nothrow) 和内存分配失败

C++ 还提供了一种不抛出异常的内存分配方式,即 new (std::nothrow)。如果使用 new (std::nothrow) 进行内存分配,当内存分配失败时,它不会抛出异常,而是返回一个空指针。

示例

#include <new> // 引入 std::nothrow

int* myArray = new(std::nothrow) int[1000000000];
if (myArray == nullptr) {
    std::cerr << "Memory allocation failed." << std::endl;
    // 处理内存分配失败的情况
}

在这个例子中,如果内存分配失败,则 myArray 会被设置为 nullptr,程序可以检查这一点并相应地处理。

注意事项

  • 及时处理分配失败:应检查每次 newnew (nothrow) 调用的返回值,并在内存分配失败时进行适当处理。
  • 避免资源泄露:在处理内存分配失败时,应确保已分配资源的正确释放,避免资源泄露。
  • 考虑备选方案:在内存紧张的环境中,考虑使用更小的内存请求,或使用备选的数据结构和算法。

4. 智能指针

在 C++ 中,智能指针是用于自动管理动态分配的内存的对象。智能指针通过在其析构函数中释放关联的内存,帮助避免内存泄漏和指针错误。标准库提供了几种类型的智能指针,最常用的包括 std::unique_ptrstd::shared_ptrstd::weak_ptr

std::unique_ptr

std::unique_ptr 是一个独占式智能指针,它拥有其指向的对象,并在 unique_ptr 对象销毁时自动删除关联的对象。

特点

  • 独占所指向的对象。
  • 不允许复制,但可以移动。
  • 自动释放所拥有的对象。

示例

#include <memory>

std::unique_ptr<int> ptr(new int(10)); // 创建 unique_ptr

std::shared_ptr

std::shared_ptr 是一个共享式智能指针,它允许多个 shared_ptr 实例共享同一个对象。当最后一个拥有对象的 shared_ptr 被销毁时,对象被删除。

特点

  • 共享对象的所有权。
  • 使用引用计数来跟踪拥有对象的 shared_ptr 数量。
  • 允许复制和移动。

示例

std::shared_ptr<int> shared1(new int(20));
std::shared_ptr<int> shared2 = shared1; // 共享所有权

std::weak_ptr

std::weak_ptr 是一种不拥有对象的智能指针,它被设计用来观察 std::shared_ptr,但不影响其引用计数。

特点

  • 不拥有对象,不影响 shared_ptr 的引用计数。
  • 用来解决 shared_ptr 相互引用导致的循环引用问题。
  • 可以从 shared_ptr 或另一个 weak_ptr 创建。

示例

std::shared_ptr<int> shared(new int(30));
std::weak_ptr<int> weak = shared; // 观察 shared_ptr

管理策略

  • unique_ptr 适用于资源的独占管理。
  • shared_ptr 适用于资源的共享管理,需要注意循环引用问题。
  • weak_ptr 用于“安全观察” shared_ptr 所管理的资源,避免循环引用。

总结

智能指针在自动管理资源方面提供了极大的方便,是现代 C++ 中管理动态资源的首选方法。使用智能指针可以减少直接使用 newdelete 的需要,降低内存泄漏和其他内存错误的风险。

5. 自定义内存管理

在 C++ 中,自定义内存管理是指开发者控制内存分配和释放的过程,而不完全依赖于标准的 newdelete 操作符或智能指针。这通常涉及到为特定类型的对象创建专用的内存池、使用自定义的分配器或实现特定的内存分配策略。

内存池

内存池是一种内存分配技术,预先分配一大块内存,并从中分配小块内存给对象使用。

优势

  • 性能提升:减少了频繁分配和释放内存的开销。
  • 碎片化减少:通过预分配大块连续内存,减少了内存碎片化。

适用场景

  • 对象频繁创建和销毁:例如,在高性能服务器或游戏引擎中。
  • 对象大小相同:适合管理大小固定的对象。

自定义分配器

自定义分配器允许开发者提供自定义的内存分配和释放逻辑。

实现

自定义分配器需要实现分配器的接口,包括分配、释放内存和构造、析构对象的方法。

适用场景

  • 特定的性能需求:比如,需要特定对齐的内存分配。
  • 特殊的内存管理策略:例如,限制内存使用或跟踪内存使用情况。

替代 newdelete

自定义内存管理有时也涉及重载类的 newdelete 操作符,以提供特定的内存分配和释放逻辑。

示例

class MyClass {
public:
    void* operator new(size_t size) {
        // 自定义内存分配
    }
    void operator delete(void* pointer) {
        // 自定义内存释放
    }
};

注意事项

  • 复杂性和风险:自定义内存管理增加了代码的复杂性,可能引入错误。
  • 适当使用:仅在标准内存管理方法不满足需求时考虑使用。
  • 调试和维护:自定义内存管理可能使得调试和维护变得更加困难。

总结

自定义内存管理是一种高级技术,它可以提供性能优势和特殊需求的解决方案。然而,它也带来了额外的复杂性和潜在风险。在大多数情况下,推荐使用 C++ 标准库提供的内存管理工具,如智能指针和标准分配器,因为它们提供了更好的安全性和足够的灵活性。自定义内存管理应当谨慎使用,并且只在确实有明确的性能或行为需求时采用。

6. 资源管理和异常安全

在 C++ 中,资源管理和异常安全是两个关键的概念,特别是在处理动态内存、文件句柄、网络连接等资源时。

资源管理

资源管理涉及到获取和释放资源,如动态内存、文件句柄等。在 C++ 中,资源管理通常遵循 RAII(Resource Acquisition Is Initialization)原则。

RAII 原则

RAII 是一种编程技术,用于确保资源的获取同时初始化对象(acquisition),资源的释放与对象的生命周期同步(destruction)。

示例
  • 智能指针std::unique_ptrstd::shared_ptr 在析构函数中自动释放所拥有的内存。
  • 文件和互斥锁:如 std::fstreamstd::lock_guard 在析构时释放资源。

优势

  • 自动资源管理:避免忘记释放资源,减少内存泄漏和资源泄露的风险。
  • 异常安全:即使发生异常,也能保证资源正确释放。

异常安全

异常安全是指代码能够在面对异常时,安全地释放资源并保持程序状态的一致性。

异常安全的级别

  • 基本保证:操作可能失败,但不会泄露资源,对象保持在有效状态。
  • 强保证:操作要么成功,要么对象状态不变(“事务性”)。
  • 无异常保证:保证不抛出异常。

实现技巧

  • 作用域保护:使用局部对象管理资源,利用其析构函数来释放资源。
  • 异常传播:允许异常从函数传播出去,而不是在内部捕获并处理。
  • 拷贝和交换:实现强异常安全保证的常用技术。

示例

class MyClass {
public:
    MyClass() {
        resource = new Resource();
    }
    ~MyClass() {
        delete resource;  // 资源在析构时释放
    }
private:
    Resource* resource;
};

在上述示例中,即使在构造 MyClass 期间发生异常,资源也会在其析构函数中被安全释放。

注意事项

  • 防止资源泄露:确保所有资源都在对象生命周期结束时释放。
  • 处理异常:在构造函数和析构函数中正确处理异常,特别是在多个资源需要被管理时。
  • 避免异常在析构函数中抛出:析构函数抛出异常可能导致程序终止。

资源管理和异常安全是 C++ 高效编程的关键。合理利用 RAII 原则和智能指针可以大大简化这些任务,提高程序的可靠性和维护性。

7. 对象的动态创建和销毁

在 C++ 中,对象的动态创建和销毁涉及使用 newdelete 操作符以及它们的变体。除了标准的 newdelete,还有一些特殊的用法,允许开发者更精细地控制对象的生命周期和内存布局。

定位 new(Placement new

定位 new 允许在已分配的内存上构造对象。这种方法常用于内存池、自定义分配器或避免额外的内存分配开销。

用法

#include <new>  // 必须包含头文件 <new>

char buffer[sizeof(MyClass)];
MyClass* myClassPtr = new (buffer) MyClass();  // 在 buffer 上构造对象

在这个示例中,MyClass 的对象被构造在预先分配的 buffer 上,而不是通过常规的 new 分配新内存。这种做法不会分配内存,但需要手动调用析构函数来销毁对象。

显式调用析构函数

当对象通过定位 new 构造时,或者需要在对象的生命周期结束前显式销毁对象时,可以直接调用其析构函数。

用法

myClassPtr->~MyClass();  // 显式调用析构函数

注意事项

  • 内存管理:使用定位 new 时,开发者需要负责相关内存的分配和释放。
  • 避免内存泄漏:确保为每次定位 new 调用显式调用相应的析构函数。
  • 对齐要求:当在自定义缓冲区上构造对象时,需要确保缓冲区满足对象的对齐要求。
  • 异常处理:在使用定位 new 时要注意异常安全性,确保在构造发生异常时适当地处理已分配的内存。

总结

对象的动态创建和销毁是 C++ 中的高级特性,它们提供了对对象生命周期和内存布局的细粒度控制。这些技术在需要优化性能、管理复杂资源或实现自定义内存管理策略时非常有用。然而,它们也增加了代码的复杂性和出错的可能性,因此应谨慎使用。在大多数情况下,推荐使用标准的内存管理工具和技术,如 RAII、智能指针和标准容器。

文章来源:https://blog.csdn.net/qq_39811006/article/details/135412947
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。