C++:一文读懂智能指针

简介: C++:一文读懂智能指针

C++11 引入了 3 个智能指针类型:

当使用智能指针时,我们首先需要包含 memory头文件,这个头文件包含了 C++ 标准库中智能指针的定义。

1.std::unique_ptr<T> :独占资源所有权的指针。

2.std::shared_ptr<T> :共享资源所有权的指针。

3.std::weak_ptr<T> :共享资源的观察者,需要和 std::shared_ptr 一起使用,不影响资源的生命周期。

std::auto_ptr 已被废弃。

std::unique_ptr 的使用

#include <iostream>
#include <memory>
int main() {
    // 创建一个 std::unique_ptr,指向一个动态分配的整数
    std::unique_ptr<int> ptr(new int(42));
    // 使用智能指针访问其所管理的对象
    std::cout << "值:" << *ptr << std::endl;
    // 不需要手动释放内存,当 std::unique_ptr 离开作用域时会自动释放内存
    return 0;
}

在这个示例中,std::unique_ptr ptr(new int(42)); 创建了一个 std::unique_ptr,并将其指向动态分配的整数。当 ptr 离开作用域时,它所管理的整数会自动被释放,无需手动调用 delete。

std::unique_ptr 的原理是基于资源获取即初始化(RAII)的概念。它在构造时接管了动态分配的内存,然后在析构时自动释放该内存。由于 std::unique_ptr 不能进行拷贝或赋值,因此保证了独占所有权的原则。

std::unique_ptr

简单说,当我们独占资源的所有权的时候,可以使用 std::unique_ptr 对资源进行管理——离开 unique_ptr 对象的作用域时,会自动释放资源。这是很基本的 RAII 思想。

std::unique_ptr 的使用比较简单,也是用得比较多的智能指针。这里直接看例子。

1.使用裸指针时,要记得释放内存。

{
    int* p = new int(100);
    // ...
    delete p;  // 要记得释放内存
}

1.使用 std::unique_ptr 自动管理内存。

{
    std::unique_ptr<int> uptr = std::make_unique<int>(200);
    //...
    // 离开 uptr 的作用域的时候自动释放内存
}

1.std::unique_ptr 是 move-only 的。

{
    std::unique_ptr<int> uptr = std::make_unique<int>(200);
    std::unique_ptr<int> uptr1 = uptr;  // 编译错误,std::unique_ptr<T> 是 move-only 的
    std::unique_ptr<int> uptr2 = std::move(uptr);
    assert(uptr == nullptr);
}

1.std::unique_ptr 可以指向一个数组。

{
    std::unique_ptr<int[]> uptr = std::make_unique<int[]>(10);
    for (int i = 0; i < 10; i++) {
        uptr[i] = i * i;
    }   
    for (int i = 0; i < 10; i++) {
        std::cout << uptr[i] << std::endl;
    }   
}

1.自定义 deleter。

{
    struct FileCloser {
        void operator()(FILE* fp) const {
            if (fp != nullptr) {
                fclose(fp);
            }
        }   
    };  
    std::unique_ptr<FILE, FileCloser> uptr(fopen("test_file.txt", "w"));
}

1.使用 Lambda 的 deleter。

{
    std::unique_ptr<FILE, std::function<void(FILE*)>> uptr(
        fopen("test_file.txt", "w"), [](FILE* fp) {
            fclose(fp);
        });
}

std::shared_ptr 的使用

#include <iostream>
#include <memory>
int main() {
    // 创建一个 std::shared_ptr,指向一个动态分配的整数
    std::shared_ptr<int> ptr1(new int(42));
    // 创建另一个 std::shared_ptr 指向同一块内存
    std::shared_ptr<int> ptr2 = ptr1;
    // 使用智能指针访问其所管理的对象
    std::cout << "ptr1 的值:" << *ptr1 << std::endl;
    std::cout << "ptr2 的值:" << *ptr2 << std::endl;
    // 不需要手动释放内存,当最后一个指向该内存的 std::shared_ptr 离开作用域时会自动释放内存
    return 0;
}

在这个示例中,std::shared_ptr ptr1(new int(42)); 创建了一个 std::shared_ptr,并将其指向动态分配的整数。然后 std::shared_ptr ptr2 = ptr1; 创建了另一个 std::shared_ptr 指向同一块内存。由于 std::shared_ptr 使用引用计数来管理内存,因此当最后一个指向该内存的 std::shared_ptr 离开作用域时,内存会被自动释放。

std::shared_ptr 的原理是基于引用计数的概念。每个 std::shared_ptr 都会维护一个引用计数,当有新的 std::shared_ptr 指向同一块内存时,引用计数会增加,当 std::shared_ptr 离开作用域时,引用计数会减少,当引用计数为 0 时,内存会被释放。这样可以确保在不再需要时释放内存,避免内存泄漏。

std::shared_ptr

std::shared_ptr 其实就是对资源做引用计数——当引用计数为 0 的时候,自动释放资源。

{
    std::shared_ptr<int> sptr = std::make_shared<int>(200);
    assert(sptr.use_count() == 1);  // 此时引用计数为 1
    {   
        std::shared_ptr<int> sptr1 = sptr;
        assert(sptr.get() == sptr1.get());
        assert(sptr.use_count() == 2);   // sptr 和 sptr1 共享资源,引用计数为 2
    }   
    assert(sptr.use_count() == 1);   // sptr1 已经释放
}
// use_count 为 0 时自动释放内存

和 unique_ptr 一样,shared_ptr 也可以指向数组和自定义 deleter。

{
    // C++20 才支持 std::make_shared<int[]>
    // std::shared_ptr<int[]> sptr = std::make_shared<int[]>(100);
    std::shared_ptr<int[]> sptr(new int[10]);
    for (int i = 0; i < 10; i++) {
        sptr[i] = i * i;
    }   
    for (int i = 0; i < 10; i++) {
        std::cout << sptr[i] << std::endl;
    }   
}
{
    std::shared_ptr<FILE> sptr(
        fopen("test_file.txt", "w"), [](FILE* fp) {
            std::cout << "close " << fp << std::endl;
            fclose(fp);
        });
}

std::shared_ptr 的实现原理

一个 shared_ptr 对象的内存开销要比裸指针和无自定义 deleter 的 unique_ptr 对象略大。

std::cout << sizeof(int*) << std::endl;  // 输出 8
  std::cout << sizeof(std::unique_ptr<int>) << std::endl;  // 输出 8
  std::cout << sizeof(std::unique_ptr<FILE, std::function<void(FILE*)>>)
            << std::endl;  // 输出 40
  std::cout << sizeof(std::shared_ptr<int>) << std::endl;  // 输出 16
  std::shared_ptr<FILE> sptr(fopen("test_file.txt", "w"), [](FILE* fp) {
    std::cout << "close " << fp << std::endl;
    fclose(fp);
  }); 
  std::cout << sizeof(sptr) << std::endl;  // 输出 16

无自定义 deleter 的 unique_ptr 只需要将裸指针用 RAII 的手法封装好就行,无需保存其它信息,所以它的开销和裸指针是一样的。如果有自定义 deleter,还需要保存 deleter 的信息。

shared_ptr 需要维护的信息有两部分:

指向共享资源的指针。

引用计数等共享资源的控制信息——实现上是维护一个指向控制信息的指针。

所以,shared_ptr 对象需要保存两个指针。shared_ptr 的 的 deleter 是保存在控制信息中,所以,是否有自定义 deleter 不影响 shared_ptr 对象的大小。

当我们创建一个 shared_ptr 时,其实现一般如下:

std::shared_ptr<T> sptr1(new T);

复制一个 shared_ptr :

std::shared_ptr<T> sptr2 = sptr1;

为什么控制信息和每个 shared_ptr 对象都需要保存指向共享资源的指针?可不可以去掉 shared_ptr 对象中指向共享资源的指针,以节省内存开销?

答案是:不能。 因为 shared_ptr 对象中的指针指向的对象不一定和控制块中的指针指向的对象一样。

来看一个例子。

struct Fruit {
    int juice;
};
struct Vegetable {
    int fiber;
};
struct Tomato : public Fruit, Vegetable {
    int sauce;
};
 // 由于继承的存在,shared_ptr 可能指向基类对象
std::shared_ptr<Tomato> tomato = std::make_shared<Tomato>();
std::shared_ptr<Fruit> fruit = tomato;
std::shared_ptr<Vegetable> vegetable = tomato;

另外,std::shared_ptr 支持 aliasing constructor。

template< class Y >
shared_ptr( const shared_ptr<Y>& r, element_type* ptr ) noexcept;

Aliasing constructor,简单说就是构造出来的 shared_ptr 对象和参数 r 指向同一个控制块(会影响 r 指向的资源的生命周期),但是指向共享资源的指针是参数 ptr。看下面这个例子。

using Vec = std::vector<int>;
std::shared_ptr<int> GetSPtr() {
    auto elts = {0, 1, 2, 3, 4};
    std::shared_ptr<Vec> pvec = std::make_shared<Vec>(elts);
    return std::shared_ptr<int>(pvec, &(*pvec)[2]);
}
std::shared_ptr<int> sptr = GetSPtr();
for (int i = -2; i < 3; ++i) {
    printf("%d\n", sptr.get()[i]);
}

看上面的例子,使用 std::shared_ptr 时,会涉及两次内存分配:一次分配共享资源对象;一次分配控制块。C++ 标准库提供了 std::make_shared 函数来创建一个 shared_ptr 对象,只需要一次内存分配。

这种情况下,不用通过控制块中的指针,我们也能知道共享资源的位置——这个指针也可以省略掉。

std::weak_ptr

std::weak_ptr 要与 std::shared_ptr 一起使用。 一个 std::weak_ptr 对象看做是 std::shared_ptr 对象管理的资源的观察者,它不影响共享资源的生命周期:

1.如果需要使用 weak_ptr 正在观察的资源,可以将 weak_ptr 提升为 shared_ptr。

2.当 shared_ptr 管理的资源被释放时,weak_ptr 会自动变成 nullptr。

void Observe(std::weak_ptr<int> wptr) {
    if (auto sptr = wptr.lock()) {
        std::cout << "value: " << *sptr << std::endl;
    } else {
        std::cout << "wptr lock fail" << std::endl;
    }
}
std::weak_ptr<int> wptr;
{
    auto sptr = std::make_shared<int>(111);
    wptr = sptr;
    Observe(wptr);  // sptr 指向的资源没被释放,wptr 可以成功提升为 shared_ptr
}
Observe(wptr);  // sptr 指向的资源已被释放,wptr 无法提升为 shared_ptr

当 shared_ptr 析构并释放共享资源的时候,只要 weak_ptr 对象还存在,控制块就会保留,weak_ptr 可以通过控制块观察到对象是否存活。

enable_shared_from_this

一个类的成员函数如何获得指向自身(this)的 shared_ptr? 看看下面这个例子有没有问题?

class Foo {
 public:
  std::shared_ptr<Foo> GetSPtr() {
    return std::shared_ptr<Foo>(this);
  }
};
auto sptr1 = std::make_shared<Foo>();
assert(sptr1.use_count() == 1);
auto sptr2 = sptr1->GetSPtr();
assert(sptr1.use_count() == 1);
assert(sptr2.use_count() == 1);

成员函数获取 this 的 shared_ptr 的正确的做法是继承 std::enable_shared_from_this。

class Bar : public std::enable_shared_from_this<Bar> {
 public:
  std::shared_ptr<Bar> GetSPtr() {
    return shared_from_this();
  }
};
auto sptr1 = std::make_shared<Bar>();
assert(sptr1.use_count() == 1);
auto sptr2 = sptr1->GetSPtr();
assert(sptr1.use_count() == 2);
assert(sptr2.use_count() == 2);

一般情况下,继承了 std::enable_shared_from_this 的子类,成员变量中增加了一个指向 this 的 weak_ptr。这个 weak_ptr 在第一次创建 shared_ptr 的时候会被初始化,指向 this。

似乎继承了 std::enable_shared_from_this 的类都被强制必须通过 shared_ptr 进行管理。

auto b = new Bar;
auto sptr = b->shared_from_this();

小结

智能指针,本质上是对资源所有权和生命周期管理的抽象:

1.当资源是被独占时,使用 std::unique_ptr 对资源进行管理。

2.当资源会被共享时,使用 std::shared_ptr 对资源进行管理。

3.使用 std::weak_ptr 作为 std::shared_ptr 管理对象的观察者。

4.通过继承 std::enable_shared_from_this 来获取 this 的 std::shared_ptr 对象。

参考资料

1.智能指针

目录
相关文章
|
7天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
26 4
|
23天前
|
存储 安全 编译器
在 C++中,引用和指针的区别
在C++中,引用和指针都是用于间接访问对象的工具,但它们有显著区别。引用是对象的别名,必须在定义时初始化且不可重新绑定;指针是一个变量,可以指向不同对象,也可为空。引用更安全,指针更灵活。
|
1月前
|
存储 C++
c++的指针完整教程
本文提供了一个全面的C++指针教程,包括指针的声明与初始化、访问指针指向的值、指针运算、指针与函数的关系、动态内存分配,以及不同类型指针(如一级指针、二级指针、整型指针、字符指针、数组指针、函数指针、成员指针、void指针)的介绍,还提到了不同位数机器上指针大小的差异。
38 1
|
1月前
|
存储 编译器 C语言
C++入门2——类与对象1(类的定义和this指针)
C++入门2——类与对象1(类的定义和this指针)
29 2
|
1月前
|
存储 安全 编译器
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(一)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
1月前
|
存储 C++ 索引
C++函数指针详解
【10月更文挑战第3天】本文介绍了C++中的函数指针概念、定义与应用。函数指针是一种指向函数的特殊指针,其类型取决于函数的返回值与参数类型。定义函数指针需指定返回类型和参数列表,如 `int (*funcPtr)(int, int);`。通过赋值函数名给指针,即可调用该函数,支持两种调用格式:`(*funcPtr)(参数)` 和 `funcPtr(参数)`。函数指针还可作为参数传递给其他函数,增强程序灵活性。此外,也可创建函数指针数组,存储多个函数指针。
|
2月前
|
编译器 C++
【C++核心】指针和引用案例详解
这篇文章详细讲解了C++中指针和引用的概念、使用场景和操作技巧,包括指针的定义、指针与数组、指针与函数的关系,以及引用的基本使用、注意事项和作为函数参数和返回值的用法。
37 3
|
1月前
|
存储 编译器 程序员
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(二)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
2月前
|
C++
C++(十八)Smart Pointer 智能指针简介
智能指针是C++中用于管理动态分配内存的一种机制,通过自动释放不再使用的内存来防止内存泄漏。`auto_ptr`是早期的一种实现,但已被`shared_ptr`和`weak_ptr`取代。这些智能指针基于RAII(Resource Acquisition Is Initialization)原则,即资源获取即初始化。RAII确保对象在其生命周期结束时自动释放资源。通过重载`*`和`-&gt;`运算符,可以方便地访问和操作智能指针所指向的对象。
|
2月前
|
C++
C++(九)this指针
`this`指针是系统在创建对象时默认生成的,用于指向当前对象,便于使用。其特性包括:指向当前对象,适用于所有成员函数但不适用于初始化列表;作为隐含参数传递,不影响对象大小;类型为`ClassName* const`,指向不可变。`this`的作用在于避免参数与成员变量重名,并支持多重串联调用。例如,在`Stu`类中,通过`this-&gt;name`和`this-&gt;age`明确区分局部变量与成员变量,同时支持链式调用如`s.growUp().growUp()`。