总结 C++ 动态内存管理_1

unique_ptr

unique_ptr 是 C++11 引入的一种智能指针(一种抽象数据类型),定义在 <memory> 头文件中。它是标准库提供的一种工具,用于管理动态分配的内存,确保该类型的对象维护的指针拥有内存的独占所有权(unique ownership)。换句话说,一个 unique_ptr 类型的对象维护的指针在整个程序的生命周期内只有一个,这不同于 shared_ptr 对象维护的指针可以是同一个,即被共享。当 unique_ptr 对象被销毁(例如离开作用域)时,它所管理的动态内存会自动被释放,从而防止内存泄漏。

unique_ptr 是对裸指针(raw pointer)的封装,旨在替代手动管理内存(newdelete)的传统方式,提供更安全、更现代的内存管理机制。

从代码层面来看,我不能执行如下的操作:

int main()
{
    std::unique_ptr<int> x(new int(3));
    std::unique_ptr<int> y = x;
}

这段代码执行的逻辑是我在堆上声明了一个 int 类型大小的内存空间并通过直接初始化的方式直接在内存中写入3,然后将表达式求值后的结果,即得到对应的地址通过构造初始化的方式写入栈上 x 对应的内存空间中,而我想将 x 对应的内存以拷贝初始化的方式写入栈上 y 对应的内存空间中,对于 unique_ptr 类型的对象维护的地址出现多次是非法的,这种操作是在编译期通过检查 unique_ptr 类型对象的赋值操作判断的。那么保证 unique_ptr 对象维护的指针在程序的整个生命周期内只有一个的原则,可以实现如下的操作:

int main()
{
    std::unique_ptr<int> x(new int(3));
    std::cout << x.get() << std::endl;
    std::unique_ptr<int> y = std::move(x);
    std::cout << x.get() << std::endl;
    std::cout << y.get() << std::endl;
}

这个操作不同的区别是我将左值 x 构造成一个将亡值将 x 对应的内存数据移动到到另一块内存中,在 ; 结束后,栈上 x 对应的内存空间被释放,从而保证了 unique_ptr 对象维护的指针只有一个。如果打印 x 值可以得到此时 x 的值为 0。

通过函数的返回值同样可以构造一个将亡值,因此可以实现如下的操作:

std::unique_ptr<int> func()
{
    std::unique_ptr<int> res(new int(3));
    return res;
}

int main()
{
    std::unique_ptr<int> x = func();
}

从内存层面理解 unique_ptr

从内存管理的角度看,unique_ptr 的核心在于其独占所有权自动释放机制。

  1. 内存分配
    当创建 unique_ptr 时(例如通过 newstd::make_unique),它在堆上分配内存,并将其地址存储在 unique_ptr 内部的原始指针中。

  2. 所有权管理
    unique_ptr 的本质是一个轻量级数据类型(通常只包含一个裸指针和可能的删除器),其大小接近于原始指针。它通过 C++ 的移动语义(std::move)确保内存地址的所有权只能属于一个 unique_ptr 实例。移动后,原 unique_ptr 的内部指针被置为 nullptr,避免重复释放。

  3. 内存释放
    unique_ptr 对象超出作用域或被销毁时,其析构函数会自动调用删除器(默认是 delete),释放堆上的内存。

  4. 删除器(Deleter)
    unique_ptr 支持自定义删除器,用于特殊资源管理(例如释放文件句柄)。删除器是编译时绑定的,不会增加运行时开销。

    auto deleter = [](int* p) { std::cout << "Custom delete\n"; delete p; };
    std::unique_ptr<int, decltype(deleter)> ptr(new int(10), deleter);
    
  5. 内存开销

    • 默认情况下,unique_ptr 只存储一个原始指针,大小与 sizeof(void*) 相同。
    • 如果使用自定义删除器,可能增加少量存储(取决于删除器类型)。

使用 unique_ptr

1. 创建 unique_ptr

可以用 std::make_unique(C++14 引入)或直接用构造函数创建:

#include <memory>
#include <iostream>

int main() {
    // 使用 std::make_unique (推荐)
    auto ptr1 = std::make_unique<int>(42);

    // 直接用构造函数
    std::unique_ptr<int> ptr2(new int(10));

    std::cout << *ptr1 << " " << *ptr2 << std::endl; // 输出: 42 10
    return 0;
}
  • std::make_unique 是首选方式,因为它更安全(避免了异常情况下内存泄漏的风险)且更简洁。

2. 访问和管理资源

  • *-> 访问管理的对象。
  • .get() 获取原始指针(但不转移所有权)。
struct MyClass {
    void sayHi() { std::cout << "Hello!\n"; }
};

int main() {
    auto ptr = std::make_unique<MyClass>();
    ptr->sayHi();              // 输出: Hello!
    MyClass* raw = ptr.get();  // 获取原始指针
    raw->sayHi();              // 输出: Hello!
    return 0;
}

3. 所有权转移

unique_ptr 不允许拷贝,但可以通过 std::move 转移所有权:

int main() {
    auto ptr1 = std::make_unique<int>(100);
    std::unique_ptr<int> ptr2 = std::move(ptr1); // 转移所有权
    // ptr1 现在为空(nullptr),ptr2 拥有资源
    if (!ptr1) std::cout << "ptr1 is null\n"; // 输出: ptr1 is null
    std::cout << *ptr2 << std::endl;          // 输出: 100
    return 0;
}

4. 释放和重置

  • .release():放弃所有权,返回原始指针,unique_ptr 变为空。
  • .reset():释放当前管理的对象,并可选地接管新对象。
int main() {
    auto ptr = std::make_unique<int>(50);
    int* raw = ptr.release(); // ptr 放弃所有权,raw 接管
    std::cout << *raw << std::endl; // 输出: 50
    delete raw; // 需要手动释放

    ptr.reset(new int(75)); // 重置为新对象
    std::cout << *ptr << std::endl; // 输出: 75
    return 0;
}

总之记住 不能拷贝,只能移动:

auto ptr1 = std::make_unique<int>(10);
// std::unique_ptr<int> ptr2 = ptr1; // 错误!不能拷贝
std::unique_ptr<int> ptr2 = std::move(ptr1); // 正确

用作函数参数或返回值,表示所有权转移。


weak_ptr

weak_ptr 是 C++11 引入的一种智能指针,定义在 <memory> 头文件中。它是 shared_ptr 的辅助工具,用于解决 shared_ptr 在特定场景下可能导致的循环引用问题。weak_ptr 本身并不拥有资源的所有权,也不会增加资源的引用计数,而是提供了一种“弱引用”机制,用于访问由 shared_ptr 管理的对象。简单来说:

  • weak_ptr 是一个非拥有型的智能指针,它指向由 shared_ptr 管理的对象。
  • 它不会影响对象的引用计数(use_count),因此不会延长对象的生命周期。
  • 当所有 shared_ptr 都被销毁,对象被释放时,weak_ptr 会自动变为“过期”(expired),无法再访问该对象。

weak_ptr 的引入主要是为了解决 shared_ptr 的循环引用问题。

shared_ptr 的循环引用问题

假设有两个类 AB,它们通过 shared_ptr 互相引用:

#include <memory>
#include <iostream>

class B;
class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::shared_ptr<A> a_ptr;
    ~B() { std::cout << "B destroyed\n"; }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    a->b_ptr = b;  // A 持有 B 的 shared_ptr
    b->a_ptr = a;  // B 持有 A 的 shared_ptr
    return 0;      // 循环引用导致内存泄漏
}

在上述代码中:

  • ab 分别是 ABshared_ptr
  • a->b_ptr 持有 bb->a_ptr 持有 a,形成循环引用。
  • main 函数结束时,ab 的引用计数并不会降为 0(因为彼此的 shared_ptr 还在互相引用),导致 AB 的对象无法被销毁,产生内存泄漏。

从实际内存角度来讲,在堆上有两个类的实例分别占有两块内存由栈上 a 和 b 两个 shared_ptr 类型的实例分别指向这两块内存,此时这两块内存的引用计数分别为 1,而堆上的这两个对象分别在各自的内存空间中维护一个 shared_ptr 类型,通过 a->b_ptr = b; b->a_ptr = a; 的方式完成互指,即循环引用,这造成的结果是此时这两块内存空间的引用计数分别为 2,而 a 和 b 离开作用域后,引用计数减 1,此时这两块内存空间的引用计数为 1,并不会释放对应的内存空间,造成内存泄露,原因是在原先指向堆上的 shared_ptr 离开作用域后我们没法直接再操作堆上这两块内存空间中对应的 shared_ptr ,可以发现只要出现这种循环引用的现象就会导致内存泄漏。这要断开这种循环引用就能解决这种问题,比如:

#include <memory>
#include <iostream>

class B;
class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::shared_ptr<A> a_ptr;
    ~B() { std::cout << "B destroyed\n"; }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    a->b_ptr = b;  // A 持有 B 的 shared_ptr
    // b->a_ptr = a;  // B 持有 A 的 shared_ptr
    return 0;      // 循环引用导致内存泄漏
}

离开作用域时可以确保内存的释放,但是如果确实有循环引用的需求怎么办?比如,循环链表,树,图等数据结构,要从根源上解决问题,因此就引入了 weak_ptr 只要确保不增加引用计数就行。

weak_ptr 如何解决

将其中一个 shared_ptr 改为 weak_ptr,打破循环引用。例如:

class B;
class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::weak_ptr<A> a_ptr;  // 改为 weak_ptr
    ~B() { std::cout << "B destroyed\n"; }
};

现在:

  • A 通过 shared_ptr 持有 B
  • B 通过 weak_ptr 持有 Aweak_ptr 不增加 A 的引用计数。
  • main 函数结束时,a 的引用计数变为 0,A 被销毁;随后 b 的引用计数变为 0,B 被销毁。内存泄漏问题解决。

因此,weak_ptr 的核心作用是避免循环引用,确保资源能被正确释放。

如何优雅使用 weak_ptr?

weak_ptr 的使用通常与 shared_ptr 配合,以下是常见操作:

3.1 创建 weak_ptr

weak_ptr 通常从 shared_ptr 构造:

std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp;  // 从 shared_ptr 创建 weak_ptr

3.2 检查是否过期

由于 weak_ptr 不拥有资源,底层对象可能已经被释放,因此需要检查其有效性,使用 expired() 方法:

if (wp.expired()) {
    std::cout << "weak_ptr 已过期\n";
}

3.3 访问对象

weak_ptr 本身不能直接解引用(*wpwp-> 是非法的),需要通过 lock() 方法获取一个 shared_ptr

if (auto sp_temp = wp.lock()) {  // lock() 返回 shared_ptr
    std::cout << "值: " << *sp_temp << "\n";  // 安全访问
} else {
    std::cout << "对象已释放\n";
}
  • 如果 weak_ptr 未过期,lock() 返回有效的 shared_ptr,引用计数会暂时增加。
  • 如果已过期,lock() 返回空的 shared_ptr

3.4 示例代码

#include <memory>
#include <iostream>

int main() {
    std::weak_ptr<int> wp;
    {
        std::shared_ptr<int> sp = std::make_shared<int>(100);
        wp = sp;  // wp 指向 sp 管理的对象
        if (auto temp = wp.lock()) {
            std::cout << "值: " << *temp << "\n";  // 输出 100
        }
    }  // sp 销毁,对象释放
    if (wp.expired()) {
        std::cout << "weak_ptr 已过期\n";  // 输出此句
    }
    return 0;
}

为了理解 weak_ptr 的内存机制,我们需要看看 shared_ptrweak_ptr 的底层实现。

当创建 shared_ptr 时(例如通过 std::make_shared),会分配一个控制块(Control Block),用于管理:

  • 强引用计数use_count):表示有多少 shared_ptr 拥有该资源。
  • 弱引用计数:表示有多少 weak_ptr 引用该资源。
  • 资源的指针和析构函数。

控制块的生命周期由强引用和弱引用的总数决定:

  • 当强引用计数为 0 时,资源被释放(调用析构函数)。
  • 当强引用和弱引用计数都为 0 时,控制块本身被销毁。

4.2 weak_ptr 的作用

  • 不影响强引用计数weak_ptr 只增加弱引用计数,不影响资源释放。
  • 依赖控制块weak_ptr 通过控制块追踪资源状态。当强引用计数为 0,资源释放后,控制块保留弱引用计数,weak_ptr 可通过 expired() 检测到资源已不可用。
  • lock() 的实现:调用 lock() 时,检查控制块中的强引用计数。如果大于 0,则创建一个新的 shared_ptr,强引用计数加 1;否则返回空指针。

4.3 内存管理示例

假设:

  1. shared_ptr<int> sp = std::make_shared<int>(42)
    • 分配资源(int)和控制块。
    • 强引用计数 = 1,弱引用计数 = 0。
  2. weak_ptr<int> wp = sp
    • 弱引用计数增至 1,强引用计数仍为 1。
  3. sp 销毁:
    • 强引用计数变为 0,资源释放。
    • 弱引用计数仍为 1,控制块保留。
  4. wp 调用 lock()
    • 检查强引用计数为 0,返回空 shared_ptr
  5. wp 销毁:
    • 弱引用计数变为 0,控制块释放。