smart pointers
在传统上,C++使用new
和delete
两个操作符进行内存管理,但是会有各种问题,比如:忘记释放内存、重复释放内存。考虑到这些问题,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_ptr与shared_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
对于一个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,可以使用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。
进一步阅读材料,可以参考:
- How do I pass a unique_ptr argument to a constructor or a function?
- Passing std::unique_ptr as function argument
作为函数返回值
虽然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 */
参考资料
- C++ Primer - 12.1.5 unique_ptr