深入理解C++智能指针:分类、使用及多线程安全性

命运对每个人都是一样的,不一样的是各自的努力和付出不同,付出的越多,努力的越多,得到的回报也越多,在你累的时候请看一下身边比你成功却还比你更努力的人,这样,你就会更有动力。

导读:本篇文章讲解 深入理解C++智能指针:分类、使用及多线程安全性,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

一、介绍

1. 普通指针的问题

在c++开发中指针是必不可少的。但是使用不当会引发一系列的bug,如常见的空指针释放,指针的重复释放,悬挂指针等等。这些错误会导致卡死程序,甚至会导致更严重的内存泄漏。

2. 智能指针的作用

为了解决指针问题c++标准库提供了智能指针。智能指针是一个封装了指针的类,它可以自动释放指针,并避免了指针的生命周期问题。智能指针还可以保证指向的内存空间在不需要的时候被及时释放。

示例展示了如何在c++中使用unique_ptr实现自动内存管理。unique_ptr是一种只能移动,不能复制的智能指针类型,可以使用std::move()将指针转移。

std::unique_ptr<int> p1(new int);
std::unique_ptr<int> p2 = p1;          // 错误,unique_ptr不支持拷贝构造
std::unique_ptr<int> p3 = std::move(p1);// 恰当的做法,使用move将所有权转移

除此之外还有shared_ptr、weak_ptr等类型的智能指针,每种智能指针都有自己独特的用法和适用场景,需要针对具体情况进行选择。使用智能指针可以帮助我们避免很多内存管理问题,让我们的代码更加安全、稳定、可靠。

二、智能指针分类

C++11标准引入了智能指针的概念,用于解决C++中的指针问题。智能指针是一种封装了原始指针的RAII类,它能够自动管理内存,避免了手动memory allocation和deletion的问题。C++标准库提供了三种类型的智能指针:shared_ptr、unique_ptr和weak_ptr。

1 shared_ptr 智能指针

shared_ptr拥有内部指针指向的内存资源。多个shared_ptr对象可以共享同一块内存资源。原始指针可以被多个shared_ptr对象管理,内存资源只有在最后一个shared_ptr销毁时才会被释放。

代码示例创建了两个shared_ptr对象p1和p2,它们共享相同的int类型内存资源。std::shared_ptr通过管理内部指针来避免内存泄漏,因为它们是RAII类,它们在销毁时会自动释放内存资源。

#include <memory>

int main()
{
    std::shared_ptr<int> p1(new int(10));
    std::shared_ptr<int> p2 = p1; // ok,p2与p1共享内存
    std::cout << "p1 use count: " << p1.use_count() << std::endl; // 输出2,表明p1和p2共享内存资源
    return 0; 
} 

2 unique_ptr 智能指针

unique_ptr指向的内存资源在任意时刻只有一个unique_ptr可以拥有。它不支持拷贝构造和赋值操作,只支持移动语义。这意味着可以通过std::move()函数将unique_ptr对象的所有权从一个对象转移到另一个对象。

代码示例创建了一个unique_ptr对象p1,它指向一个int类型的内存资源。由于unique_ptr不支持拷贝构造,因此不能将p1直接赋值给p2。可以使用std::move()函数将p1对象的所有权转移到p3。在这之后,p1成为了一个nullptr,因为它的所有权已经被p3转移。

#include <memory>

int main()
{
    std::unique_ptr<int> p1(new int(10));
    // std::unique_ptr<int> p2 = p1; // 编译错误,因为不支持拷贝构造

    std::unique_ptr<int> p3 = std::move(p1); // 通过std::move()函数将p1所有权转移
    if(p1 == nullptr)
    {
        std::cout << "p1 is nullptr" << std::endl; // 输出p1 is nullptr
    }
    return 0;
}

3 weak_ptr 智能指针

weak_ptr也是一种智能指针,它拥有的内存资源已经被一个或多个shared_ptr对象共享。weak_ptr可以从一个shared_ptr对象构造,如果最后一个shared_ptr销毁,那么它指向的内存资源将自动释放。

代码创建了一个shared_ptr对象p1,然后使用std::weak_ptr从p1创建了一个wp对象。std::weak_ptr没有自己的内存资源,但是可以从shared_ptr对象中获取内存资源。用户可以使用wp.lock()函数从std::weak_ptr中获取一个shared_ptr对象。代码的下一部分,手动销毁了p1对象。在销毁后,wp指向的内存资源已经被释放。在再次调用wp.lock()时,将返回一个nullptr,表明无法创建shared_ptr对象。

#include <memory>

int main()
{
    std::shared_ptr<int> p1(new int(10));
    std::weak_ptr<int> wp(p1);
    {
        std::shared_ptr<int> p2 = wp.lock(); // 使用weak_ptr对象创建shared_ptr对象
        if (p2 != nullptr)
        {
            std::cout << "p2: " << *p2 << std::endl;
        }
    }
    p1.reset(); // 手动销毁p1对象
    {
        std::shared_ptr<int> p3 = wp.lock();
        if (p3 == nullptr)
        {
            std::cout << "wp is expired" << std::endl; // weak_ptr无法创建shared_ptr,输出"wp is expired"
        }
    }
    return 0;
}

三、 shared_ptr 智能指针

1 shared_ptr 概述

C++ 的智能指针 shared_ptr 是对原始指针的一种封装。使用 shared_ptr 可以自动管理内存,避免了内存泄漏的问题。
shared_ptr 是 C++11 标准引入的,位于头文件 <memory> 中。

2 shared_ptr 使用方式

shared_ptr 可以通过 new 关键字初始化:

#include <memory>

std::shared_ptr<int> p1(new int(10)); // 使用 new 关键字初始化

或者通过 make_shared 函数初始化(推荐):

#include <memory>

std::shared_ptr<int> p2 = std::make_shared<int>(10); // 使用 make_shared 函数初始化

make_shared 函数可以避免了 new 关键字产生的额外开销,效率更高。

3 多个 shared_ptr 共享对象

多个 shared_ptr 对象可以共享同一个对象:

#include <memory>

std::shared_ptr<int> p1(new int(10));
std::shared_ptr<int> p2 = p1; // p1 和 p2 共享同一个对象

示例代码中p1 被初始化为一个指向 int 类型的值为 10 的指针,p2 则是将 p1 赋值给它,因此它们共享同一个对象。当 p1 或 p2 被销毁时,它们所占用的内存资源会自动被释放。

4 shared_ptr 循环引用问题

使用 shared_ptr 时需要小心循环引用的问题:

#include <memory>

class B; // 前置声明

class A
{
public:
    std::shared_ptr<B> b_ptr;
};

class B
{
public:
    std::shared_ptr<A> a_ptr;
};

int main()
{
    std::shared_ptr<A> a(new A());
    std::shared_ptr<B> b(new B());
    // 错误的示范
    a->b_ptr = b;
    b->a_ptr = a;
    return 0;
}

上述代码中A 和 B 类互相拥有 shared_ptr。当 A 和 B 对象被释放时,由于它们相互拥有 shared_ptr,因此它们的引用计数永远不会降到 0,导致内存泄漏。

这个问题可以通过将其中一个 shared_ptr 改为 weak_ptr 解决例如:

#include <memory>

class A; // 前置声明

class B
{
public:
    std::weak_ptr<A> a_ptr; // 将其中一个 shared_ptr 改为 weak_ptr
};

class A
{
public:
    std::shared_ptr<B> b_ptr;
};

int main()
{
    std::shared_ptr<A> a(new A());
    std::shared_ptr<B> b(new B());
    a->b_ptr = b;
    b->a_ptr = a; // 将 B 的成员变量 a_ptr 改为 weak_ptr
    return 0;
}

这样在 A 和 B 对象销毁时,其中一个 shared_ptr 的引用计数会降为 0,而另一个对象则可以正常析构并释放内存。

5 shared_ptr 自定义删除器

shared_ptr 可以使用自定义删除器,删除器是一个函数对象或者函数指针,用于在 shared_ptr 对象被销毁时释放资源。自定义删除器需要满足一定的定义:

示例代码中定义了一个自定义删除器函数 deleter,当 shared_ptr 对象销毁时,它将被调用。使用这个自定义删除器函数创建了一个 shared_ptr 对象 p,销毁它时,将输出 delete 10 的信息。

void deleter(T *ptr); // 函数指针

class Deleter
{
public:
    void operator()(T *ptr) const; // 函数对象
};

其中 T 是 shared_ptr 指向的类的类型。

#include <iostream>
#include <memory>

void deleter(int *x) // 自定义删除器函数
{
    std::cout << "delete " << *x << std::endl;
    delete x;
}

int main()
{
    std::shared_ptr<int> p(new int(10), deleter);
    return 0;
}

四、 unique_ptr 智能指针

1 unique_ptr 概述

unique_ptr 是 C++11 标准引入的智能指针,用于管理动态分配的对象。unique_ptr 和 shared_ptr 不同,它采用独占的方式来管理指针,使得每个 unique_ptr 实例只能拥有一个指向对象的指针。unique_ptr 拥有比原始指针更好的内存管理,能够自动释放内存,从而避免了内存泄漏和悬垂指针的问题。

2 unique_ptr 使用方式

unique_ptr 的使用方式和原始指针类似。

#include <memory>

std::unique_ptr<int> ptr(new int(42));  // 使用 new 来动态分配内存

也可以使用 make_unique 函数来创建 unique_ptr。

#include <memory>

auto ptr = std::make_unique<int>(42);   // 使用 make_unique 创建 unique_ptr

unique_ptr 可以访问被管理对象的成员
以上代码中,定义了一个结构体 Foo,指针 ptr 指向一个 Foo 对象,可以通过箭头运算符 -> 访问成员变量。

#include <memory>

struct Foo {
    int num;
    Foo() : num(42) {}
};

int main() {
    std::unique_ptr<Foo> ptr(new Foo());
    std::cout << ptr->num << std::endl;   // 输出 42
    return 0;
}

3 unique_ptr 转移所有权

unique_ptr 实例是唯一的,你可以将所有权从一个 unique_ptr 实例移动到另一个 unique_ptr 实例,或者将其移动到一个新的实例中。

示例代码中p1 指向的内存被移动到了 p2 中,并且 p1 变成了 nullptr,随后又将 p2 移动到 p3 中,并且 p2 变成了 nullptr。

#include <memory>

std::unique_ptr<int> p1(new int(42));
std::unique_ptr<int> p2 = std::move(p1); // 所有权被 p2 接管,p1 变为 nullptr
std::unique_ptr<int> p3(std::move(p2)); // 所有权被 p3 接管,p2 变为 nullptr

4 unique_ptr 作为容器元素

与 shared_ptr 不同,unique_ptr 可以作为容器元素使用。

示例代码中定义了一个 vector,其中的元素是 unique_ptr,可以像普通指针一样使用,也可以调用其成员函数。

#include <memory>
#include <vector>

void foo() {
    std::vector<std::unique_ptr<int>> v;
    v.emplace_back(new int(1));     // 向 vector 中添加一个元素,用 unique_ptr 来管理指针
    v.emplace_back(new int(2));
    v.emplace_back(new int(3));

    for (auto & p : v) {
        std::cout << *p << std::endl; // 输出指针指向的值
    }
}

5 unique_ptr 自定义删除器

unique_ptr 可以使用自定义删除器,删除器是一个函数对象或者函数指针,用于在 unique_ptr 对象被销毁时释放资源。

示例代码中定义了一个自定义删除器函数 fclose_deleter,传入给 unique_ptr 与文件指针一起初始化。当 unique_ptr 对象销毁时,它将调用自定义删除器函数 fclose_deleter 释放文件资源。

#include <iostream>
#include <cstdio>
#include <memory>

void fclose_deleter(std::FILE *f)  // 自定义删除器函数
{
    std::cout << "fclose " << f << std::endl;
    std::fclose(f);
}

void foo() {
    std::unique_ptr<std::FILE, decltype(&fclose_deleter)> file(std::fopen("test.txt", "w"), fclose_deleter);
    if (file) {
        std::fprintf(file.get(), "hello, world\n"); // 输出到文件
    }
}

五、 weak_ptr 智能指针

1 weak_ptr 概述

weak_ptr 也是 C++11 标准引入的智能指针,它是一种非拥有(non-owning)的智能指针,不能直接使用所管理的对象,也不能改变所管理的对象。与 shared_ptr 不同,weak_ptr 不会增加所管理对象的引用计数,因此不能保证所管理的对象一定存在。

weak_ptr 主要用于检查所管理的资源是否已经被释放,以及协助 shared_ptr 防止循环引用。

2 weak_ptr 使用方式

由于 weak_ptr 不会增加所管理对象的引用计数,因此需要 shared_ptr 实例转化为 weak_ptr 实例。

#include <memory>

std::shared_ptr<int> sp(new int(42));
std::weak_ptr<int> wp(sp);   // 使用 shared_ptr 实例创建 weak_ptr 实例

可以使用 lock 函数从 weak_ptr 中提升 shared_ptr,如果所管理对象已经被销毁,则返回一个空的 shared_ptr。

#include <memory>

std::shared_ptr<int> sp(new int(42));
std::weak_ptr<int> wp(sp);

{
    std::shared_ptr<int> p = wp.lock();  // 从 weak_ptr 提升为 shared_ptr
    if (p) {
        // 可以使用所管理对象
    } else {
        // 对象已经被销毁
    }
}

lock 函数返回的 shared_ptr 实例和之前的 shared_ptr 实例是相同的,都指向同一个对象。但是需要注意的是,如果返回的是空的 shared_ptr,则无法操作对象,否则会引发异常。

3 可以通过 weak_ptr 检查资源是否存在

weak_ptr 主要用于检查所管理的资源是否存在,如果所管理的对象已经被销毁,则无法创建 shared_ptr 实例,只能创建空的 weak_ptr 实例。

#include <memory>

std::shared_ptr<int> sp(new int(42));
std::weak_ptr<int> wp(sp);

sp.reset();   // 销毁 shared_ptr

if (wp.expired()) { // 检查所管理对象是否存在
    // 资源不存在
} else {
    // 资源存在
}

这里使用 expired 来检查所管理的资源是否存在。如果 expired 返回 true,则表示资源已经被释放,反之,则表示资源可用。

4 weak_ptr 协助 shared_ptr 防止循环引用

循环引用的问题在前面强调了多次,weak_ptr 能够协助 shared_ptr 解决该问题。在需要解决循环引用问题的类中,将有可能持有对象的指针定义为 weak_ptr 类型。在需要使用该对象时,先检查 weak_ptr 是否已经提升为 shared_ptr,如果提升失败,则对象已经被销毁,否则,就使用 shared_ptr 操作对象。

以上代码中A 和 C 互相引用,导致无法释放内存。改为使用 weak_ptr 协助 shared_ptr 防止循环引用。

#include <memory>

// A 引用 C,同时 C 引用 A
class A;
class C;

class A {
public:
    void setC(std::shared_ptr<C> p) {
        _c = p;
    }
private:
    std::weak_ptr<C> _c;
};

class C {
public:
    void setA(std::shared_ptr<A> p) {
        _a = p;
    }
private:
    std::weak_ptr<A> _a;
};

int main() {
    // 创建 A 和 C 的 shared_ptr 实例
    std::shared_ptr<A> ap(new A());
    std::shared_ptr<C> cp(new C());

    // A 和 C 互相引用,导致无法释放内存
    ap->setC(cp);
    cp->setA(ap);

    return 0;
}

示例代码中使用了 weak_ptr 来协助 shared_ptr 防止循环引用,将 C 中的 _a 定义为 weak_ptr 类型,使用 lock 函数来检测 A 是否已经被释放,如果未被释放则执行操作,否则不执行操作。这样就避免了循环引用的问题。

#include <memory>

// A 引用 C,同时 C 引用 A
class A;
class C;

class A {
public:
    void setC(std::shared_ptr<C> p) {
        _c = p;
    }
private:
    std::weak_ptr<C> _c;
};

class C {
public:
    void setA(std::shared_ptr<A> p) {
        _a = p;
    }

    void doSomething() {
        // 获取 _a 所管理对象的指针
        std::shared_ptr<A> a = _a.lock();
        if (a) {
            // 执行操作
        }
    }

private:
    std::weak_ptr<A> _a;
};

int main() {
    // 创建 A 和 C 的 shared_ptr 实例
    std::shared_ptr<A> ap(new A());
    std::shared_ptr<C> cp(new C());

    // A 和 C 互相引用,但是使用 weak_ptr 避免了循环引用的问题
    ap->setC(cp);
    cp->setA(ap);

    return 0;
}

六、 智能指针与多线程

在C++ 中使用智能指针管理动态资源分配是一种比较安全和方便的方法,但是在多线程环境下,指针的安全性是一个问题。在本文中,我们将分别讨论智能指针的线程安全性,如何实现 shared_ptr 的原子引用计数,以及 shared_ptr 的引用计数如何实现线程安全。

1 智能指针的线程安全性

在多线程环境下使用智能指针,需要注意以下几点:

  • 一个线程拥有了指针所有权之后,不要让其他线程使用该指针。
  • 当使用一个指针时,需要使用锁来保护该指针。
  • 不要让一个指针在一个线程中 delete,而在另一个线程中访问它。

2 shared_ptr 原子引用计数

在C++11中std::shared_ptr 指针的引用计数实现是原子的,可以保证多线程环境下的线程安全。当拷贝一个 shared_ptr 去创建一个新的引用计数时,默认使用原子计数的方式,保证操作的原子性。

示例代码中在两个线程中各创建了 1000000 次 shared_ptr 的拷贝,使用原子引用计数可以保证线程安全。

#include <memory>
#include <atomic>
#include <thread>

void thread1(std::shared_ptr<int> num) {
    for (int i = 0; i < 1000000; ++i) {
        std::shared_ptr<int> p(num);
    }
}

void thread2(std::shared_ptr<int> num) {
    for (int i = 0; i < 1000000; ++i) {
        std::shared_ptr<int> p(num);
    }
}

int main() {
    auto num = std::make_shared<int>(42);

    std::thread t1(thread1, num);
    std::thread t2(thread2, num);

    t1.join();
    t2.join();

    return 0;
}

3 shared_ptr 引用计数如何实现线程安全

shared_ptr 的引用计数是通过原子操作实现的,即多线程环境下可以保证线程安全。在 std::shared_ptr 中,使用 std::atomic 类型来实现引用计数的原子操作。该类型是 C++11 标准库中提供的,支持原子类型加、减、读、写等操作。

引用计数的实现如下:

#include <atomic>

template<typename T>
class shared_ptr {
public:
    // ...

private:
    // 引用计数
    std::atomic<int>* _refcount;
    // 所管理的指针
    T* _ptr;
};

在 shared_ptr 类中,_refcount 定义为一个 std::atomic* 指针类型,这个指针类型是一个原子类型,支持原子操作,可以保证多线程环境下的线程安全。

在引用计数的加、减和读、写操作中,简单的使用 _refcount 指针即可,由于使用了原子类型,可以确保线程安全性。

template<typename T>
class shared_ptr {
public:
    // ...

    // 获取当前引用计数
    int getRefCount() const {
        return _refcount->load();
    }

    // 增加引用计数
    void addRef() {
        ++(*_refcount);
    }

    // 减少引用计数
    void releaseRef() {
        if (--(*_refcount) == 0) {
            delete _refcount;
            delete _ptr;
        }
    }

private:
    std::atomic<int>* _refcount;
    T* _ptr;
};

以上我们讨论了智能指针在多线程环境下的安全性。在使用智能指针时,需要注意指针的安全性,并使用锁来保护指针的访问。对于 shared_ptr 指针,C++11 标准实现了原子引用计数的操作,可以保证多线程环境下的线程安全。

七、小结

1 智能指针使用注意事项

智能指针可以自动地管理动态分配的内存,使得程序不需要手动管理内存,避免因为内存泄漏或者重复释放内存等问题而导致的非常严重的问题。然而,在使用智能指针时,需要注意以下几点

  • 不要使用传统的指针来修改智能指针管理的对象,而是应该使用智能指针提供的接口。
  • 不要在一个类的方法中返回智能指针管理的成员,而是应该返回它的副本。
  • 如果智能指针管理的对象创建和销毁的时间并不相同时,应该使用 std::weak_ptr 来保存指向所管理对象的引用,避免因为对象销毁时多次访问已释放内存的问题。

2 智能指针的优缺点比较

2.1 智能指针的优点

  • 自动检查和释放内存。智能指针能够自动检查内存泄漏等问题并释放内存,避免程序造成严重的后果。
  • 提高程序的可靠性。由于智能指针能够有效地提高程序的可靠性,避免程序在向空指针中写数据,或者动态分配内存时忘记释放而出现问题。
  • 代码简洁。智能指针能够简化代码的书写操作,使得代码的可读性和可维护性都得到了很大的提高。

2.2 智能指针的缺点

  • 降低程序效率。智能指针需要做的工作比传统的指针要复杂,因此在一定程度上降低了程序的效率。
  • 不能替代传统指针的所有功能。有些情况下,智能指针无法替代传统指针,例如在特定的算法中需要使用指针的操作等。
  • 多线程环境下需要注意安全问题。多线程环境下,智能指针需要使用特定的技术来保证线程安全。

总的来说,智能指针是可以提高程序的可靠性和代码的可读性,并减少因为内存泄漏或者重复释放造成的严重后果。然而,在使用智能指针时,仍然需要注意多线程环境下的安全问题,并且需要在需要使用指针的特定场景中使用传统指针。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由半码博客整理,本文链接:https://www.bmabk.com/index.php/post/144167.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
半码博客——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!