[c++] Why should I use Smart Pointers

深入理解智能指针


专有指针

Ref: unique_ptr的使用和陷阱

一、初始化

只可以使用new来分配内存,不可 拷贝和赋值。

unique_ptr<int> up1(new int());    // okay,直接初始化
unique_ptr<int> up2 = new int();   // error! 构造函数是 explicit
unique_ptr<int> up3(up1);          // error! 不允许拷贝

智能指针的创建。

// 智能指针的创建  
unique_ptr<int> u_i;       // 创建空智能指针
u_i.reset(new int(3));     // 绑定动态对象  

unique_ptr<int> u_i2(new int(4));  // 创建时指定动态对象
unique_ptr<T,D> u(d);      // 创建空 unique_ptr,执行类型为 T 的对象,用类型为 D 的对象 d 来替代默认的删除器 delete

 

二、基本操作

unique_ptr<T> up 
空的unique_ptr,可以指向类型为T的对象,默认使用delete来释放内存

unique_ptr<T,D> up(d) 
空的unique_ptr同上,接受一个D类型的删除器d,使用删除器d来释放内存

up = nullptr 
释放up指向的对象,将up置为空

up.release() 
up放弃对它所指对象的控制权,并返回保存的指针,将up置为空,不会释放内存

up.reset(…) 
参数可以为 空、内置指针,先将up所指对象释放,然后重置up的值.
View Code

有意思的地方,指针设置为空,则控制的那块“空间”也会自动被执行释放操作。

// 所有权的变化  
int *p_i = u_i2.release();                // 释放:释放所有权  
unique_ptr<string> u_s(new string("abc"));  
unique_ptr<string> u_s2 = std::move(u_s); // 转移:所有权转移(通过移动语义),u_s所有权转移后,变成“空指针” 
u_s2.reset(u_s.release());                // 所有权转移:释放+赋值 == 转移
u_s2=nullptr;                            // 显式销毁所指对象,同时智能指针变为空指针。与u_s2.reset()等价

 

三、参数、返回值

unique_ptr不可拷贝和赋值,那要怎样传递unique_ptr参数和返回unique_ptr呢? 

事实上不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr (C++ Primer 5th p418)

 

  • 返回值

//从函数返回一个unique_ptr
unique_ptr func1(int a)
{
    return unique_ptr<int> (new int(a));
}
 
//返回一个局部对象的拷贝
unique_ptr func2(int a)
{
    unique_ptr<int> up(new int(a));
    return up;
}

 

  • 参数

使用 “引用”,避免所有权的转移,或者暂时的移交所有权。

// 传引用
void
func1(unique_ptr<int> &up){ cout<< *up <<endl; }

// 传值 unique_ptr
<int> func2(unique_ptr<int> up){ cout<< *up <<endl; return up; } unique_ptr<int> up(new int(10)); // 传引用,不拷贝,不涉及所有权的转移 func1(up);
// 暂时转移所有权,函数结束时返回拷贝,重新收回所有权 up = func2(unique_ptr<int> (up.release()));

 

四、删除器

类似shared_ptr,用unique_ptr管理非new对象没有析构函数的类时,需要向unique_ptr传递一个删除器。

不同的是,unique_ptr管理删除器的方式,我们必须在尖括号中unique_ptr指向类型后面提供删除器的类型,在创建或reset一个这种unique_ptr对象时,必须提供一个相同类型的可调用对象(删除器),这个删除器接受一个T*参数。

 

五、常见问题

Ref: 将unique_ptr传递给函数

用作参数,加上 const,至少不准轻松地被修改.

但const unique_ptr<Device>&取代每一次Device*都是一个好的开始。 您显然无法复制unique_ptr并且您不想移动它。通过引用unique_ptr来替换现有函数体的主体可以继续工作。

现在有一个陷阱,您必须通过const &来阻止被调用者执行unique_ptr.reset()或unique_ptr().release()。

请注意,这仍然会将可修改的指针传递给设备。使用此解决方案,您无法轻松地将指针或引用传递给const Device。


Ref: 如何评价 C++11 的右值引用(Rvalue reference)特性?

如果函数按值返回,return语句又直接返回了一个栈上的左值对象(输入参数除外)时,标准要求:“优先调用移动构造函数,如果不符再调用拷贝构造函数”。尽管v是左值,仍然会优先采用移动语义,返回vector<string>从此变得云淡风轻。此外,无论移动或是拷贝,可能的情况下仍然适用编译器优化,但语义不受影响。
 
[知识1] 对于std::unique_ptr来说,这简直就是福音。函数内部 new 出一个unique_ptr。
unique_ptr<SomeObj> create_obj(/*...*/) {
  unique_ptr<SomeObj> ptr(new SomeObj(/*...*/));
  ptr->foo(); // 一些可能的初始化
  return ptr; // 左值
}
unique_ptr<SomeObj> create_obj(/*...*/) {
  return unique_ptr<SomeObj>(new SomeObj(/*...*/));  // 右值
}

在工厂类中,这样的语义是非常常见的。返回unique_ptr能够明确对所构造对象的所有权转移,特别的,这样的工厂类返回值可以被忽略而不会造成内存泄露。上面两种形式分别返回 栈上的左值和右值,但都适用移动语义(unique_ptr不支持拷贝)。

 
[知识2] std::unique_ptr放入容器
曾经,由于vector增长时会复制对象,像std::unique_ptr这样不可复制的对象是无法放入容器的。但实际上vector并不复制对象,而只是“移动”对象。所以随着移动语义的引入,std::unique_ptr放入std::vector成为理所当然的事情。
 
容器中存储 std::unique_ptr 有太多好处。想必每个人都写过这样的代码:繁琐暂且不说,异常安全也是大问题。使用vector<unique_ptr<T>>,完全无需显式析构,unqiue_ptr自会打理一切。完全不用写析构函数。
MyObj::MyObj() {
  for (...) {
    vec.push_back(new T());
  }
  // ...
}

MyObj::~MyObj() {
  for (vector<T*>::iterator iter = vec.begin(); iter != vec.end(); ++iter) {
    if (*iter) delete *iter;  // 其实这里没什么用
  }
  // ...
}

 

 

共享指针

一、学习资源

C++中的智能指针

Ref: https://zhuanlan.zhihu.com/p/71649913

 

二、销毁

[ 这部分仅关于:shared_ptr ]

vector销毁,vector中的指针们所指向的各自“空间“也需要销毁。

vector的某个指针改变,相联系的指针内容全部改变。

// util/sharedptr1.cpp
#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;
int main()
{
// two shared pointers representing two persons by their name
    shared_ptr<string> pNico(new string("nico"));
    shared_ptr<string> pJutta(new string("jutta"));
// capitalize person names
    (*pNico)[0] = ’N’;
    pJutta->replace(0,1,"J");
// put them multiple times in a container
    vector<shared_ptr<string>> whoMadeCoffee;
    whoMadeCoffee.push_back(pJutta);
    whoMadeCoffee.push_back(pJutta);
    whoMadeCoffee.push_back(pNico);
    whoMadeCoffee.push_back(pJutta);
    whoMadeCoffee.push_back(pNico);
// print all elements
    for (auto ptr : whoMadeCoffee) {
        cout << *ptr << " ";
    }
    cout << endl;
// overwrite a name again
    *pNico = "Nicolai";
// print all elements again
    for (auto ptr : whoMadeCoffee) {
        cout << *ptr << " ";
    }
    cout << endl;
// print some internal data
    cout << "use_count: " << whoMadeCoffee[0].use_count() << endl;
}
View Code

 

定义销毁行为

如果某个指针要放弃某一块内存时,使用 lambda 作为构造函数的第二个参数,定义销毁操作。

当对象的最后一个指针被调用时,lambda被调用。

shared_ptr<string> pNico(new string("nico"),
                         [](string* p) {
                                         cout << "delete " << *p << endl;
                                         delete p;
                                       }
);
...
pNico = nullptr; // pNico does not refer to the string any longer
whoMadeCoffee.resize(2); // all copies of the string in pNico are destroyed

 

处理数组

默认的销毁行为不会执行delete[],所以要自己定义销毁行为,例如:

std::shared_ptr<int> p(new int[10],
                       [](int* p) {
                         delete[] p;  // <---- lambda
                       }
);

另一种方式也可以:

std::shared_ptr<int> p(new int[10],
                       std::default_delete<int[]>());

 

清理临时文件

处理清理内存以外,还有其他的资源需要处理,以下是一个清理临时文件的示例:

共享指针,指向 ”输出流“ 这个 ”对象“。

#include <string>
#include <fstream> // for ofstream
#include <memory>  // for shared_ptr
#include <cstdio>  // for remove()

class FileDeleter { private: std::string filename;
public: FileDeleter (const std::string& fn): filename(fn) {
}
void operator () (std::ofstream* fp) { fp->close(); // close.file std::remove(filename.c_str()); // delete file } };
int main() { // create and open temporary file: std::shared_ptr<std::ofstream> fp( new std::ofstream("tmpfile.txt"), FileDeleter("tmpfile.txt") ); ... }

 

清理共享内存

shm_unlink主要用于linux Posix模式共享内存中的删除共享内存。

// util/sharedptr3.cpp
#include <memory>      // for shared_ptr
#include <sys/mman.h>  // for shared memory
#include <fcntl.h>
#include <unistd.h>
#include <cstring>     // for strerror()
#include <cerrno>      // for errno
#include <string>
#include <iostream>
class SharedMemoryDetacher { 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> getSharedIntMemory(int num) { void* mem;
int shmfd = shm_open("/tmp1234", O_CREAT|O_RDWR, S_IRWXU|S_IRWXG); if (shmfd < 0) { throw std::string(strerror(errno)); } if (ftruncate(shmfd, num*sizeof(int)) == -1) { throw std::string(strerror(errno)); }
mem
= mmap(nullptr, num*sizeof(int), PROT_READ|PROT_WRITE, MAP_SHARED, shmfd, 0); if (mem == MAP_FAILED) { throw std::string(strerror(errno)); } return std::shared_ptr<int>(static_cast<int*>(mem), SharedMemoryDetacher()); }
int main() { // get and attach shared memory for 100 ints: std::shared_ptr<int> smp(getSharedIntMemory(100));
// init the shared memory for (int i = 0; i < 100; ++i) { smp.get()[i] = i*42; }
// deal with shared memory somewhere else: ... std::cout << "<return>" << std::endl; std::cin.get();
// release shared memory here: smp.reset(); ... }

 

析构函数清理

当然还有一种更加清晰的实现方法:构造函数实现初始化,析构函数实现清理。这样就可以简单地使用shared_ptr管理对象。

 

三、初始化

(1) 为了避免隐式转换,智能指针不能使用赋值的方式初始化,使用括号初始化或者列表初始化是没有问题的。

(2) 另一种初始化的方法是使用make_shared<>,它是一种 更好且更安全 的方法:因为使用new时会创建一个对象,计算它的引用计数时会创建一个对象,而make_shared只会创建一个对象,并且不会出现控制模块失效的情况。

shared_ptr<string> p1 = make_shared<string>(10, '9');  
shared_ptr<string> p2 = make_shared<string>("hello");  
shared_ptr<string> p3 = make_shared<string>(); 

(3) 先定义一个智能指针再进行赋值。但是不能使用赋值运算符(=),必须使用reset函数。

shared_ptr<string> pNico4;
pNico4 = new string("nico"); // ERROR: no assignment for ordinary pointers
pNico4.reset(new string("nico")); // OK

 

 

弱指针

一、有什么特点

Ref: C++11中weak_ptr的使用

Ref: C++ STL 四种智能指针 [写得用心]

C++11标准库还定义了一个名为weak_ptr的辅助类,它是一种弱引用,指向shared_ptr所管理的对象。

weak_ptr 被设计为与 shared_ptr 共同工作,可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。同样,在weak_ptr析构时也不会导致引用计数的减少,它只是一个静静地观察者

weak_ptr没有重载operator*和->,这是特意的,因为它不共享指针,不能操作资源,这是它弱的原因。

weak_ptr并没有重载operator->和operator *操作符,因此不可直接通过weak_ptr使用对象。

weak_ptr提供了 expired() 与 lock() 成员函数:

 a) 前者用于判断weak_ptr指向的对象是否已被销毁,

 b) 后者返回其所指对象的shared_ptr智能指针(对象销毁时返回”空”shared_ptr)。

 

二、用法

基本的方法。

weak_ptr<T> w;         // 创建空 weak_ptr,可以指向类型为 T 的对象

// 共享另一个对象 weak_ptr<T> w(sp); // 与 shared_ptr 指向相同的对象,shared_ptr 引用计数不变。T必须能转换为 sp 指向的类型 w=p; // p 可以是 shared_ptr 或 weak_ptr,赋值后 w 与 p 共享对象

// 基本方法 w.reset(); // 将 w 置空 w.use_count(); // 返回与 w 共享对象的 shared_ptr 的数量 w.expired(); // 若 w.use_count() 为 0,返回 true,否则返回 false w.lock(); // 如果 expired() 为 true,返回一个空 shared_ptr,否则返回非空 shared_ptr

典型的 weak_ptr 使用方法。

#include <assert.h>
#include <iostream>
#include <memory>
#include <string>
using namespace std;

int main()
{
    shared_ptr<int> sp(new int(10));
    assert(sp.use_count() == 1);

// 创建 weak_ptr 通过 shared_ptr weak_ptr
<int> wp(sp);
assert(wp.use_count() == 1); if (!wp.expired()) {
// 通过 weak_ptr 得到 shared_ptr,然后修改一下值 shared_ptr
<int> sp2 = wp.lock(); *sp2 = 100; assert(wp.use_count() == 2); } assert(wp.use_count() == 1); cout << "int:" << *sp << endl; return 0; }

 

三、解决什么问题

weak_ptr 到底有什么作用呢?从上面那个例子看来,似乎没有任何作用。

(1) weak_ptr 用于解决”引用计数”模型 循环依赖 问题,weak_ptr指向一个对象,并不增减该对象的引用计数器。

(2) weak_ptr 用于配合shared_ptr使用,并不影响动态对象的生命周期,即其存在与否并不影响对象的引用计数器。

 

不能管理循环引用的对象。一个简单的例子如下:

(1) 互为 “夫妻” 的 “循环引用”。

int main(int argc, char** argv)
{  
    std::shared_ptr<Man>   m(new Man());  
    std::shared_ptr<Woman> w(new Woman());  
    if(m && w)
    {  
        m->setWife(w);  
        w->setHusband(m);  
    }  
    return 0;  
}

 

(2) 类的定义

做法就是代码注释的地方取消注释,取消 Woman 类或者 Man 类的任意一个即可,也可同时取消注释,全部换成弱引用 weak_ptr。

class Man
{  
private:  
    std::weak_ptr<Woman> _wife;  // 管理wife的生命周期,不方便使用shared_ptr
    // std::shared_ptr<Woman> _wife;  
public: void setWife(std::shared_ptr<Woman> woman) { _wife = woman; } void doSomthing() { if(_wife.lock()) {} } ~Man() { std::cout << "kill man\n"; } };
class Woman
{  
private:  
    std::weak_ptr<Man> _husband;  
    // std::shared_ptr<Man> _husband;  
public: void setHusband(std::shared_ptr<Man> man) { _husband = man; }
~Woman() { std::cout <<"kill woman\n"; } };

 

破解相互引用

在 Man 类内部会引用一个 Woman,Woman 类内部也引用一个 Man。

当一个 man 和一个 woman 是夫妻的时候,他们直接就存在了相互引用问题。

man 内部有个 用于管理wife生命期 的 shared_ptr 变量,也就是说 wife 必定是在 husband 去世之后才能去世。同样的,woman 内部也有一个管理 husband 生命期的 shared_ptr 变量,也就是说 husband 必须在 wife 去世之后才能去世。这就是循环引用存在的问题。

husband 的生命期由 wife 的生命期决定,wife 的生命期由 husband 的生命期决定,最后两人都死不掉,违反了自然规律,导致了内存泄漏。

lock()的价值

另外很自然地一个问题是:既然 weak_ptr 不增加资源的引用计数,那么在使用 weak_ptr 对象的时候,资源被突然释放了怎么办呢?

不用担心,因为不能直接通过 weak_ptr 来访问资源。需要访问资源的时候,weak_ptr 为你生成一个shared_ptr,shared_ptr 能够保证在 shared_ptr 没有被释放之前,其所管理的资源是不会被释放的。创建 shared_ptr 的方法就是 lock() 成员函数。

 

 

 

 

如何选择智能指针


一、下面给出几个使用指南

(1) 如果程序要使用多个指向同一个对象的指针,应选择 shared_ptr。这样的情况包括:

  a)将指针作为参数或者函数的返回值进行传递的话,应该使用 shared_ptr;
  b)两个对象都包含指向第三个对象的指针,此时应该使用 shared_ptr 来管理第三个对象;
  c)STL 容器包含指针。很多 STL 算法都支持复制和赋值操作,这些操作可用于 shared_ptr,但不能用于 unique_ptr(编译器发出warning)和 auto_ptr(行为不确定)。如果你的编译器没有提供 shared_ptr,可使用 Boost 库提供的 shared_ptr。

 

(2) 如果程序不需要多个指向同一个对象的指针,则可使用 unique_ptr。

如果函数使用 new 分配内存,并返还指向该内存的指针,将其返回类型声明为 unique_ptr 是不错的选择。这样,所有权转让给接受返回值的 unique_ptr,而该智能指针将负责调用 delete。

可将 unique_ptr 存储到 STL 容器中,只要不调用将一个 unique_ptr 复制或赋值给另一个的算法(如 sort())。例如,可在程序中使用类似于下面的代码段。

unique_ptr<int> make_int(int n)
{
    return unique_ptr<int>(new int(n));  // 返回的是一个临时对象
}

void show(unique_ptr<int>& p1)  // 需要按照引用去show
{
    cout << *p1 << ' ';
}

int main()
{
    //...
    vector<unique_ptr<int>> vp(size);
    for(int i = 0; i < vp.size(); i++)
    {
        vp[i] = make_int(rand() % 1000);       //copy temporary unique_ptr
    }
vp.push_back(make_int(rand()
% 1000)); //ok because arg is temporary for_each(vp.begin(), vp.end(), show); //use for_each() //... }

其中 push_back 调用没有问题,因为它返回一个临时 unique_ptr,该 unique_ptr 被赋给 vp 中的一个 unique_ptr。另外,如果按值而不是按引用给 show() 传递对象,for_each() 将非法,因为这将导致使用一个来自 vp 的非临时 unique_ptr 初始化 pi,而这是不允许的。前面说过,编译器将发现错误使用 unique_ptr 的企图。

在 unique_ptr 为右值时,可将其赋给 shared_ptr,这与将一个 unique_ptr 赋给另一个 unique_ptr 需要满足的条件相同,即 unique_ptr 必须是一个临时对象。

 

(3) unique_ptr --> shared_ptr

与前面一样,在下面的代码中,make_int() 的返回类型为 unique_ptr<int>:

unique_ptr<int> pup(make_int(rand() % 1000));        // ok
shared_ptr<int> spp(pup);                            // not allowed, pup as lvalue
shared_ptr<int> spr(make_int(rand() % 1000));        // ok

模板 shared_ptr 包含一个显式构造函数,可用于将右值 unique_ptr 转换为 shared_ptr。shared_ptr 将接管原来归 unique_ptr 所有的对象。简而言之,您可以轻松有效地将std :: unique_ptr转换为std :: shared_ptr,但不能将std :: shared_ptr转换为std :: unique_ptr

Ref: C 11 unique_ptr和shared_ptr是否能够转换为彼此...

第一策略:

std::unique_ptr<std::string> unique = std::make_unique<std::string>("test");
std::shared_ptr<std::string> shared = std::move(unique);

 

第二策略:

std::shared_ptr<std::string> shared = std::make_unique<std::string>("test");

 

 

二、 更多的智能指针

上文简单地介绍了 C++ STL 的四种智能指针。当然,除了 STL 的智能指针,C++ 准标准库 Boost 的智能指针,比如 boost::scoped_ptr、boost::shared_array、boost::intrusive_ptr 也可以在编程实践中拿来使用,但这里不做进一步的介绍,有兴趣的读者可以参考:C++ 智能指针详解。

 End.

posted @ 2020-04-25 19:59  郝壹贰叁  阅读(135)  评论(0编辑  收藏  举报