C++ 动态内存

在 C++ 中,“动态内存”通常指在运行时从堆(heap)上分配和释放的内存空间,它与栈上分配的自动变量相对。合理使用动态内存可以让程序在运行时根据需要灵活调整资源,但不当使用也极易导致内存泄漏、悬空指针和资源竞用等问题。下面从基本机制到高级管理分章介绍。


1. 原始操作:new / delete

1.1 单对象分配与释放

// 分配一个 int,初始值为 42
int* p = new int(42);

// 使用
std::cout << *p << "\n";  // 输出 42

// 释放
delete p;
p = nullptr;  // 避免成为悬空指针

1.2 数组分配与释放

// 分配一个长度为 N 的 double 数组
size_t N = 100;
double* arr = new double[N];  // 元素未初始化

// 使用 arr[i]

// 释放时必须用 delete[]
delete[] arr;
arr = nullptr;
  • 注意new[] 必须配对 delete[],否则可能只调用首元素析构,或产生未定义行为。

2. C 风格接口:malloc / free

#include <cstdlib>

int* q = static_cast<int*>(std::malloc(sizeof(int) * N));
if (!q) throw std::bad_alloc();  // malloc 失败返回 nullptr

// 使用 q[i]

// 释放
std::free(q);
q = nullptr;
  • malloc/free 属于 C 标准库,不调用构造/析构函数;在 C++ 中一般不推荐直接使用,除非与 C 代码互操作。

3. 智能指针(Smart Pointer)

为了避免手动 new/delete 带来的管理负担,C++11 起引入智能指针,通过 RAII 保证资源自动释放。

3.1 std::unique_ptr

  • 独占所有权:一个 unique_ptr 拥有某块内存,离开作用域或被重置时自动 delete
  • 禁止拷贝,可 移动
#include <memory>

auto up = std::make_unique<MyClass>(args…);
// 等价于: unique_ptr<MyClass> up(new MyClass(args…));

up->doSomething();

// 转移所有权
auto up2 = std::move(up);
if (!up) {
    // up 现在为空
}

// 离开作用域时自动 delete up2 所指对象

3.2 std::shared_ptr / std::weak_ptr

  • 共享所有权:多个 shared_ptr 可以指向同一对象,通过引用计数管理生命周期;最后一个 shared_ptr销毁时释放资源。
  • 配合 weak_ptr:解决循环引用导致的泄漏问题。
#include <memory>

auto sp1 = std::make_shared<MyClass>(args…);
{
    auto sp2 = sp1;  // 引用计数 +1
    // … 
}   // sp2 离开作用域,计数 -1

// 若要观察对象但不增加计数,用 weak_ptr
std::weak_ptr<MyClass> wp = sp1;
if (auto sp3 = wp.lock()) {
    sp3->doSomething();
} else {
    // 对象已被销毁
}
  • shared_ptr 对象大小稍大(内部含控制块指针),并有原子操作开销。

4. RAII 与容器封装

  • 尽量使用标准容器(std::vectorstd::stringstd::map…)来管理动态分配的数据,容器析构时会自动释放内部资源。
  • 对于非内存资源(文件句柄、锁等),也可封装成对象,在构造时获取资源、析构时释放(RAII 原则)。
// 例:将 FILE* 封装
class File {
    FILE* f_;
public:
    File(const char* path, const char* mode) 
      : f_(std::fopen(path, mode)) {
        if (!f_) throw std::runtime_error("open failed");
    }
    ~File() {
        if (f_) std::fclose(f_);
    }
    // 禁止拷贝,允许移动
    File(const File&) = delete;
    File(File&& o) noexcept : f_(o.f_) { o.f_ = nullptr; }
    FILE* get() const { return f_; }
};

5. 常见错误与防范

问题类型原因防范措施
内存泄漏new 后未 delete用智能指针或容器管理;代码审计
悬空指针delete 后仍使用指针delete 后置空指针;尽量减少裸指针
重复释放同一指针多次 delete统一所有权;使用智能指针
越界访问对数组或缓冲区下标检查不足使用容器的 .at();手动检查边界
对象部分析构new[] vs deletenew vs delete[] 匹配错误严格配对;尽量避免裸数组
循环引用两个或以上 shared_ptr 互相引用,计数永不为零用 weak_ptr 打破循环

6. 自定义分配器

在性能敏感或需要特殊对齐、内存池策略时,可为容器或对象提供自定义分配器。

template<typename T>
struct MyAllocator {
    using value_type = T;
    T* allocate(std::size_t n) {
        return static_cast<T*>(::operator new(n * sizeof(T)));
    }
    void deallocate(T* p, std::size_t) noexcept {
        ::operator delete(p);
    }
};

std::vector<int, MyAllocator<int>> v;
  • 自定义分配器需符合 Allocator 概念,包含 allocatedeallocatevalue_type 等。

7. 调试与工具

  • Valgrind / AddressSanitizer:检测泄漏、越界、未初始化读写等。
  • Static Analyzer:Clang-Tidy、Visual Studio Code Analysis 可静态检测潜在内存错误。
  • 智能指针覆盖率:在 code review 中优先考虑智能指针模式。

小结

  1. 原始方式new/deletemalloc/free,但易错需谨慎配对。
  2. 智能指针unique_ptrshared_ptrweak_ptr,结合 RAII 自动管控生命周期。
  3. 容器优先:优先使用 STL 容器管理动态内存,减少手动操作。
  4. 资源封装:将所有资源(内存、文件、锁)封装在对象内,析构时自动释放。
  5. 检测工具:借助动态工具和静态分析,及时发现内存相关错误。

掌握以上策略,便能在 C++ 项目中安全、高效地使用动态内存。

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注