Skip to content

C++ 动态内存

理解动态内存管理在 C++ 中至关重要。C++ 程序通常使用两个主要区域进行内存分配:

  • 栈(The Stack): 用于局部变量、函数参数和返回地址的内存。分配和释放是自动且快速的(LIFO - 后进先出)。大小通常有限。
  • 堆(The Heap)/自由存储区(Free Store): 程序执行期间可用于动态分配的一大块内存池。分配和释放是手动的(或通过智能指针管理),可能较慢,但在编译时不知道数据大小或生命周期,或者数据需要比创建它的函数存活更久时是必需的。

动态内存分配允许您在运行时从堆中请求内存,此时所需的精确数量事先未知。

裸指针:new 和 delete(谨慎使用!)

Section titled “裸指针:new 和 delete(谨慎使用!)”

历史上,C++ 使用 new 运算符在堆上分配内存,使用 delete 运算符释放内存。

// 为一个 double 分配内存
double* pValue = new double;
*pValue = 3.14159; // 使用已分配的内存
// ... 稍后 ...
delete pValue; // *** 关键:释放内存 ***
pValue = nullptr; // 好习惯:避免悬空指针

分配数组:

// 为包含 10 个整数的数组分配内存
int* pArray = new int[10];
pArray[0] = 100; // 使用已分配的数组
// ... 稍后 ...
delete[] pArray; // *** 关键:对数组使用 delete[] ***
pArray = nullptr;

使用原始 new/delete 的问题:

  • 内存泄漏(Memory Leaks): 忘记调用 delete 或 delete[] 会导致内存泄漏(内存仍然分配着但无法使用)。
  • 悬空指针(Dangling Pointers): 在调用 delete 后通过指针访问内存会导致未定义行为。
  • 重复释放(Double Deletion): 对同一个指针调用 delete 或 delete[] 两次会导致未定义行为。
  • 异常安全(Exception Safety): 如果在 new 和 delete 之间发生异常,delete 可能被跳过,导致泄漏。
  • 所有权混淆(Ownership Confusion): 通常不清楚代码的哪一部分负责删除内存。

处理分配失败:默认情况下,如果 new 未能分配内存(例如,内存不足),它会抛出类型为 std::bad_alloc(定义在 <new> 中)的异常。您应该使用 try...catch 块来处理这种情况。

#include <iostream>
#include <new> // 需要包含 std::bad_alloc
int main() {
double* pHugeArray = nullptr;
try {
// 尝试分配大量内存(可能失败)
pHugeArray = new double[1000000000000];
std::cout << "内存分配成功(不太可能!)" << std::endl;
// 在此处使用 pHugeArray...
} catch (const std::bad_alloc& e) {
std::cerr << "内存分配失败: " << e.what() << std::endl;
// 适当处理错误
return 1; // 退出或恢复
}
// 如果分配成功,记住要删除
delete[] pHugeArray;
pHugeArray = nullptr;
return 0;
}

还有一个非抛出异常的版本:new(std::nothrow) type。它在失败时返回 nullptr 而不是抛出异常,需要手动进行空指针检查。然而,在现代 C++ 中,通常更推荐使用抛出异常的版本。

现代 C++:智能指针(推荐方式)

Section titled “现代 C++:智能指针(推荐方式)”

现代 C++ 强烈建议使用智能指针(smart pointers)(定义在 <memory> 中)来管理动态内存。它们使用 RAII(Resource Acquisition Is Initialization,资源获取即初始化)原则自动化释放过程:在智能指针的构造函数中获取内存,并在其析构函数中(当智能指针超出作用域时)自动释放。

std::unique_ptr: 表示独占所有权。任何时候只有一个 unique_ptr 可以指向某个资源。当 unique_ptr 被销毁(超出作用域)时,它会自动删除管理的*对象。它轻量级,是单所有权场景的首选。

#include <iostream>
#include <memory> // 需要包含智能指针
#include <vector>
class MyResource {
public:
MyResource() { std::cout << "MyResource Created\n"; }
~MyResource() { std::cout << "MyResource Destroyed\n"; }
void use() { std::cout << "Using MyResource\n"; }
};
int main() {
// 创建一个管理 MyResource 对象的 unique_ptr
// 使用 std::make_unique (C++14+) 进行更安全的分配
auto ptr1 = std::make_unique<MyResource>();
// 通过 unique_ptr 使用资源(行为类似于裸指针)
ptr1->use();
// 用于数组的 unique_ptr
auto ptrArray = std::make_unique<int[]>(5); // 包含 5 个整数的数组
ptrArray[0] = 10;
std::cout << "ptrArray[0] = " << ptrArray[0] << std::endl;
// 无需手动调用 delete 或 delete[]!
// 当 ptr1 和 ptrArray 超出作用域时,内存会自动释放。
std::cout << "Exiting main...\n";
return 0;
}
// 输出将显示 MyResource Created, Using MyResource, ptrArray[0]=10, Exiting main..., MyResource Destroyed

std::shared_ptr: 表示共享所有权。多个 shared_ptr 实例可以指向同一个资源。只有当指向该资源的*最后一个 shared_ptr 被销毁或重置时,资源才会被删除。它内部使用引用计数(reference counting),与 unique_ptr 相比会增加一些开销。

#include <iostream>
#include <memory>
#include <vector>
class MyResource { // 定义和前面 unique_ptr 示例中的 MyResource 相同
public:
MyResource() { std::cout << "MyResource Created\n"; }
~MyResource() { std::cout << "MyResource Destroyed\n"; }
void use() { std::cout << "Using MyResource\n"; }
};
int main() {
// 使用 std::make_shared(推荐方式)
auto shared_res = std::make_shared<MyResource>();
std::cout << "Initial use count: " << shared_res.use_count() << std::endl; // 通常是 1
std::vector<std::shared_ptr<MyResource>> ptr_vec;
ptr_vec.push_back(shared_res); // 复制会增加使用计数
std::cout << "Use count after vector push_back: " << shared_res.use_count() << std::endl; // 通常是 2
auto another_shared = shared_res; // 另一个复制
std::cout << "Use count after assignment: " << another_shared.use_count() << std::endl; // 通常是 3
shared_res->use();
// 指针超出作用域,使用计数减少。
// 当使用计数降至 0 时,资源被删除。
std::cout << "Exiting main...\n";
return 0;
}
// 输出显示使用计数的变化以及资源在结束时被销毁。

std::weak_ptr: shared_ptr 的非拥有(non-owning)伴侣。它指向一个由 shared_ptr 管理的对象,但不影响引用计数。用于打破涉及 shared_ptr 的潜在循环引用(circular references)。

总结: 优先使用智能指针(std::unique_ptr、std::shared_ptr)而非原始的 new 和 delete。使用 std::make_unique 和 std::make_shared 进行更安全且可能更高效的分配。这种方法很大程度上消除了内存泄漏,并使代码更安全、更易于管理。

当您使用 new 或智能指针动态分配对象时,它们的构造函数会被调用。当它们被释放(通过 delete 或智能指针析构)时,它们的析构函数会被调用。

#include <iostream>
#include <memory>
class Box {
public:
Box() { std::cout << "Constructor called!\n"; }
~Box() { std::cout << "Destructor called!\n"; }
};
int main() {
std::cout << "Creating unique_ptr for single Box:\n";
{
auto singleBoxPtr = std::make_unique<Box>();
// 构造函数在此处调用
} // singleBoxPtr 超出作用域时,析构函数在此处调用
std::cout << "\nCreating unique_ptr for Box array:\n";
{
// 分配包含 4 个 Box 对象的数组
auto myBoxArray = std::make_unique<Box[]>(4);
// 4 个构造函数调用在此处发生
} // myBoxArray 超出作用域时,4 个析构函数调用在此处发生
std::cout << "\nExiting main.\n";
return 0;
}

输出演示了由 std::unique_ptr 自动管理的构造函数/析构函数调用:

Creating unique_ptr for single Box:
Constructor called!
Destructor called!
Creating unique_ptr for Box array:
Constructor called!
Constructor called!
Constructor called!
Constructor called!
Destructor called!
Destructor called!
Destructor called!
Destructor called!
Exiting main.