C++的智能指针unique_ptr、shared_ptr和weak_ptr

2023-07-30 15:53:59 浏览数 (3)

C 的智能指针是一种特殊的指针类型,它能够自动管理内存资源,避免常见的内存泄漏和多次释放等问题。C 11引入了三种主要的智能指针:unique_ptr、shared_ptr和weak_ptr。

①unique_ptr

在C 中,unique_ptr是一个智能指针(smart pointer)类模板,用于管理动态分配的内存资源,它提供了自动释放内存的功能。与原始指针相比,unique_ptr有更高的安全性和易用性。

unique_ptr具有以下特点:

独占所有权:每个unique_ptr实例拥有对其所指向对象的唯一所有权。这意味着在任何时候只有一个unique_ptr可以指向一个特定的对象。

自动释放内存:当unique_ptr超出作用域或被重新赋值时,它所管理的内存会自动释放。这样就避免了内存泄漏的问题。

指针语义:unique_ptr的使用方式与原始指针相似,可以通过指针操作符(->)和解引用操作符(*)来访问所指向对象的成员。

不可拷贝:unique_ptr是不可拷贝的,即不能进行复制构造和赋值操作。这是为了确保独占所有权的特性,防止多个指针同时管理同一个对象的内存。

支持移动语义:unique_ptr支持移动构造和移动赋值操作,可以将所有权转移给新的unique_ptr,而无需进行内存拷贝。

可自定义删除器:unique_ptr可以通过模板参数来指定一个删除器(deleter)函数对象,用于在释放内存时执行额外的清理操作。

示例代码:

代码语言:javascript复制
#include <memory>

int main() {
    // 创建一个unique_ptr,指向一个动态分配的int对象
    std::unique_ptr<int> ptr(new int(42));

    // 使用指针操作符和解引用操作符访问所指向对象的值
    std::cout << *ptr << std::endl;  // 输出: 42

    // 通过移动构造函数将所有权转移给另一个unique_ptr
    std::unique_ptr<int> ptr2 = std::move(ptr);

    // 注意,此时ptr已经为空指针,不再拥有对象的所有权
    std::cout << *ptr2 << std::endl;  // 输出: 42

    // 使用自定义删除器
    struct Deleter {
        void operator()(int* p) {
            std::cout << "Custom deleter called" << std::endl;
            delete p;
        }
    };

    std::unique_ptr<int, Deleter> ptr3(new int(100), Deleter());

    // unique_ptr超出作用域时会自动释放内存,同时调用自定义删除器
    return 0;
}

常见成员函数

operator*:解引用操作符,用于获取 unique_ptr 所指向对象的引用。

operator->:箭头操作符,用于通过 unique_ptr 访问对象的成员函数或成员变量。

get:返回指向所管理对象的裸指针。

reset:重置 unique_ptr,释放当前所管理的对象并接管新的对象。

release:释放对所管理对象的控制权,并返回该指针的裸指针。

swap:交换两个 unique_ptr 的内容。

②shared_ptr

在C 中,shared_ptr是一个智能指针(smart pointer)类模板,用于管理动态分配的内存资源。与unique_ptr相比,shared_ptr可以实现多个指针共享同一块内存,并且提供了自动释放内存的功能。

shared_ptr具有以下特点:

共享所有权:多个shared_ptr实例可以同时指向同一个对象,它们共享对所指向对象的所有权。只有当所有shared_ptr都超出作用域或被重新赋值时,才会释放所管理的内存。

自动释放内存:当最后一个指向对象的shared_ptr超出作用域或被重新赋值时,它会自动释放所管理的内存。这种机制称为引用计数(reference counting),通过计数器来追踪当前有多少个shared_ptr指向同一块内存。

指针语义:shared_ptr的使用方式与原始指针相似,可以通过指针操作符(->)和解引用操作符(*)来访问所指向对象的成员。

可拷贝:shared_ptr是可拷贝的,即可以进行复制构造和赋值操作。每次拷贝会增加引用计数。当引用计数变为0时,表示没有任何shared_ptr指向该内存,会释放内存。

循环引用问题:如果存在循环引用(两个或多个对象相互持有shared_ptr),会导致内存泄漏。为了解决这个问题,可以使用弱引用指针weak_ptr。

示例代码:

代码语言:javascript复制
#include <memory>

int main() {
    // 创建一个shared_ptr,指向一个动态分配的int对象
    std::shared_ptr<int> ptr1(new int(42));

    // 使用指针操作符和解引用操作符访问所指向对象的值
    std::cout << *ptr1 << std::endl;  // 输出: 42

    // 复制构造函数,共享同一块内存
    std::shared_ptr<int> ptr2 = ptr1;

    // 增加引用计数
    std::cout << ptr1.use_count() << std::endl;  // 输出: 2

    // 通过弱引用指针weak_ptr解决循环引用问题
    std::weak_ptr<int> weakPtr = ptr1;

    // 使用lock()函数获取一个shared_ptr
    std::shared_ptr<int> ptr3 = weakPtr.lock();
    if (ptr3 != nullptr) {
        // 成功获取shared_ptr
    }

    // 减少引用计数
    ptr1.reset();

    return 0;
}

常见成员函数

operator*:解引用操作符,用于获取 shared_ptr 所指向对象的引用。

operator->:箭头操作符,用于通过 shared_ptr 访问对象的成员函数或成员变量。

get:返回指向所管理对象的裸指针。

reset:重置 shared_ptr,释放当前所管理的对象并接管新的对象。

release:释放对所管理对象的控制权,并返回该指针的裸指针。

swap:交换两个 shared_ptr 的内容。

use_count:返回当前被所有 shared_ptr 指向的对象的引用计数。

③weak_ptr

在 C 中,weak_ptr 是一种智能指针(smart pointer),用于解决循环引用问题。它是由 shared_ptr 派生而来,但不会增加引用计数,只是对所指向对象进行观察,并不拥有对象的所有权。

循环引用问题

循环引用问题指的是在使用shared_ptr管理对象时,存在两个或多个对象相互持有shared_ptr,形成一个循环引用的情况。这种情况下,每个对象的引用计数都不会变为0,导致内存泄漏。

具体来说,当两个对象相互持有shared_ptr时,它们的引用计数始终大于0,因此它们所指向的内存块永远不会被释放。即使程序使用结束,这部分内存也无法回收,造成了内存泄漏的问题。

循环引用问题的实际场景可能是两个对象之间存在双向关联,比如A对象持有shared_ptr指向B对象,而B对象也持有shared_ptr指向A对象。当这两个对象的生命周期延长,超过了程序实际需要它们的时间时,就会造成循环引用和内存泄露。

为了解决循环引用问题,C 中引入了弱引用指针weak_ptr。弱引用指针和shared_ptr不同,它不会增加引用计数,只是对所指向对象进行观察,并不拥有对象的所有权。通过弱引用指针,我们可以在需要时使用lock()函数获取一个有效的shared_ptr来操作对象,一旦对象的引用计数变为0,弱引用指针将自动失效。

使用弱引用指针可以破坏循环引用,让所有的shared_ptr都能够正常析构并释放所管理的内存,避免了潜在的内存泄漏风险。

weak_ptr 具有以下特点和用法

弱引用:因为 weak_ptr 不会增加引用计数,所以当所有 shared_ptr 都释放后,weak_ptr 将自动失效。它允许你观察一个对象,但不影响其生命周期。

通过 shared_ptr 创建:通常,我们使用 shared_ptr 来初始化 weak_ptr。这样可以确保 weak_ptr 观察的对象仍然存在。

使用 lock() 获取 shared_ptr:要操作 weak_ptr 所观察的对象,可以使用 lock() 函数获取一个有效的 shared_ptr。如果原始的 shared_ptr 已经被释放,lock() 返回一个空的 shared_ptr。

判断是否有效:可以使用 expired() 函数来检查 weak_ptr 是否已经失效,即所观察的 shared_ptr 是否已经被释放。

解决循环引用问题:由于 weak_ptr 不增加引用计数,可以用于解决两个或多个对象之间的循环引用问题,避免内存泄漏。

示例代码:

代码语言:javascript复制
#include <memory>
#include <iostream>

class B; // 前置声明

class A {
public:
    std::shared_ptr<B> bPtr;

    ~A() { std::cout << "A destroyed" << std::endl; }
};

class B {
public:
    std::weak_ptr<A> aWeakPtr;

    ~B() { std::cout << "B destroyed" << std::endl; }
};

int main() {
    std::shared_ptr<A> aPtr = std::make_shared<A>();
    std::shared_ptr<B> bPtr = std::make_shared<B>();

    aPtr->bPtr = bPtr;
    bPtr->aWeakPtr = aPtr;

    std::cout << "Use count of aPtr: " << aPtr.use_count() << std::endl;
    std::cout << "Use count of bPtr: " << bPtr.use_count() << std::endl;

    // 使用 lock() 获取有效的 shared_ptr
    std::shared_ptr<A> lockedAPtr = bPtr->aWeakPtr.lock();
    if (lockedAPtr) {
        // 使用 lockedAPtr 操作所观察的对象
        std::cout << "A exists" << std::endl;
    } else {
        std::cout << "A does not exist" << std::endl;
    }

    return 0;
}

常见成员函数

expired:检查 weak_ptr 所观察的 shared_ptr 是否已经失效。

lock:获取一个有效的 shared_ptr,用于操作所观察的对象。如果原始的 shared_ptr 已经被释放,返回一个空的 shared_ptr。

use_count:返回当前被所有 shared_ptr 指向的对象的引用计数。

reset:重置 weak_ptr。

0 人点赞