(读书笔记:全部摘抄自cPP标注库)
C++ 11 中,标准库提供了两大类smart pointer:
1. Class shared_ptr 实现共享式拥有(shared ownership)概念。多个 只能指针 可以指向相同的对象,改对象和其相关资源会在 “最后一个reference被销毁” 时释放。为了在结构较为复杂的情境中执行上述工作,标准库提供了weak_ptr ,bad_weak_ptr 和 enable_shared_from_this等辅助类。
2,Class_unique_ptr 实现独占式拥有(exclusive ownership) 或 严格拥有(strict ownership)概念。保证同一时间内只有一个smart pointer可以指向该对象。你可以移交拥有权。他对于 避免资源泄露特别有用。
c++98只让c++标准库提供一个smart pointer class : auto_ptr<>,其设计是为了执行现今的unique_ptr所提供的服务。然而由于当时缺乏语言特性如“针对构造和赋值”的move语义,以及其他瑕疵,这个class不易被理解且容易出错。因此在TR1引入clss shared_ptr,c++11引入class unique_ptr之后,auto_ptr成为c++11中被证实反对的成分,除非老旧代码需要编译,否则你不应使用它。
所有smart pointer class 都被定义于头文件<memory>内。
shard_ptr 的目标是没有蛀牙(在其所指向的对象不再被需要之后,自动释放与对象相关的资源)。
使用shard_ptr
#include<iostream> #include<string> #include<vector> #include<memory> using namespace std; int main(){ shared_ptr<string> pNico(new string("nico")); shared_ptr<string> pJutta(new string("jutta")); (*pNico)[0] = 'N'; pJutta->replace(0,1,"J"); vector<shared_ptr<string>> whoMadeCoffee; whoMadeCoffee.push_back(pJutta); whoMadeCoffee.push_back(pJutta); whoMadeCoffee.push_back(pNico); whoMadeCoffee.push_back(pNico); for(auto ptr : whoMadeCoffee) { cout << *ptr << " "; } cout << endl; *pNico = " Nicolai"; for(auto ptr : whoMadeCoffee) { cout << *ptr << " "; } cout << endl; cout << "use_cout " << whoMadeCoffee[0].use_count() << endl; }
需要注意的是,由于“接受单一pointer作为唯一实参" 的构造函数是explicit,所以这里不能使用赋值符,因为那样的话会被视为需要一个隐式转换,然而新式的语法是被接受的:
shared_ptr<string> pNico = new string("nico"); //ERROR shared_ptr<string> pNico{new string("nico")}; //OK
也可以使用便捷的 make_shared() :
shared_ptr<string> pNico = make_shared<string>("nico");
这种方式比较快,也比较安全,因为它使用一次而非二次分配:一次针对对象,另一次针对"shared pointer 用以控制对象" 的shared data。
另一种写法是,先声明shard pointer ,然后对它赋值一个new pointer,然后不可以使用assignment操作符,必须改用reset():
pNico3 = new string("nico"); //ERROR :no assignment for ordinary pointers pNico3.reset(new string("nico")); //Ok
定义一个Deleter
定义一个deleter,例如让它在“删除被指向对象”之前先打印一条信息:
shared_ptr<string> pNico(new string("nico"), [](string * p){ cout << "delete " << *p << endl; delete p; }); pNico = nullptr; //pNico does not refer to string any longer whoMadeCoffee.resize(2); // all copies of string in pNico are destoryed
其中函数对象 D del参数是一个lambda表达式。
关于shared_ptr 的构造函数,参考链接
对付Array
shared_ptr提供的default deleter 调用的是delete,不是delete[] ,这意味着局限性,只有当shared_ptr拥有“由new建立起来的单一对象”,default deleter才试用。然而很不幸,为array建立一个shared_ptr是可能的,却是错误的:
std::shared_ptr<int> p(new int[10]); //ERROR,but compiles
故,如果你试用new[]建立一个array of object ,必须自己定义自己deleter。你可以传递一个函数,或者函数对象,或者lambda,让它们针对传入的寻常指针调用delete[]。例如:
shared_ptr<int> p(new int[10], [](int *p) { delete[] p; });
也可以试用为unique_ptr而提供的辅助函数作为deleter,其内调用deleter[]:
std::shared_ptr<int> p(new int[10], std::default_delete<int[]> ());
需要注意的是:shared_ptr 和unique_ptr 以稍稍不同的方式处理deleter。例如unique_ptr允许只传递对应的元素类型作为template实参,但这对shared_ptr就不行:
std::unique_ptr<int[]> p(new int[10]); //OK std::shared_ptr<int[]> p(new int[10]); //ERROR:does not compile
此外,对于 unique_ptr,你必须明确给予第二个template实参,指出自己的deleter:
std::unique_ptr<int,void(*)(int *)> p(new int[10], [](int *p) { delete[] p; });
还需注意,shared_ptr不提供operator[]。至于unique_ptr,它有一个针对array的偏特化版本,该版本提供operator[] 取代 operator* 和 operator-> 。之所以有此差异是因为,unique_ptr在效能和弹性上进行了优化。
其他析构策略
eg1:假设我想确保“指向某个临时文件”之最末一个reference被销毁时,该文件即被移除:
class FileDeleter{ private: string filename; public: FileDeleter (const string& fn) : filename(fn){ } void operator () (std::ofstream* fp){ fp->close(); std::remove(filename.c_str()); } }; int main(){ //create and open temporary file: std::shared_ptr<std::ofstream> fp(new std::ofstream("tmpfile.txt"),FileDeleter("tmpfile.txt")); ... }
eg2: 使用shared_ptr处理共享内存:
#include<memory> #include<sys/mman.h> #include<fcntl.h> #include<unistd.h> #include<cstring> #include<cerrno> #include<string> #include<iostream> using namespace std; class SharedMemDetacher{ public: void operator()(int* p){ std::cout << "unlink /tmp1234" << std::endl; if(shm_unlink("/tmp1234") != 0) { std::cerr << "OOPS :shm_unlink() failed " << std::endl; } } }; std::shared_ptr<int> getSharedMem(int num){ void* mem; int shmfd = shm_open("/tmp1234",O_CREAT|O_RDWR,0777); if(shmfd < 0){ cout << "shmfd open fialed"; throw std::string(strerror(errno)); } if(ftruncate(shmfd,num*sizeof(int)) == -1) { throw std::string(strerror(errno)); cout << "ftruncate failed "; } mem = mmap(nullptr,num*sizeof(int),PROT_READ | PROT_WRITE,MAP_SHARED,shmfd,0); if(mem == MAP_FAILED){ throw std::string(strerror(errno)); cout << "map 失败 "; } return std::shared_ptr<int>(static_cast<int*>(mem),SharedMemDetacher()); } int main() { std::shared_ptr<int> smp(getSharedMem(100)); for(int i = 0;i<100;++i){ smp.get()[i] = i*42; } std::cout << "<return>" << std::endl; std::cin.get(); //release memory here: smp.reset(); //.... }
weak_ptr
在一些情况下,shared_ptr并不是总是按照期望运行的。比如
- 环式指向。比如两个shared_ptr互相指向对方,而一旦不存在其他reference指向它们,你想释放它们和其相应的资源。因为每个对象的use_count() 仍是1,所以shared_ptr不会释放数据。
- 在一些情况下,你想“共享某个对象,而不拥有”。你要的语义是:reference的寿命比其所指向的对象寿命更长。因此shared_ptr绝不释放对象,而寻常pointer可能不会注意到他们所指向的对象已经不再有效。导致了“访问已被释放的数据”的风险。
于是,标准库提供了class weak_ptr,允许“共享但不拥有”对象。这个class会建立一个shared_ptr。一旦最后一个拥有该对象的shread pointer失去了拥有权,任何weak pointer 都会自动成空。因此,在default 和copy构造函数之外,class weak_ptr只提供“接受一个shared_ptr”的构造函数。
你不能使用操作符 * 和 -> 访问weak_ptr对象,而是必须另外建立一个shared pointer。
class Person{ public: string name; shared_ptr<Person> monther; shared_ptr<Person> father; vector<shared_ptr<Person>> kids; Person(const string& n, shared_ptr<Person> m = nullptr, shared_ptr<Person> f = nullptr) : name(n),monther(m),father(f){ } ~Person(){ cout << "delete " << name << endl; } }; shared_ptr<Person> initFamily(const string& name){ 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)); mom->kids.push_back(kid); dad->kids.push_back(kid); return kid; } int main(){ shared_ptr<Person> p = initFamily("nico"); cout << "nico's family exists "; cout << "- nico is shared " << p.use_count() << " times "; cout << "- name of 1st kid of nico's mom: " << p->monther->kids[0]->name << endl; p = initFamily("jim"); cout << "Jim's family exists "; })
在
p = initFamily("jim");
被赋值前,nico被共享了3次。但是如果释放最末一个指向该家庭的handle(也就是p)---也许是对p指派了一个新Person或者一个nullptr,也许是main()结束离开作用域---总之,没有任何Person被释放,因为它们都仍至少被一个shared pointer指向。于是每个Person的析构函数从未被调用(打印delete name):
nico's family exists
- nico shared 3times
- name of 1st kid of nicos mom: nico
jim's family exists
这种情况下使用weak_ptr会带来帮助。
class Person{ public: string name; shared_ptr<Person> monther; shared_ptr<Person> father; //vector<shared_ptr<Person>> kids; vector<weak_ptr<Person>> kids; // Person(const string& n, shared_ptr<Person> m = nullptr, shared_ptr<Person> f = nullptr) : name(n),monther(m),father(f){ } ~Person(){ cout << "delete " << name << endl; } };
这样就打破了shared_ptr形成的循环,使得在kid到parent方向上用的是shared pointer,parent 到 kids 方向上是weak pointer。
一旦失去了指向某个家庭的handle,这个家庭中的kid对象也就失去了其最末一个拥有者,导致其父母也都失去了最末拥有者。于是,最初以new建立的所有对象,现在都被delete,因此他们的析构函数都会被调用。
需要注意到是,使用weak pointer时候,必须改变被指向对象的访问方式,。不应该再调用 p->mother->kids[0]->name,现在必须在式子上加上lock():
p->mother->kids[0].lock()->name
这个导致新产生一个“得自于kids容器内含之weak_ptr”的shared_ptr。如果无法进行这样的改动---例如由于对象的最末拥有者也在此时释放了对象---lock()会产生个empty shared_ptr。这种情况下调用* 或者 ->操作符会引发不明确行为。
如果不确定隐身于weak pointer背后的对象是否存活,有以下三个选择:
1. 调用expeired() 。他会在weak_ptr不再共享对象时返回true。这等同于检查use_count()是否为0,但是速度较快。
2. 使用相应的shared_ptr构造函数明确的将weak_ptr转换为一个shared_ptr。如果卑职对象已经不存在,该构造函数抛出一个异常bad_weak_ptr。派生自std::exception
shared_ptr<stirng> sp(new string("hi")); weak_ptr<string> wp = sp; shared_ptr<string> p(wp);
3. 调用use_count(),询问相应对象的拥有者数量。,这通常用于调试,因为并不是总是很有效率。