继前一篇:
shared_pr会出现循环以来造成悬挂指针。另外一个问题则是:必须确保某个对象只被一组shared pointer拥有。示例一个错误代码:
int * p = new int; shared_ptr<int> sp1(p); shared_ptr<int> sp2(p); //ERROR:two shared pointers manage allocated int
问题在于sp1和sp2都拥有释放权,这会导致两次delete。因此应该直接在创建对象和资源的时候即刻设置smart pointer:
shared_ptr<int> sp1(new int); shared_ptr<int> sp2(sp1);
这个问题可能间接发生,根据上一节的例子,准备为Person引入一个成员函数,用来建立“从kid指向parent” 的reference 及其反向reference:
shared_ptr<Person> mom(new Person(name + "'s mom")); shared_ptr<Person> dad(new Person(name + "'s dad")); shared_ptr<Person> kid(new Person(name, mom, dad)); kid->setParentsAndTheirKids(mom, dad); //下面是一个错误的直观版本 class Person{ public: //.... void setParentAndTheirKids(shared_ptr<Person> m = nullptr, shared_ptr<Person> f = nullptr) { mother = m; father = f; if(m != nullptr){ m->kids.push_back(shared_ptr<Perrson>(this));} //ERROR if(f != nllptr){ f->kids.push_back(shared_ptr<Person>(this));} //ERROR //.... } };
问题出在this的那个shared pointer 的建立。之所以那么做是为了设置mother 和 father两个成员的kids。但是为了那么做,需要一个shared pointer指向kid,而我们没有 。然而,根据this建立一个新的shared pointer不能解决问题,因为这么一来就是开启了新的拥有者团队(a new group of owners)。
对付这个问题的办法之一就是,将指向kid的那个shared pointer传递为第三实参。但c++标准库提供了另外一个选项:class std::enable_shared_from_this<> 。
你可以从class std::enable_shared_from_this<> 派生自己的class,表现出被shared pointer管理的对象,做法是将class 名称当做template是实参传入。
然后可以使用一个派生的成员函数 shared_from_this() 建立一个源自this的正确shared_ptr。
class Person : public std::enable_shared_from_this<Person>{ public: //.... void setParentAndTheirKids(shared_ptr<Person> m = nullptr, shared_ptr<Person> f = nullptr) { mother = m; father = f; if(m != nullptr){ m->kids.push_back(shared_from_this());} //Ok if(f != nllptr){ f->kids.push_back(shared_from_this());} //OK //.... } };
但shared_form_this 不能放在构造函数中,因为在Person构造结束之前,shared_ptr本身被存放在Person的基类中,也就是enable_shared_from_this<>内部的一个private成员中。所以,在构造期间,绝对无法建立shared pointer的循环引用。
get_deleter() 会取得一个pointer to deleter(如果定义deleter的话),要不就取得nullptr。只要shared pointer还拥有那个deleter,该pointer就有效。然而为了取得deleter,你必须以其类型作为template的实参,例如:
auto del = [] (int* p) { delete p; }; std::shared_ptr<int> p(new int, del); decltype(del)* pd = std::get_deleter<decltype(del)>(p);
需要注意的是,shared pointer 并未提供release() 操作用以放弃拥有权并将对象控制权交给调用者,原因是其他shared pointer可能还拥有该对象。
更复杂的shared_ptr操作
aliasing 构造函数指的是“接受一个shared pointer 和一个raw pointer” ,它允许你掌握一个事实(???):某对象拥有另一对象。例如:
struct X{ int a; }; shared_ptr<X> px(new X); shared_ptr<int> pi(px,&px->a);
类型X的对象拥有成员a,为了产生一个shared pointer指向a,必须保持外环对象(surrounding object)活着,做法是借由aliasing构造函数附加其reference count(被引用次数)。更复杂的例子:指向某个容器元素或者某个shared library symbol。
需要注意的是,作为一种必然结果,必须确保两对象的寿命相称(match),否则可能发生悬挂指针或者资源泄露。例如:
shared_ptr<X> sp1(new X); shared_ptr<X> sp2(sp1,new X); //ERROR 此X永远不会被delete sp1.reset(); //deletes firrst X:makes sp1 empty shared_ptr<X sp3(sp1,new X); //use_count() == 0,but get() != nullptr
make_shared() 和allocate——shared() 都用来优化“被共享对象以及其相应之控制区块(如用以维护使用次数)”的创建,。注意: shared_ptr<X> (new X(...)) 执行了二次分配:一次针对X,一次针对shared pointer的控制区块(用以管理使用次数)。如果替换为make_shared<X>(...) 会快速很多,只执行一次分配而且比较安全.allocate_shared() 允许传入自己的allocator作为第一实参。
线程安全的shared pointer接口
一般而言,shared pointer并非线程安全,为了避免data race造成不明确的行为,,必须使用mutex或者lock等技术。
相当于寻常pointer之原子性C-style接口,为shared pointer设计的重载版本允许并发处理shared pointer。注意,并发访问的是pointer而非其指向的值。
std::shared_ptr<X> global; void foo(){ std::shared_ptr<X> local{new X}; ... std::atomic_store(&global,local); }
Class unique_ptr (cppreference.com)
std::unique_ptr
是通过指针占有并管理另一对象,并在 unique_ptr
离开作用域时释放该对象的智能指针。
在下列两者之一发生时用关联的删除器释放对象:
unique_ptr不允许通过赋值语法将一个寻常的指针作为初值,必须直接初始化:
unique_ptr<int> up = new int; //ERROR unique_ptr<int> up(new int); //OK
可以调用release(),获得unique_ptr拥有的对象,于是调用者对该对象负有责任:
unique_ptr<string> up(new string("nico")); ... string *sp = up.release(); //获得拥有权
可以通过bool()操作符检查是否unique pointer拥有对象:
if(up) { cout << *up << endl; }
也可以和nullptr比较,以及通过get()成员函数查询unique_ptr内的raw pointer。
转移unique_ptr的拥有权
同一个pointer不能同时作为两个unique pointer的初值。
string* sp = new string("nico"); unique_ptr<string> up1(sp); unique_ptr<string> up2(sp); //ERROR 运行时错误
类满足可移动构造 (MoveConstructible) 和可移动赋值 (MoveAssignable) 的要求,但不满足可复制构造 (CopyConstructible) 或可复制赋值 (CopyAssignable) 的要求。
unique_ptr<ClassA> up1(new ClassA); //copy unique_ptr<ClassA> up2(up1); //ERROR not possible //转移所有权 unique_ptr<ClassA> up3(std::move(up1)); //OK
因为unique_ptr是独占式的,所有up2不能成为原对象的另一个拥有者。Assignment操作符也类似。
需要注意的是,如果要指派新值给unique_ptr,新值也必须是unique_ptr,不可以是寻常pointer:
std::unique_ptr<ClassA> ptr; ptr = new ClassA; //error ptr = unique_ptr<ClassA>(new ClassA); //OK 删除旧对象并拥有新对象
(上边这个乍一看跟copy很像,其实不是,因为 new ClassA是新构建的,并不是左值,故不是copy)
赋值nullptr也是可以的,和调用reset() 效率相同。
unique_ptr被当作成员
在class内使用unique_ptr可以避免资源泄露,unique_ptr可以协助避免“对象初始化期间抛出异常而造成的资源泄漏”。对于“拥有多个raw pointer”的class,如果构造期间第一个new成功而第二个失败,就可能导致资源泄漏,例如:
class ClassB { private: ClassA * ptr1; ClassA* ptr2; public: //构造函数,初始化指针,第二个new可能失败,造成资源泄漏 ClassB(int val1, int val2) : ptr1(new ClassA(val1)), ptr2(new ClassA(val2)) {} //复制构造函数,第二个new可能分配失败 ClassB(const ClassB& x) :ptr1(new ClassA(*x.ptr1)),ptr2(new ClassA(*x.ptr2)){} //赋值操作符 const ClassB& operator= (const ClassB& x) { *ptr1 = *x.ptr1; *ptr2 = *x.ptr2; return *this; } ~ClassB() { delete ptr1; delete ptr2; } }; |
class ClassB { private: unique_ptr<ClassA> ptr1; unique_ptr<ClassA> ptr2; public: //构造函数,初始化unique_ptr指针,不会造成资源泄漏 ClassB(int val1, int val2) : ptr1(new ClassA(val1)), ptr2(new ClassA(val2)) {} //复制构造函数, ClassB(const ClassB& x) :ptr1(new ClassA(*x.ptr1)),ptr2(new ClassA(*x.ptr2)){} //赋值操作符 const ClassB& operator= (const ClassB& x) { *ptr1 = *x.ptr1; *ptr2 = *x.ptr2; return *this; } |
上边使用unique_ptr不需要写析构函数,但必须写copy构造函数和赋值操作符构造函数,因为默认版本会拷贝或者赋值成员,但这对于unique_ptr是不可能的,如果你没有自己提供,ClassB将只提供move语义。
对于Array
对于array有特殊的规定。就因为释放对象应该是delete[]而不是delete。C++ std提供了class unique_ptr的一个偏特化版本处理array:
unique_ptr<string> up(new string[10]); //ERROR unique_ptr<string[]> up(new string[10]); //OK ... cout << *up << endl; //ERROR cout << up[0] << endl; //OK
注意,对于array来讲,这里* 和->不再提供,只能用操作符[]。
其他资源的dleter
当指向的资源用delete或者delete[]释放满足不了需求时,需要自定义deleter。此处的deleter定义方式不同于shared_ptr。必须指明deleter的类型作为第二个template实参。该类型可能是函数引用,或者函数指针或者函数对象(function object,也叫仿函数)。如果是函数对象,其function call操作符 () 应该接受一个 “指向对象”的pointer。举例而言:
class ClassADeleter { public: void operator () (ClassA* p) { cout << "call delete for ClassA object" << endl; delete p; } }; unique_ptr<ClassA, ClassADeleter> up(new ClassA);
如果你给的是个函数或lambda,你必须声明deleter的类型为 void(*)(T*) 或者 function<void(T*)>,要不就使用decltype。例如,若要为一个array of int 指定自己的deleter,并以lambda形式呈现,应该如下:
std::unique_ptr<int, void(*)(int*)> up(new int[10], [](int* p) { //... delete[] p; }); std::unique_ptr<int, std::function<void(int *)>> up1(new int[10], [](int* p) { //... delete[] p; }); auto l = [](int *p) { //... delete[] p; }; std::unique_ptr<int, decltype(l)> up3(new int[10], l );
为了避免“传递function pointer 或者 lambda时必须指明deleter的类型”,可以使用 alias template这是自c11的语言特性:
template <typename T> using uniquePtr = std::unique_ptr<T, void(*)(T*)>; //... uniquePtr<int> up(new int[10], [](int* p)(int* p) { //... delete[] p; });
下面是个完整的例子 :
#include<memory> #include<iostream> #include<dirent.h> #include<string> #include<cstring> #include<cerrno> using namespace std; class DirDirCloser{ public: void operator() (DIR* dp){ if(closedir(dp) != 0){ std::cerr << "Failed closedir()" << std::endl; } } }; int main(){ //open current dir unique_ptr<DIR,DirDirCloser> pDir(opendir(".")); //... //process each dir entry; struct dirent 8 dp; while((dp = readdir(pDir.get())) != nullptr){ string filename(dp->d_name); cout << "process " << filename << endl; } }
如果必须昂处理closedir的返回值,可以直接传递closedir() 成为一个function pointer,并明确指出deleter是个function pointer。但是务必晓得,下面这个经常被推荐的声明式子并不保证可移植:
unique_ptr<DIR, int(*)(DIR*)> pDir(opendir("."),closedir);
因为closedir具备extern “C” linkage,所以在C++代码中并不能保证被转换为int(*)(DIR*)。下面的方式具备可移植性,其需要一个中介类型定义式如下:
extern "C" typedef int(*DIRDeleter)(DIR*); unique_ptr<DIR, DIRDeleter> pDir(open("."),closedir);