C++智能指针2——共享指针shared_ptr详解

目录

简介

示例

代码

输出

创建

定义删除器

示例

处理数组

销毁其他资源

接口列表

隐式指针转换

比较运算符

获取删除器

别名构造函数

make_shared()和allocate_shared()

强制类型转换

线程安全接口

使用错误


简介

共享指针shared_ptr是具有共享所有权语义的智能指针。 每当共享指针shared_ptr的最后一个所有者被销毁时,关联对象都将被删除(或关联资源被清除)。

模板类shared_ptr<>被模板化为初始指针所指向的对象的类型(类型参数可能为void):

namespace std {
    template <typename T>
    class shared_ptr
    {
        public:
            typedef T element_type;
            ...
    };
}

示例

代码

#include <iostream>
#include <string>
#include <vector>
#include <memory>

using namespace std;

int main()
{
    //新建两个共享型智能指针
    shared_ptr<string> pTom(new string("tom"));
    shared_ptr<string> pJerry(new string("jerry"));
    cout << "Start: " << *pTom << "\t" << *pJerry << endl;

    //首字母大写
    (*pTom)[0] = 'T';
    pJerry->replace(0,1,"J");
    cout << "After capitalization: " << *pTom << "\t" << *pJerry << endl;
    
    //多次加入容器
    vector<shared_ptr<string>> whoCleanRoom;
    whoCleanRoom.push_back(pJerry);
    whoCleanRoom.push_back(pJerry);
    whoCleanRoom.push_back(pTom);
    whoCleanRoom.push_back(pJerry);
    whoCleanRoom.push_back(pTom);

    //打印所有元素
    cout << "whoCleanRoom: ";
    for (auto ptr = whoCleanRoom.cbegin(); ptr != whoCleanRoom.cend(); ++ptr) {
        cout << **ptr << "  ";
    }
    cout << endl;

    //设置新名字
    *pTom = "Tomy";

    //打印所有元素
    cout << "whoCleanRoom: ";
    for (auto ptr = whoCleanRoom.cbegin(); ptr != whoCleanRoom.cend(); ++ptr) {
        cout << **ptr << "  ";
    }
    cout << endl;
    
    //打印内部信息
    cout << "use_count: " << whoCleanRoom[0].use_count() << endl;
}

输出

Start: tom	jerry
After capitalization: Tom	Jerry
whoCleanRoom: Jerry  Jerry  Tom  Jerry  Tom  
whoCleanRoom: Jerry  Jerry  Tomy  Jerry  Tomy  
use_count: 4

创建

方式一:shared_ptr<string> pTom{new string("tom")};
方式二:shared_ptr<string> pTom;
       pTom.reset(new string("tom"));
方式三:shared_ptr<string> pTom = make_shared<string>("tom");

相对于方式一和方式二,方式三更快,更安全,因为它只进行了一次内存分配而不是两次(一次为对象分配内存,另一次为共享指针的控制块分配内存)。

定义删除器

当要删除资源时,会调用删除器。

示例

shared_ptr<string> pTom(new string("tom"),
                        [](string* p) {
                            cout << "delete " << *p << endl;
                            delete p;
                        });

处理数组

请注意,共享指针shared_ptr提供的默认删除程序将调用delete,而不是delete []。 这意味着仅当共享指针拥有使用new创建的单个对象时,默认删除器才适用。不幸的是,可以为数组创建一个共享指针shared_ptr,但这是错误的:

std::shared_ptr<char> ptr(new char[20]); //错误,但能编译通过

因此,如果使用new []创建对象数组,则必须自定义删除器。 你可以通过传递函数,函数对象或lambda来做到这一点,后者对传递的普通指针调用delete []。 例如:

std::shared_ptr<char> ptr(new char[20],
                       [](char* p) {
                           delete[] p;
                       }
                      );

也可以使用default_delete作删除器,因为它使用delete []。使用代码如下:

std::shared_ptr<char> p(new char[20],
                       std::default_delete<char[]>());

注意,共享指针shared_ptr不提供运算符[]。

销毁其他资源

如果共享指针shared_ptr的清除动作不只是删除内存,则必须自定义删除器。 

示例一:假设要确保在删除对临时文件的最后一个引用时将其删除,可以使用如下代码:

#include <string>
#include <fstream>
#include <memory>
#include <cstdio>
#include <iostream>

using std::cout;
using std::endl;

class FileDeleter
{
  public:
    FileDeleter (const std::string& sFileName)
     : m_sFileName(sFileName) {
    }

    void operator () (std::ofstream* pOfs) {
        delete pOfs;                     //关闭文件
        std::remove(m_sFileName.c_str()); //删除文件
        cout << "Delete file -- " << m_sFileName << endl;
    }

  private:
    std::string m_sFileName;
};

int main()
{
    const std::string sFileName = "TempFile.txt";
    std::shared_ptr<std::ofstream> fp(new std::ofstream(sFileName),
                                      FileDeleter(sFileName));
    cout << "Program exit" << endl;
}

程序输出:

Program exit
Delete file -- TempFile.txt

以上代码使用新创建的输出文件初始化了共享指针shared_ptr。 传递的FileDeleter可确保使用此共享指针shared_ptr的最后一个副本失去此输出流的所有权时,使用<cstdio>中提供的标准C函数remove()关闭并删除此文件。 因为remove()需要文件名,所以我们将文件名作为参数传递给FileDeleter的构造函数。

第二个示例演示了如何使用共享指针shared_ptr处理共享内存:

#include <memory>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <string>
#include <iostream>

using std::string;

class SharedMemoryDetacher
{
    public:
        SharedMemoryDetacher(const string & sFileName)
            : m_sFilePath(sFileName) {}

        void operator () (long *) {
            std::cout << "unlink " << m_sFilePath << std::endl;
            if (shm_unlink(m_sFilePath.data()) != 0) {
                std::cerr << "OOPS: shm_unlink() failed" << std::endl;
            }
        }

    private:
        const string & m_sFilePath;
};

std::shared_ptr<long> getSharedIntMemory (int num, const string & sFilePath)
{
    void* mem;
    int shmfd = shm_open(sFilePath.data(), O_CREAT|O_RDWR, S_IRWXU|S_IRWXG);
    if (shmfd < 0) {
        throw std::string(strerror(errno));
    }
    if (ftruncate(shmfd, num*sizeof(long)) == -1) {
        throw std::string(strerror(errno));
    }
    mem = mmap(nullptr, num*sizeof(long), PROT_READ | PROT_WRITE,
               MAP_SHARED, shmfd, 0);
    if (mem == MAP_FAILED) {
        throw std::string(strerror(errno));
    }
    return std::shared_ptr<long>(static_cast<long*>(mem),
                                 SharedMemoryDetacher(sFilePath));
}

int main()
{
    //获取并连接共享内存
    const int C_INT_NUM = 10;
    const string C_STR_FILE_PATH = "shmTmp";
    std::shared_ptr<long> smp(getSharedIntMemory(C_INT_NUM, C_STR_FILE_PATH));
    
    //初始化共享内存
    for (int i=0; i<C_INT_NUM; ++i) {
        smp.get()[i] = i*36;
    }

    //其他地方处理共享内存
    //...
    std::cout << "<return>" << std::endl;
    std::cin.get(); 

    //释放共享内存
    smp.reset();
    //...
}

首先,定义一个删除程序SharedMemoryDetacher来分离共享内存。 删除程序释放共享内存,该共享内存由getSharedIntMemory()获取并附加。 为了确保在最后一次使用共享内存时删除器被调用,当getSharedIntMemory()为连接的内存创建共享指针shared_ptr时,将传递该删除器:

return std::shared_ptr<long>(static_cast<long*>(mem),
                             SharedMemoryDetacher()); // calls shmdt()

也可以在此处使用lambda(省略前缀std::):

return shared_ptr<long>(static_cast<long*>(mem),
                       [](long* p) {
                           cout << "unlink shmTemp" << endl;
                           if (shm_unlink("/shmTemp") != 0) {
                               cerr << "shm_unlink()失败"
                                    << endl;
                           }
                        });

由于不允许删除器抛出异常,因此仅在此处将错误消息写入std::cerr。

因为shm_unlink()的签名已经适合作为删除器,所以如果你不想检查其返回值,甚至可以直接使用shm_unlink()作为删除器:

return std::shared_ptr<long>(static_cast<long*>(mem),
                             shm_unlink);

请注意,共享指针shared_ptr仅提供运算符*和->,没有提供指针算术和运算符[]。 因此,要访问内存,必须使用get(),它会返回由共享指针shared_ptr封装的内部指针,以提供完整的指针语义:

smp.get()[i] = i*36;

get()提供了另外一种调用方法:

(&*smp)[i] = i*36;

对于这两个示例,另一种可能的实现技术可能比这更简洁:只需创建一个新类,构造函数执行初始工作,而析构函数执行清除。 然后,可以只使用共享指针shared_ptr来管理用new创建的此类的对象。 这样做的好处是可以定义一个更直观的接口,例如代表共享内存的对象的运算符[],但应该仔细考虑复制和赋值操作。

接口列表

下表列出了为共享指针shared_ptr提供的所有操作。

操作结果
shared_ptr<T> sp默认构造函数;使用默认删除器(调用delete)创建一个空的共享指针
shared_ptr<T> sp(ptr)使用默认删除器(调用delete)创建一个拥有*ptr的共享指针
shared_ptr<T> sp(ptr,del) 使用del作为删除器创建拥有*ptr的共享指针
shared_ptr<T> sp(ptr, del, ac)使用del作为删除器并使用ac作为分配器创建一个拥有*ptr的共享指针
shared_ptr<T> sp(nullptr)使用默认删除器(调用delete)创建空的共享指针
shared_ptr<T> sp(nullptr, del)使用del作为删除器创建一个空的共享指针
shared_ptr<T> sp(nullptr, del, ac)使用del作为删除器和ac作为分配器创建一个空的共享指针
shared_ptr<T> sp(sp2)创建与sp2共享所有权的共享指针
shared_ptr<T> sp(move(sp2))创建一个共享指针,该共享指针拥有先前由sp2拥有的指针(sp2之后为空)
shared_ptr<T> sp(sp2, ptr)别名构造函数;创建一个共享指针,共享sp2的所有权,但引用*ptr
shared_ptr<T> sp(wp)从弱指针wp创建共享指针
shared_ptr<T> sp(move(up))从unique_ptr创建共享指针
shared_ptr<T> sp(move(ap))从auto_ptr创建共享指针
sp.~shared_ptr()析构函数;如果sp拥有对象,则调用deleter
sp = sp2赋值(sp之后与sp2共享所有权,放弃先前拥有的对象的所有权)
sp = move(sp2)移动赋值(sp2将所有权转移到sp)
sp = move(up)使用unique_ptr进行移动赋值(up将所有权转让给sp)
sp = move(ap)使用auto_ptr进行移动赋值(ap将所有权转让给sp)
sp1.swap(sp2)交换sp1和sp2的指针和删除器
swap(sp1, sp2)交换sp1和sp2的指针和删除器
sp.reset()放弃所有权并将共享指针重新初始化为空
sp.reset(ptr)放弃所有权并使用默认删除器(称为delete)重新初始化共享指针,拥有*ptr
sp.reset(ptr, del)放弃所有权并使用del作为删除器重新初始化共享指针,拥有* ptr
sp.reset(ptr, del, ac)放弃所有权并重新初始化共享指针,拥有* ptr,使用del作为删除器,使用ac作为分配器
make_shared(...)为通过传递的参数初始化的新对象创建共享指针
allocate_shared(ac, ...)使用分配器ac为由传递的参数初始化的新对象创建共享指针
sp.get()返回存储的指针(通常是拥有对象的地址,如果没有则返回nullptr)
*sp返回拥有的对象(如果没有则为未定义的行为)
sp->...提供对拥有对象的成员访问权限(如果没有,则行为未定义)
sp.use_count()返回共享所有者(包括sp)的数目;如果共享指针为空,则返回0
sp.unique()返回sp是否是唯一所有者(等效于sp.use_count()== 1,但可能更快)
if (sp)运算符bool();返回sp是否为空
sp1 == sp2对存储的指针调用==(存储的指针可以为nullptr)
sp1 != sp2对存储的指针调用!=(存储的指针可以为nullptr)
sp1 < sp2对存储的指针调用<(存储的指针可以为nullptr)
sp1 <= sp2对存储的指针调用<=(存储的指针可以为nullptr)
sp1 > sp2对存储的指针调用>(存储的指针可以为nullptr)
sp1 >= sp2对存储的指针调用>=(存储的指针可以为nullptr)
static_pointer_cast(sp)sp的static_cast<>语义
dynamic_pointer_cast(sp)sp的dynamic_cast<>语义
const_pointer_cast(sp)sp的const_cast<>语义
get_deleter(sp)返回删除器的地址(如果有),否则返回nullptr
strm << sp调用原始指针的输出运算符(等于strm << sp.get())
sp.owner_before(sp2)提供严格的弱排序和另一个共享指针
sp.owner_before(wp)通过弱指针提供严格的弱排序

每当所有权转移到已经拥有另一个对象的共享指针时,如果该共享指针是最后一个所有者,则将调用先前拥有对象的deleter。如果共享指针通过分配新值或调用reset()获得新值,则同样适用。

空的共享指针shared_ptr不共享对象的所有权,因此use_count()函数返回0。但由于一个特殊的构造函数,共享指针仍可能引用一个对象。

隐式指针转换

如果存在隐式指针转换,则共享指针可能使用不同的对象类型。因此,构造函数、赋值运算符和reset()是成员模板,而比较运算符则针对不同类型进行模板化。

比较运算符

所有比较运算符都会调用共享指针内部封装的原始指针的比较运算符(即它们为get()返回的值调用相同的运算符)。它们都将nullptr作为参数进行了重载。因此,您可以检查是否存在有效的指针,甚至可以检查原始指针是否小于或大于nullptr。

获取删除器

get_deleter()返回指向删除器的指针(如果有的话),否则返回nullptr。 只要共享指针拥有该删除器,该指针就有效。 但是,要获取删除器,必须将其类型作为模板参数传递。 例如:

auto del = [] (int* p) {
               delete p;
           };
std::shared_ptr<int> p(new int, del);
decltype(del)* pd = std::get_deleter<decltype(del)>(p);

注意,共享指针不提供release()操作来放弃所有权并将对象的控制权返回给调用者,原因是其他共享指针可能仍然拥有对象。

别名构造函数

带有另一个共享指针和另一个原始指针的构造函数是所谓的别名构造函数,它使你可以捕获一个对象拥有另一个对象的事实。 例如:

struct X
{
    int a;
};
shared_ptr<X> px(new X);
shared_ptr<int> pi(px, &px->a);

类型X的对象“拥有”其成员a,因此要创建指向a的共享指针,你需要通过使用别名构造函数将其附加到其引用计数上,以使周围的对象保持活动状态。 还存在其他更复杂的示例,例如引用容器元素或共享库符号。

程序员必须确保两个对象的生存期匹配,否则可能会出现悬空指针或资源泄漏。 例如:

shared_ptr<X> sp1(new X);
shared_ptr<X> sp2(sp1, new X); //错误: 这个X的删除器永远不会被调用
sp1.reset(); //删除第一个X,让sp1变空
shared_ptr<X> sp3(sp1, new X); //use_count() == 0, 但get()!=nullptr

make_shared()和allocate_shared()

make_shared()和allocate_shared()优化了共享对象及其关联的控制块(例如,维护使用计数)的创建。

shared_ptr<X>(new X(...))

以上代码执行两次内存分配:一次内存分配给X,一次内存分配给控制块,例如由共享指针管理其使用计数。 

make_shared<X>(...)

上面创建共享指针的方法速度更快,仅执行一次内存,并且更安全,因为不会发生X内存分配成功但控制块内存分配失败的情况。 allocate_shared()允许传递你自己的分配器作为第一个参数。

强制类型转换

共享指针强制转换运算符允许将共享指针中包裹的指针强制转换为其他类型。 语义与相应的运算符相同,得到的是另一个不同类型的共享指针。 注意,不能使用普通的强制转换运算符,因为它会导致未定义的行为:

shared_ptr<void> sp(new int); //智能指针内部保存void*指针
...
shared_ptr<int>(static_cast<int*>(sp.get())) //错误:未定义的行为
static_pointer_cast<int*>(sp) //正确

线程安全接口

通常,共享指针shared_ptr不是线程安全的。因此,为避免由于数据竞争而导致的未定义行为,当共享指针在多个线程中引用同一对象时,必须使用互斥或​​锁等技术。

对应于普通指针的C风格原子接口,标准库提供了共享指针的重载版本。该接口允许多个线程同时操作共享指针。注意,这只意味着并发访问指针,而不是对它们所引用的值。

例如:

std::shared_ptr<X> global; //创建空的共享指针
void foo()
{
    std::shared_ptr<X> local{new X};
    ...
    std::atomic_store(&global, local);
}

下表列出了共享指针shared_ptr的高级原子操作。

操作结果
atomic_is_lock_free(&sp)如果sp的原子接口是无锁的,则返回true
atomic_load(&sp)返回sp
atomic_store(&sp,sp2)使用sp2对sp进行赋值
atomic_exchange(&sp,sp2)交换sp与sp2的值

使用错误

尽管共享指针shared_ptr指针改善了程序安全性,但是由于通常会自动释放与对象关联的资源,所以当不再使用对象时可能会出现问题。 
例如,必须确保只有一组共享指针拥有一个对象。 如下代码所示:

int* p = new int;
shared_ptr<int> sp1(p);
shared_ptr<int> sp2(p); //错误:两个共享指针同时管理int内存

以上代码的问题在于,当sp1和sp2失去对p的所有权时,它们都将释放关联的资源(调用delete),这将导致关联资源的释放会执行两次。 因此,在创建具有关联资源的对象时,应该始终直接初始化智能指针:

shared_ptr<int> sp1(new int);
shared_ptr<int> sp2(sp1);

也可能间接发生此问题。 在《C++智能指针3——弱指针weak_ptr详解》的示例中,假设要为Person引入一个成员函数,该函数既创建了从孩子到父母的引用,又创建了从父母到孩子的应用:

shared_ptr<Person> pMom(new Person(sName + "的母亲"));
shared_ptr<Person> pDad(new Person(sName + "的父亲"));
shared_ptr<Person> pKid(new Person(sName));
pKid->setParentsAndTheirKids(pMom, pDad);

下面代码是setParentsAndTheirKids()的简单实现:

class Person {
    public:
        ...
        void setParentsAndTheirKids (shared_ptr<Person> pMother = nullptr,
                                     shared_ptr<Person> pFather = nullptr) {
            m_pMother = pMother;
            m_pFather = pFather;
            if (pMother != nullptr) {
                pMonther->kids.push_back(shared_ptr<Person>(this)); //错误
            }
            if (pFather != nullptr) {
                pFather->kids.push_back(shared_ptr<Person>(this)); //错误
            }
        }
        ...
};

以上代码的问题在于使用this创建共享指针。 这样做是因为要设置成员父亲和母亲的孩子。但要做到这一点,需要一个指向孩子的共享指针,而手边又没有这个指针。 然而,使用this创建新的共享指针并不能解决问题,因为这样打开了一个新的所有者组。

解决此问题的一种方法是将共享指针作为第三个参数传递给孩子,不过C++标准库提供了另一个选项:类std::enable_shared_from_this<>。 可以使用类std::enable_shared_from_this<>派生自己的类,该类表示由共享指针管理的对象,并将类名作为模板参数传递。 这样可以使用派生成员函数shared_from_this()在此基础上创建正确的共享指针shared_ptr:

#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;

class Person : public enable_shared_from_this<Person> {
  public:
    string m_sName;
    shared_ptr<Person> m_pMother;
    shared_ptr<Person> m_pFather;
    vector<weak_ptr<Person>> m_oKids;  // weak pointer !!!

    Person (const string& sName)
     : m_sName(sName) {
    }

    void setParentsAndTheirKids (shared_ptr<Person> pMother = nullptr,
                                 shared_ptr<Person> pFather = nullptr) {
        m_pMother = pMother;
        m_pFather = pFather;
        if (pMother != nullptr) {
            pMother->m_oKids.push_back(shared_from_this());
        }
        if (pFather != nullptr) {
            pFather->m_oKids.push_back(shared_from_this());
        }
    }

    ~Person() {
      cout << "删除 " << m_sName << endl;
    }
};

shared_ptr<Person> initFamily (const string& sName)
{
    shared_ptr<Person> pMom(new Person(sName + "的母亲"));
    shared_ptr<Person> pDad(new Person(sName + "的父亲"));
    shared_ptr<Person> pKid(new Person(sName));
    pKid->setParentsAndTheirKids(pMom, pDad);
    return pKid;
}

int main()
{
    string sName = "张三";
    shared_ptr<Person> pPerson = initFamily(sName);
    cout << sName << "家已经存在" << endl;
    cout << "- " << sName << "被分享" << pPerson.use_count() << "次" << endl;
    cout << "- 张三母亲的地一个孩子的名字是:"
         << pPerson->m_pMother->m_oKids[0].lock()->m_sName << endl;

    sName = "李四";
    pPerson = initFamily(sName);
    cout << sName << "家已经存在" << endl;
}

注意,不能在构造函数内部调用shared_from_this(),如果这么做结果是运行时错误:

class Person : public std::enable_shared_from_this<Person> {
    public:
        ...
        Person (const string& sName,
                shared_ptr<Person> pMother = nullptr,
                shared_ptr<Person> pFather = nullptr)
            : m_sName(sName), m_pMother(pMother), m_pFather(pFather) {
            if (pMother != nullptr) {
                pMother->kids.push_back(shared_from_this()); //错误
            }
            if (pFather != nullptr) {
                pFather->kids.push_back(shared_from_this()); //错误
            }
        }
        ...
};

上面代码的问题在于,在Person构造结束时,共享指针shared_ptr将自身存储在Person基类的私有成员中enable_shared_from_this <>。

因此,在初始化共享指针对象的构造过程中,绝对没有办法创建共享指针的循环引用。 

  • 6
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值