【C++内存管理】C++智能指针

1. unique_ptr

std::unique_ptr是一种智能指针,它主要用于在C++中封装裸指针,以保证内存安全。当std::unique_ptr生命周期结束时,它会自动删除其所有权的对象。

1.1 基本功能和特性

  • 所有权独特:正如其名字 “unique”,这种类型的指针拥有其指向的对象的独特所有权,这意味着在任何时间点,只有一个unique_ptr可以指向一个给定的对象。

  • 防止内存泄漏:它自动管理生命周期,在unique_ptr生命周期结束(例如离开作用域)时,自动删除其指向的对象。这样保证了不会因为忘记手动释放内存而引起内存泄漏。

  • 不可复制但可移动unique_ptr是不可复制的,防止多个指针拥有同一对象的所有权。但是,所有权可以被移动,这意味着所有权可以从一个unique_ptr转移到另一个。

下面的代码演示了这些特性:

#include <memory>

// 定义一个函数,用于演示unique_ptr的功能
void Process(std::unique_ptr<int> ptr) {
    // 在此函数中,ptr是唯一对内存的所有者
    // 在此函数结束时,ptr将被销毁,并且其指向的内存也将被释放
}

int main() {
    std::unique_ptr<int> ptr1(new int(5));  // 分配一个新的int

    // std::unique_ptr<int> ptr2 = ptr1;  // 编译错误,unique_ptr是不可复制的

    std::unique_ptr<int> ptr3 = std::move(ptr1);  // ptr1的所有权被移动到ptr3,现在ptr1不再拥有任何对象,ptr1变为nullptr

    Process(std::move(ptr3));  // ptr3的所有权被移动到Process函数的参数,然后在函数结束时释放内存

    return 0;
}

1.2 管理动态数组

unique_ptr还可以用于管理动态数组,在这种情况下,当unique_ptr生命周期结束时,将调用 delete[] 来释放内存,而不是单个的delete

int main() {
    std::unique_ptr<int[]> arr(new int[10]);  // arr指向一个包含10个int的新数组

    for(int i = 0; i < 10; i++) {
        arr[i] = i;  // 我们可以像使用普通数组一样使用它
    }

    // 当arr离开作用域时,它会自动删除它所拥有的数组,无需手动调用 delete[] arr;
    return 0;
}

1.3 可移动构造 (MoveConstructible) 和可移动赋值 (MoveAssignable)

unique_ptr是可移动的,但不可复制。这就意味着你可以将一个unique_ptr的所有权转移给另一个unique_ptr,但你不能创建一个unique_ptr的副本。这是因为unique_ptr的设计初衷就是保证在任何时候,被管理的对象都只有一个所有者。

如下面的代码所示:

#include <memory>

std::unique_ptr<int> p1(new int(42));
std::unique_ptr<int> p2 = std::move(p1);  // Now p2 owns the int, and p1 is set to nullptr.

// This would be an error:
// std::unique_ptr<int> p3 = p2;  // This is not allowed because unique_ptr is not copyable.

1.4 转移所有权

一如既往地强调,unique_ptr对象不能被复制,但是所有权可以通过 std::move() 转移。所有权的转移也会改变原有的unique_ptr对象。当所有权从一个unique_ptr转移到另一个unique_ptr时,原unique_ptr将被设为空。这意味着任何试图使用原unique_ptr的操作都会导致未定义的行为,因为它现在是空指针。

std::unique_ptr<int> ptr1(new int(10)); 
std::unique_ptr<int> ptr2 = std::move(ptr1);  // 所有权转移
// 此时,ptr1已经不再拥有任何对象,而ptr2拥有了该对象。

1.5 自定义删除器

unique_ptr提供了使用自定义删除器的功能,这在某些情况下是很有用的。例如,当你需要一个特定的清理程序或有特定的内存管理需求时,就可以使用自定义删除器。

auto deleter = [](int* ptr) {
    std::cout << "Custom deleting. \n";
    delete ptr;
};
std::unique_ptr<int, decltype(deleter)> ptr(new int, deleter);

在上述代码中,当unique_ptr析构时,会调用指定的删除器来释放内存。

1.6 使用make_unique创建unique_ptr

为了避免直接使用 new,C++14 引入了 std::make_unique 函数模板来创建 unique_ptr。使用 make_unique 不仅使代码更简洁,还可以更好地避免内存泄漏并提供异常安全性。

auto ptr = std::make_unique<int>(10);  // 自动推导出类型为 std::unique_ptr<int>

1.7 避免原始指针操作

使用unique_ptr可以避免直接操作原始指针。原始指针可能会引起内存泄漏、野指针等问题。使用unique_ptr后,你可以放心地返回函数,知道不会泄露任何资源。

std::unique_ptr<int> safe_func() {
    return std::make_unique<int>(10);
}

2. shared_ptr 共享智能指针

std::shared_ptr 是一种智能指针,用于实现共享所有权的概念。多个 shared_ptr 可以指向相同的对象,这个对象和其相关的资源会在“最后一个引用被销毁”时释放。为了执行这个功能,shared_ptr 使用了计数机制来确保跟踪指向一个对象的指针数量。

2.1 创建 shared_ptr

创建 shared_ptr 的最直接方式是使用 std::make_shared

auto ptr = std::make_shared<int>(10);

在上述代码中,我们创建了一个 shared_ptr,它指向一个动态分配的整数。

2.2 共享所有权

shared_ptr 最关键的特性就是多个 shared_ptr 可以指向相同的对象,但没有一个 shared_ptr 比其他的更主导。每个 shared_ptr 都有等同的所有权,而当最后一个 shared_ptr 停止指向对象时,那个对象就会被删除。

std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
std::shared_ptr<int> ptr2 = ptr1;  // 复制 ptr1;引用计数增加

// ptr1 和 ptr2 都销毁时,内存被释放

2.3 引用计数

如前所述,shared_ptr 通过引用计数实现共享所有权。每当有新的 shared_ptr 指向对象,引用计数就会增加;当 shared_ptr 被销毁(或重新指向其他对象),引用计数就会减少。当引用计数变为0时,对象就会被删除。

我们可以通过 use_count 方法来查看当前的引用计数:

auto ptr1 = std::make_shared<int>(10);
std::cout << ptr1.use_count() << '\n';  // 输出:1

auto ptr2 = ptr1;
std::cout << ptr1.use_count() << '\n';  // 输出:2

注意,use_count 主要用于调试目的,它并不是一个常数时间操作。而且,即使我们可以看到 use_count 的值,但在多线程环境中,我们不能使用它来可靠地判断是否存在其他 shared_ptr,因为在你检查 use_count 之后,可能会有其他 shared_ptr 被创建或销毁。

2.4 自定义删除器

unique_ptr 一样,shared_ptr 也支持自定义删除器:

auto deleter = [](int* ptr) {
    std::cout << "Custom deleting. \n";
    delete ptr;
};
std::shared_ptr<int> ptr(new int, deleter);

在上述代码中,我们提供了一个自定义的删除器函数。当 shared_ptr 需要删除其管理的对象时,它会调用这个删除器。

2.5 make_shared vs shared_ptr 的直接使用

创建 shared_ptr 时,推荐使用 std::make_shared,原因有两点:性能和安全性。std::make_shared 会一次性分配内存,存储对象和控制块(包含引用计数等信息),这有助于提高内存使用效率。而直接使用 shared_ptr 构造函数则需要两次分配内存,一次用于对象,一次用于控制块。另外,std::make_shared 可以更好地防止由于异常导致的内存泄漏。

2.6 weak_ptr 的用途

std::shared_ptr 有一个相关的智能指针类型:std::weak_ptrweak_ptr 是为了解决 shared_ptr 中可能出现的循环引用问题而存在的。我们会在下面进行详细讨论 weak_ptr

3. shared_ptr 循环引用问题

尽管 shared_ptr 提供了自动管理和删除动态分配的对象的便利,但如果不恰当地使用,它也可能引发问题。一个典型的问题就是循环引用。

循环引用发生在两个或多个对象相互持有对方的 shared_ptr 时。在这种情况下,每个对象都有一个指向它的 shared_ptr,因此它们的引用计数永远不会达到零,即使它们在实际上已经不再可访问。

以下代码展示了这样的一个例子:

class B;  // 预声明 B 类

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

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

int main() {
    {
        std::shared_ptr<A> a(new A());
        std::shared_ptr<B> b(new B());
        a->ptr = b;
        b->ptr = a;
    }  // 当离开这个块时,a 和 b 的 shared_ptr 将被销毁,但是由于循环引用,A 和 B 对象并未被删除

    return 0;
}

在上述代码中,我们创建了两个对象 A 和 B,并让 A 持有 B 的 shared_ptr,B 持有 A 的 shared_ptr。这就形成了一个循环引用,所以当我们试图删除 a 和 b 时,A 和 B 对象并未被删除,因为它们的引用计数并未达到零。

这就是循环引用问题。这个问题的结果是内存泄漏:尽管我们的意图是在 shared_ptr 被销毁时删除对象,但由于循环引用,对象的内存并未被释放。

4. weak_ptr 解决循环引用

std::weak_ptr 是一种特殊的智能指针,它指向一个由 std::shared_ptr 管理的对象,但它不会增加该对象的引用计数。由于 std::weak_ptr 不会增加引用计数,因此它不会阻止所指向的对象被删除。这就是 weak_ptr 如何解决循环引用问题的:将引发循环的其中一个 shared_ptr 替换为 weak_ptr

下面的代码示例显示了如何使用 weak_ptr 来解决前面提到的循环引用问题:

class B;  // 预声明 B 类

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

class B {
public:
    std::weak_ptr<A> ptr;  // 注意这里我们使用了 weak_ptr 而不是 shared_ptr
    ~B() { std::cout << "B deleted\n"; }
};

int main() {
    {
        std::shared_ptr<A> a(new A());
        std::shared_ptr<B> b(new B());
        a->ptr = b;
        b->ptr = a;  // B 现在持有 A 的 weak_ptr
    }  // 当离开这个块时,A 和 B 对象被成功删除

    return 0;
}

在上述代码中,我们创建了两个对象 A 和 B,但这次 B 持有 A 的 weak_ptr 而不是 shared_ptr。因此,即使 A 持有 B 的 shared_ptr,但 B 并不持有 A 的 shared_ptr,这样就没有循环引用,当我们试图删除 a 和 b 时,A 和 B 对象都会被成功删除。

注意:当你需要从 weak_ptr 获取 shared_ptr 时,你可以使用 weak_ptrlock 函数。但这并不总是成功,因为所指向的对象可能已经被删除。因此,返回的 shared_ptr 可能为空。

std::weak_ptr<int> weak;
{
    auto shared = std::make_shared<int>(42);
    weak = shared;
}
// 此时,shared 已经销毁,所以从 weak 中获取 shared_ptr 会得到一个空的 shared_ptr。
auto sharedFromWeak = weak.lock();  // 返回一个空的 shared_ptr
if (sharedFromWeak) {
    // 这里的代码不会被执行,因为 sharedFromWeak 是空的。
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ricky_0528

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值