C++内存管理:智能指针与unique_ptr


smart pointers

在传统上,C++使用newdelete两个操作符进行内存管理,但是会有各种问题,比如:忘记释放内存、重复释放内存。考虑到这些问题,C++中提出了smart pointers的概念(取名太low,差评!)。

smart pointers有点像Java的虚拟机,自动管理对象的内存空间。从实现机制上,通过引用计数来实现,在Java中的一些虚拟机中,有些也是用引用计数来判断对象是否存活,以实现垃圾回收,但是现在JVM很少使用这种机制了,从这个角度来说,C++是不是落后了,哈哈哈。

A smart pointer acts like a regular pointer with the important exception that it automatically deletes the object to which it points.

smart pointers中一般有3种:

  • shared_ptr, which allows multiple pointers to refer to the same object.
  • unique_ptr, which “owns” the object to which it points.
  • weak_ptr, which is a weak reference to an object managed by a shared_ptr.

All three are defined in the <memory> header.

unique_ptr

特点

unique_ptrshared_ptr是不同的:

  • unique_ptr 完全占有它所指向的object;
    • 意味着,这种是完全控制的、独占的、排他的;
    • 任何时刻,只有一个unique_ptr能指向特定的对象,即不会出现一个内存中的object,被多个unique_ptr所指向的情况;
  • 被该unique_ptr指向的object的lifetime受到该智能指针的影响,当该智能指针destroyed,其指向的object也会destroyed。

初始化

对于unique_ptr的出初始化,与shared_ptr有相同和不同的部分:

  • 相同点

    它们都可以使用new来完成初始化,也都必须是direct initialization

    // unique_ptr that can point at an int, but initialzied to nullptr 
    unique_ptr<int> p1;
    unique_ptr<int> p2();
    // p3 points to int with value 42
    unique_ptr<int> p3(new int(42));
    
    cout << (p1 == nullptr) << endl;
    cout << (p2 == nullptr) << endl;
    cout << *p2 << endl;
    cout << *p3 << endl;
    // output
    /**
    1
    0
    1
    42
    */
    

    从输出的数据可以看出几个关键点:

    • unique_ptr<int> p1中,返回的是一个指向int类型的nullptr
    • 通过使用new来direct iniliazation,是C++11中初始化一个非空指针的合适的方式;
    • 最奇怪的是,使用unique_ptr<int> p2()来初始化,得到竟然不是nullptr,而是一个有效的unique_ptr,并且其指向的数字为1,且是固定的,不是一个undefined value,可能与unique_ptr的构造函数的实现有关,但是考虑到这种奇怪的问题,强烈禁止使用这种方式来初始化unique_ptr
  • 不同点

    在C++11中,shared_ptr可以使用make_shared来初始化,但是在unique_ptr中不可以。直到C++14中,才可以使用make_unique来初始化,如下:

    auto pi = make_unique<int>(10);
    auto ps1 = make_unique<string>("Hello, world");
    auto ps2 = make_unique<string>(5, 'q');
    
    cout<< *pi << endl;
    cout<< *ps1 << endl;
    cout<< *ps2 << endl;
    
    /**
    output:
    cout<< *pi << endl;
    cout<< *ps1 << endl;
    cout<< *ps2 << endl;
    */
    
    • 其中,make_unique<string>(...)函数的参数为string的构造函数要求的参数类型;
    • 并且,在支持C++14的场景下,强烈推荐使用这种方式来初始化unique_ptr

copy & assignment & transfer

既然unique_ptr是独占一段内存中的object的,因此就不能将其copy或者assignment给别人,因为一份object不允许变成两份。

unique_ptr<int> p1(new int(10));
// error: copy constructor
unique_ptr<int> p2(p1);
// error: normal assignment
unique_ptr<int> p3;
p3 = p1;

编译输出的结果如下:

尝试对unique_ptr进行拷贝和赋值

但是一个unique_ptr对于一个object的控制权虽然不能copy,但是可以transfer。

一个不太合适的对比,皇帝的权力不能共享,但是换个人来当皇帝是可以的。

可以用两种方法实现控制权的转移。

release()

该函数的调用一般是为了将控制权转移出去,打破了unique_ptr及其指向的object之间的联系,完成了2件事情:

  • 返回存储在unique_ptr中的指针;
  • 使得unique_ptr变成nullptr

一般用来初始化另一个智能指针。

// init p1
unique_ptr<int> p1(new int(10));
// return a unique_ptr and make p1 a nullptr
// p2 is initialized with unique_ptr which is from p1.
unique_ptr<int> p2(p1.release());

cout << *p2 << endl;
// output: 10

上例中,将p1.release()之后,将p1指向的对象交给p2负责,自己成为nullptr。同时,也衍生出了一种行为,当p1.release()之后,没有人接盘怎么办?如下:

unique_ptr<int> p1(new int(10));
p1.release();

此时会导致p1自己变成了nullptr,同时其指向的object没有人显式接盘,也就没有人负责free,如果只是上述这种写法,会导致这个object对应的内存无法被释放,此文有所描述,因此,此时应该如下:

auto p = p1.release();
// free p explicitly
delete p;

此时,p1.release()之后,指针p接盘了,因此p1没责任了,可以自由地死去了,由p负责之前喜指向的object了,赵氏孤儿的既视感。

move()

这函数类似于release,也完成了unique_ptr控制权的转移。

该函数在右值引用中也有出现。

unique_ptr<int> p1(new int(10));
unique_ptr<int> p2;
// move makes p1 a nullptr
p2 = std::move(p1);
cout << *p2 << endl;
// output: 10

例子:release()导致的困惑问题

最近看到一个让人迷惑的代码,下述代码的输出是什么?

如果你认为是崩溃,那可以打错、特错了,我一开始也是这么认为的!

class C {
public:
    void foo(){
        cout << "Foo" << endl;
    }
};

void func1(){
    unique_ptr<int> up1(new int(100));
    unique_ptr<int> up2(up1.release());

    if(up1) {
        cout << "up1 ok" << endl;
    }

    cout << "up1" << *up1 << endl;
    cout << "up2" << *up2 << endl;
}

void func2(){
    unique_ptr<C> a1(new C());
    a1 -> foo();
    unique_ptr<C> a2(a1.release());
    a2 -> foo();
    if(a1 == nullptr) {
        cout << "a1 == nullptr: " << (a1 == nullptr) <<  endl;
    }
    a1 -> foo();
}

int main(){
    func1();
    // func2();
    return 0;
}

当调用func1时会出现segmentation fault,但是同样的,当调用func2时,我们可能理所当然的认为也会segmentation fault,但是真实的输出如下:

Foo
Foo
a1 == nullptr: 1
Foo

nullptr可以调用成员函数,而且还不会崩溃,这说明啥?说明这个调用过程压根就没用上nullptr本身,根据文章所述,空指针可以调用成员函数,因为成员函数在所有类对象之间共享,并不属于特定对象。a1 -> foo()相当于C::foo(this),而函数foo中并未有成员变量,因此不会发生程序崩溃,当稍微改一下上例时,如下:

class C {
public:
    void foo(){
        cout << "Foo" << endl;
        cout << i << endl;
    }
private:
    int i;
};

当func2访问C的变量i时,就会出现segmentation fault。

用在函数中

作为参数传递

unique_ptr作为参数传递给函数时,也要遵守”完全独占“导致的禁止拷贝和赋值的原则。那么按照之前了解的函数传递方式:

  • pass by reference

    void func1(unique_ptr<int> &u){
        cout << *u << endl;
    }
    unique_ptr<int> p1(new int(10));
    // ok
    func1(p1);
    
  • pass by value

    • 对于直接使用pass by value

      void func2(unique_ptr<int> u){
          cout << *u << endl;
      }
      unique_ptr<int> p2(new int(10));
      // error, copy is forbidden.
      func2(p2);
      

      会导致如下的错误:
      直接使用pass by value

    • 为了pass by value,可以使用std::move()转移控制权。

      unique_ptr<int> p1(new int(10));
      // ok, use move to transfer ownership
      func2(std::move(p1));
      

      如果你了解右值引用和移动语义,可能会角色,这里使用move时,需要func2中的参数应该是&&形式,但是我们这里没有这么写,也是可以的,瞬间感觉到C++语言的严谨,哈哈。。。

    • 还可以隐式使用移动构造函数,完成函数的调用。

      // ok, call move constructor implicitly.
      func(make_unique<int>(10));
      

      因为在unique_ptr内部的实现中,通过使用move constructor禁止了拷贝构造函数,因此所以的拷贝和赋值都是调用move constructor和assignment。

    • 使用release()是不可以的。

      auto up = std::make_unique<int>(10);
      // error
      func(up.release());
      

      编译得到如下错误,因为release()返回的对象是普通的pointer,不能转换为smart pointers。
      通过release()隐式初始化unique_ptr被禁止

进一步阅读材料,可以参考:

作为函数返回值

虽然unique_ptr具有“独占”的特点,但是总会有“法外狂徒“。当unique_ptr作为函数的返回值时,是允许的。虽然在函数返回时,出现了copy或者assignment行为,但是“合法”,即便此时函数内部的unique_ptr在函数结束后,会被destroyed。

因为其作为返回值时,当函数内的unqiue_ptr死亡后,返回值所赋予的智能指针仍然是唯一指向之前object的unique_ptr,本质上不违反“独占”的特点,但是此时compiler仍会执行一种特殊的copy。

使用实例如下:

unique_ptr<int> func1(int p){
    // create a unique_ptr pointing to an int with value p
    return unique_ptr<int>(new int(p));
}

unique_ptr<int> func2(int p){
    // create a unique_ptr pointing to an int with value p
    unique_ptr<int> up(new int(p));
    // return copy of a unique_ptr
    return up;
}

reset的行为

reset(...)的行为有点复杂,可以概括为:打扫干净屋子再请客

  • 打扫干净屋子

    当没有参数时,其行为如下:

    • 删除unique_ptr指向的object;
    • 将自身置为nullptr;
    unique_ptr<int> p1(new int(10));
    p1.reset();
    cout << (p1 == nullptr) << endl;
    
    // output: 1
    
  • 请客

    当有参数时,行为如下:

    • 删除unique_ptr指向的object;
    • 使得自身的unique_ptr指向新的object;
    unique_ptr<int> p1(new int(10));
    unique_ptr<int> p2(new int(100));
    p1.reset(p2.release());
    
    cout << (p1 == nullptr) << endl;
    cout << *p1 << endl;
    /** output:
    0
    100
    */
    

参考资料

  1. C++ Primer - 12.1.5 unique_ptr

文章作者: alex Li
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 alex Li !
  目录