C/C++学习之路: 智能指针
目录
- 前言
- shared_ptr
1. 前言
- 在C++中,动态内存的管理是通过一对运算符完成的:
- new:在动态内存中为对象分配空间并返回一个指向该对象的指针,可以选择对对象进行初始化
- delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存
- 动态内存使用容易出现问题,因为很难保证在正确的时间释放内存,如果忘记释放内存就会产生内存泄漏,或者在有指针引用内存的情况下释放就会产生引用非法内存的指针。
- 为了更方便也更安全使用动态内存,新的标准库提供了两种智能指针类型来管理动态对象。智能指针类似于常规指针,区别在于智能指针负责自动释放所指向的对象。
- 新标准库提供的这两种智能指针区别在于管理低层指针的方式:
- shared_ptr允许多个指针指向同一个对象。
- unique_ptr则独占所指向的对象。
- 标准库还定义了weak_ptr的伴随类,是一种弱引用,指向shared_ptr所管理的对象。
2. shared_ptr
- 类似于vector,智能指针也是模板,创建一个智能指针时,必须提供指针可以指向的类型。
shared_ptr<int> p1;
shared_ptr<list<string>> p2;
- 默认智能指针保存一个空指针。
- 智能指针的使用方式和普通指针类似,解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用智能指针就是检测它是否为空。
if (p1 && p1->empty()) {
*p1 = "hi";
}
-
下标列出了shared_ptr和unique_ptr都支持的操作。
-
shared_ptr独有的操作
1. make_shared函数
- 最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数,会在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。
- 当用make_shared时,必须指定要创建的对象的类型。
//指向一个值为42的int的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);
//指向一个值为"999999999"的string
shared_ptr<string> p4 = make_shared<string>(10, '9');
//指向一个值初始化的int,即值为0
shared_ptr<int> p5 = make_shared<int>();
- make_shared用参数来构造给定类型的对象,如果不传递任何参数,对象就进行值初始化。
//p6指向一个动态分配的空vector<string>
auto p6 = make_shared<vector<string>>();
2. shared_ptr的拷贝和赋值
- 当进行拷贝或赋值时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象:
auto p = make_shared<int>(42); //p指向的对象只有p一个引用者
auto q(p); //p和q指向相同对象,此对象有两个引用者
- 每个shared_ptr都有一个引用计数器,无论拷贝哪个shared_ptr,计数器都会递增。比如用一个shared_ptr初始化另一个shared_ptr,或者将它作为参数传递给一个函数以及作为函数的返回值时,所关联的计数器都会递增。
- 当给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域),计数器就会递减。
- 一旦一个shared_ptr计数器为0,就会自动释放自己所管理的对象。
auto r = make_shared<int>(42);
r = q; //给r赋值,令它指向另一个地址
// 递增q指向的对象的引用计数,递减r原来指向的对象的引用计数
// r原来指向的对象已没有引用者,会自动释放
- 上例中分配了一个int,将其职责保存在r中,接下来将一个新的值赋予r,在此情况下,r是唯一指向此int的shared_ptr,在把q赋给r后,此int被自动释放。
3. shared_ptr自动销毁所管理的对象
- 当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象。它是通过析构函数完成销毁工作的。
- 析构函数一般用来释放对象所分配的资源。例如string的构造函数会分配内存来保存构成string的字符。string的析构函数就负责释放这些内存。
- shared_ptr的析构函数会递减它所指向的对象的引用计数,如果引用计数为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存。
4. shared_ptr还会自动释放相关联的内存
- 当动态对象不再使用时,shared_ptr类会自动释放动态对象,这一特性使得动态内存的使用变得非常容易。
- 例如有一个函数,返回一个shared_ptr,指向一个Foo类型的动态分配的对象,对象是通过一个类型为T的参数进行初始化的:
shared_ptr<Foo> factory(T arg) {
//恰当处理arg
//shared_ptr负责释放内存
return make_shared<Foo>(arg);
}
- 由于factory返回一个shared_ptr,所以可以确保它分配的对象会在恰当的时刻被释放。例如,下面的函数将factory返回的shared_ptr保存在局部变量中:
void use_factory(T arg) {
shared_ptr<Foo> p = factory(arg);
//使用p,p离开作用域,它指向的内存会被自动释放掉
}
- 由于p是use_factory的局部变量,在use_factory结束时它将被销毁,当p被销毁时,将递减其引用计数并检查它是否为0,例子中,p是唯一引用factory返回的内存的对象。由于p将要销毁,p指向的这个对象也会被销毁,所占用的内存会释放。
- 但如果有其他shared_ptr也指向这块内存,它就不会被释放。
- 对于一块内存,shared_ptr类保证只要有任何shared_ptr对象引用它,它就不会被释放掉。
- 由于最后一个shared_ptr销毁前内存都不会被释放,保证shared_ptr在无用之后不再保留就很重要了。
- 如果忘记销毁程序不再需要的shared_ptr,程序仍会正常执行,但会浪费内存。
- 如果将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。
5. 使用了动态生存期的资源的类
- 程序使用动态内存出于以下三种原因之一
- 程序不知道自己需要使用多少对象
- 程序不知道所需对象的准确类型
- 程序需要在多个对象间共享数据
- 容器类是出于第一种原因而使用动态内存的典型例子。下面将定义一个类,使用动态内存是为了让多个对象能共享相同的底层数据。
- 在我们使用过的类中,分配的资源都与对应对象生存期一致。例如每个vector“拥有”自己的元素,当我们拷贝一个vector时,原vector和副本vector中的元素的相互分离的。
vector<string> v1; //空vector
{
vector<string> v2 = {"a", "an", "the"};
v1 = v2; // 从v2拷贝元素到v1中
} //v2被销毁,其中的元素也被销毁,v1有三个元素,是原来v2中元素的拷贝
- 由一个vector分配的元素只有当这个vector存在时才存在,当一个vector被销毁时,这个vector中的元素也都被销毁。
- 但某些类分配的资源具有与原来对象相独立的生存期。例如,定义一个Blob的类,保存一组元素,Blob对象的不同拷贝之间共享相同的元素,即当拷贝一个Blob时,原Blob对象及其拷贝应该引用相同的低层元素。
- 一般而言,如果两个对象共享底层的数据,当某个对象被销毁时,不能单方面地销毁底层数据:
Blob<string> b1; //空Blob
{ //新作用域
Blob<string> b2 = {"a", "an", "the"};
b1 = b2; // b1和b2共享相同的元素
} // b2被销毁了,但b2中的元素不能销毁,b1指向最初由b2创建的元素
- b1和b2共享相同的元素,当b2离开作用域后,这些元素必须保留,因为b1仍然在使用它们。
- 使用动态内存的一个常见原因是允许多个对象共享相同的状态。
6. 定义StrBlob类
- 定义一个管理string的类命名为StrBlob,实现一个新的集合类型最简单方法是使用某个标准库容器来管理元素,在本例中,将使用vector保存元素。
- 为了保证vector的元素继续存在,将vector保存在动态内存中。为每个StrBlob设置一个shared_ptr来管理动态分配的vector。此shared_ptr的成员将记录有多少个StrBlob共享相同的vector,并在vector的最后一个使用者被销毁时释放vector。
- 这个类提供的操作有修改访问元素的操作(如front和back),如果用户访问不存在的元素,会抛出一个异常。
- 类有一个默认构造函数和一个析构函数,接受单一的initializer_list类型参数。此构造函数可以接受一个初始化器的花括号列表。
class StrBlob {
public:
typedef std::vector<std::string>::size_type size_type;
StrBlob();
StrBlob(std::initializer_list<std::string> il);
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
//添加和删除元素
void push_back(const std::string &t) { data->push_back(t); }
void pop_back();
//元素访问
std::string &front();
std::string &back();
private:
std::shared_ptr<std::vector<std::string>> data;
//如果data[i]不合法,抛出一个异常
void check(size_type i, const std::string &msg) const;
};
- StrBlob类实现了size,empty和push_back成员。这些成员通过指向底层vector的data成员来完成工作。例如,对一个StrBlob对象调用size()会调用data->size(),以此类推。
7. StrBlob构造函数
- 两个构造函数都使用初始化列表来初始化其data成员,令它指向一个动态分配的vector。默认构造函数分配了一个空vector。
StrBlob::StrBlob() : data(make_shared<vector<string>>()) {
}
StrBlob::StrBlob(std::initializer_list<std::string> il) : data(make_shared<vector<string>>(il)) {
}
- 接受一个initializer_list的构造函数将其参数传递给对应的vector构造函数。此构造函数通过拷贝列表中的值来初始化vector的元素。
8. 元素访问成员函数
- pop_back、front和back操作访问vector中的元素,这些操作试图访问元素之前必须检查元素是否存在。
- 由于这些成员函数需要做相同的检查操作,所以为StrBlob定义了一个名为check的private工具函数,它检查一个给定索引是否在合法范围内。
- 除了索引,check还接受一个string参数传递给异常处理程序,这个string描述了错误内容。
void StrBlob::check(StrBlob::size_type i, const std::string &msg) const {
if (i >= data->size()) {
throw out_of_range(msg);
}
}
- pop_back和元素访问成员函数首先调用check。如果check成功,这些成员函数继续利用底层vector的操作来完成自己的工作。
std::string &StrBlob::front() {
//如果vector为空,check会抛出一个异常
check(0, "front ont empty StrBlob");
return data->front();
}
std::string &StrBlob::back() {
check(0, "back ont empty StrBlob");
return data->back();
}
void StrBlob::pop_back() {
check(0, "pop_back ont empty StrBlob");
data->pop_back();
}
9. StrBlob的拷贝,赋值和销毁
- StrBlob使用默认版本的拷贝、赋值和销毁成员函数来对此类型的对象进行操作。StrBlob类只有一个数据成员,它是shared_ptr类型,因此,当拷贝,赋值和销毁一个StrBlob对象时,它的shared_ptr成员会被拷贝,赋值和销毁。
- 拷贝一个shared_ptr会递增其引用计数,将一个shared_ptr赋予另一个shared_ptr会递增赋值号右侧shared_ptr的引用计数,而递减左侧shared_ptr的引用计数。
- 如果一个shared_ptr的引用计数变为0,它所指向的对象会被自动销毁,因此,对于StrBlob构造函数分配的vector,当最后一个指向它的StrBlob对象被销毁时也会随着销毁。
2. 直接管理内存
- C++定义了两个运算符来分配和释放动态内存。运算符new分配内存,delete释放new分配的内存。
- 相对于智能指针,使用这两个运算符管理内存非常容易出错,直接管理内存的类与使用智能指针的类不同,不能依赖对象拷贝、赋值和销毁操作的任何默认定义。
- 因此,使用智能指针的程序更容易编写和调试。
1. 使用new动态分配和初始化对象
- 在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向对象的指针:
int *p = new int; //p指向一个动态分配的、未初始化的无名对象
- 默认情况下,动态分配的对象是默认初始化的,意味着内置类型或组合类型的对象是指将是未定义的,而类的类型对象将用默认够赞函数进行初始化。
string *ps = new string; //初始化为空string
- 可以使用直接初始化方式来初始化一个动态分配的对象。
int *pi = new int(1024); //pi指向的对象的值为1024
string *ps = new string(10,'9'); // *ps为‘9999999999’
//vector有10个元素,值依次从0到9
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};
- 也可以对动态分配的对象进行值初始化,只需要在类型名后跟一对空括号就行
string *ps1 = new string; //默认初始化为空string
string *ps = new string(); //值初始化为空string
int *pi1 = new int; //默认初始化,*pi1的值未定义
int *pi2 = new int(); //值初始化为0,*pi2为0
- 对于定义了自己的构造函数的类类型(例如string)来说,要求值初始化是没有意义的。不管采用什么形式,对象都会通过默认构造函数来初始化。
- 但对于内置类型,两种形式的差别就很大:值初始化的内置类型对象有定义的值,而默认初始化的对象的值是未定义的。
- 对于类中那些依赖编译器合成的默认构造函数的内置类型成员,如果它们未在类中被初始化,那么值也是未定义的。
- 如果提供了一个括号包围的初始化器,就可以使用auto从此初始化器来推断想要分配的对象的类型。
- 但是,由于编译器要用初始化器的类型来推断要分配的类型,只有当括号中仅有单一初始化器材可以使用auto:
auto p1 = new auto(obj); //p指向一个与obj类型相同的对象,该对象用obj进行初始化
auto p2 = new auto{a,b,c}; // 错误:括号中只能有单个初始化器
- p1的类型是一个指针,指向从obj自动推断出的类型。如果obj是一个int,那么p1就是int *,如果obj是string,那么p1就是string *。
2. 动态分配的const对象
- 用new分配const对象是合法的
const int *pci = new const int(1024); //分配并初始化一个const int
const string *pcs = new const string; //分配并默认初始化一个const的空string
- 一个动态分配的const对必须进行初始化,对于一个定义了默认构造函数的类类型,const动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。
- 由于分配的对象是const的,new返回的指针是一个指向const的指针。
3. 内存耗尽
- 一旦一个程序用光了它所有可用的内存,new表达式就会失败。默认情况下,如果new不能分配所要求的内存空间,就会抛出一个类型为bad_alloc的异常,我们可以改变使用new的方式来阻止它抛出异常。
//如果分配失败,new返回一个空指针
int *p1 = new int; // 如果分配失败,new抛出std::bad_alloc
int *p2 = new (nothrow) int; //如果分配失败,new返回一个空指针
4. 释放动态内存
- 为了防止内存耗尽,在动态内存使用完毕后,必须将其归还给系统。通过delete表达式来将动态内存归还给系统。delete表达式接受一个指针,指向我们想要释放的对象:
delete p; //p必须指向一个动态分配的对象或者是一个空指针。
- delete表达式执行两个动作:销毁给定的指针指向的对象,释放对应的内存
5. 指针值和delete
- 传递给delete的指针必须指向动态内存分配的内存,或者是一个空指针。释放一块非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的。
int i, *pi1 = &i, *pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;
delete i; // 错误:i不是一个指针
delete pi1; // 未定义,pi1指向一个局部变量
delete pd; // 正确
delete pd2; // 未定义:pd2指向的内存已经被释放了
delete pi2; // 正确,释放一个空指针总是没有错误的
- 对于delete i的请求,编译器会生成一个错误信息,因为它知道i不是一个指针。
- 执行delete pi1和pd2所产生的错误则更具潜在危害:因为通常情况下,编译器不能分辨一个指针指向的是静态还是动态分配的对象。
- 类似的,编译器也不能分辨一个指针所指向的内存是否已经被释放掉了。
- 对于这些delete表达式,大多数编译器会编译通过,尽管是错误的。
- const对象的值不能被改变,但本身是可以被销毁的。
const int *pci = new const int(1024);
delete pci; // 正确,释放一个const对象
6. 动态对象的生存期直到被释放为止
- shared_ptr管理的内存在最后一个shared_ptr销毁时会自动释放。但对于通过内置指针类型来管理的动态对象,直到被显式释放之前都是存在的。
- 返回指向动态内存的指针的函数,调用者必须记得释放内存。
// factory返回一个指针,指向一个动态分配的对象
Foo* factory(T arg){
return new Foo(arg); //调用者负责释放此内存
}
- factory分配一个对象,但不delete,factory的调用者负责在不需要此对象时释放。
void use_factory(T arg) {
Foo *p = factory(arg); //使用*p但不delete
} // p离开了它的作用域,但它指向的内存没有被释放
- 修正这个错误的正确方法是在use_factory中释放内存
void use_factory(T arg) {
Foo *p = factory(arg);
// 使用p
delete p;
}
- 如果系统中的其他代码要使用use_factory所分配的对象,就要返回一个指针,指向它分配的内存
Foo* use_factory(T arg) {
Foo *p = factory(arg);
// 使用p
return p; //调用者必须释放内存
}
- 使用new和delete管理动态内存三个常见问题
- 忘记delete内存。忘记释放动态内存会导致“内存泄漏”问题,因为这种内存永远不可能被归还给自由空间了。查找内存泄漏问题是很难的,因为通常应用程序要运行很长时间后,真正耗尽内存时,才能检测这种错误。
- 使用已经释放掉的对象。通过在释放内存后将指针置为空,有时候可以检测出这种错误。
- 同一块内存释放两次。当有两个指针指向相同的动态分配对象时,可能发生这种错误。如果对其中一个指针进行delete操作,对象的内存就归还给自由空间了,如果随后又delete第二个指针,自由空间就可能被破坏。
3. shared_ptr和new结合使用
- 如果不初始化一个只能指针,就会被初始化为一个空指针。可以用new返回的指针来初始化智能指针。
shared_ptr p1; // shared_ptr可以指向一个double
shared_ptr p2(new int(1024)); // p2指向一个值为42的int
- 接受指针参数的智能指针构造函数是explicit的,因此,不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式。
shared_ptr> p1 = new int(1024); // 错误:必须使用直接初始化形式
shared_ptr p2(new int(1024)); // 正确:使用了直接初始化形式
- 因为不能进行内置指针到智能指针的隐式转换,因此第一条语句初始化是错误的,同样,一个返回shared_ptr的函数不能在返回语句中隐式转换一个普通指针。
shared_ptr clone(int p){
return new int§; //错误:隐式转换为 shared_ptr
}
-
一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放所关联的对象。
-
可以将智能指针绑定到一个指向其他类型资源的指针上,但是为了这样做,必须提供操作来代替delete。
-
定义和改变shared_ptr的其他方法
- shared_ptr p(q):p管理内置指针q所指向的对象,q必须指向new分配的内存,并且能够转换成T*类型
- shared_ptr p(u):p从unique_ptr u那里接管了对象的所有权,将u置为空
- shared_ptr p(q, d):p接管了内置指针q所指向对象的所有权,q必须能转换为T*类型。p将使用可调用对象d来代替delete
- shared_ptr p(p2, d):p是shared_ptr p2的拷贝,唯一区别是p将用可调用对象d来代替delete
- p.reset():若p是唯一指向其对象的shared_ptr,reset会释放此对象。
- p.reset(q):若传递了可选的参数内置指针q,会令p指向q,否则将p值为空。
- p.reset(q, d):如果还传递了参数d,将会调用d而不是delete来释放q
1. 不要混合使用普通指针和智能指针
- shared_ptr可以协调对象的析构,但仅限于自身的拷贝之间。这也就是为什么推荐使用make_shared而不是new的原因。这样就能在分配对象的同时将shared_ptr与之绑定,从而避免了无意中将同一块内存绑定到多个独立创建的shared_ptr上。
//在函数被调用是ptr被创建并初始化
void process(shared_ptr ptr){
//使用ptr
} // ptr离开作用域被销毁
- process的参数是值传递方法,因此实参会被拷贝到ptr中,拷贝一个shared_ptr会递增引用计数,因此,在process运行过程中,引用计数值至少为2。
- 当process结束时,ptr的引用计数会递减,但不会变为0。因此当局部变量ptr被销毁时,ptr指向的内存不会被释放。
shared_ptr p(new int(42)); //引用计数为1
process§; //拷贝p会递增它的引用计数,在process中引用计数值为2
int i = *p; //正确:引用计数为1
- 虽然不能传递给process一个内置指针,但可以传递一个临时的shared_ptr,这个shared_ptr是用一个内置指针显式构造的,但这样很可能导致错误:
int x(new int(42));
process(x); //错误:不能将int 转换为一个shared_ptr
process(shared_ptr(x)); //合法,但内存会被释放
int j = *x; //未定义的,x是一个空悬指针
- 上面将一个临时shared_ptr传递给process,当process结束时,临时shared_ptr对象就被销毁了,递减引用计数,此时引用计数为0,所指向的内存会被释放。x继续指向已经释放的内存,从而变成一个空悬指针。如果试图使用x的值,是未定义的。
- 当将一个shared_ptr绑定到一个普通指针时,就将内存管理交给了shared_ptr,就不应该再使用内置指针来访问shared_ptr所指向的内存了。
2. 也不要使用get初始化另一个指针指针或为智能指针赋值
- 智能指针的get函数返回一个内置指针,指向智能指针管理的对象。此函数是为这样一种情况设计的:需要向不能使用智能指针的代码传递一个内置指针,但使用get返回的指针的代码不能delete此指针。
- 虽然编译器不会给出错误信息,但将另一个智能指针也绑定到get返回的指针上是错误的:
shared_ptr p(new int(42)); //引用计数为1
int *q = p.get(); // 正确,但使用q要注意,不能让它管理的指针被释放
{
//未定义,两个独立的shared_ptr指向相同的内存
shared_ptr(q);
} // 程序块结束,q被销毁,指向的内存被释放
int foo = *p; //未定义,p指向的内存已经被释放了
- p和q指向相同的内存,由于它们是相互独立创建的,因此各自的引用计数为1.当q所在程序块结束时,q被销毁,导致q指向的内存被释放,从而p变成一个空悬指针,此时使用p时将发生未定义的行为。
- 而且,当p被销毁时,这块内存会被第二次delete
3. 其他shared_ptr操作
- shared_ptr定义了其他操作,可以用reset来讲一个新的指针赋予一个shared_ptr
p = new int(42); //错误,不能想一个指针赋予shared_ptr
p.reset(new int(42)); //正确,p指向一个新对象
- 与赋值类似,reset会更新引用计数,如果需要的话,会释放p指向的对象。reset成员经常与unique一起使用,来控制多个shared_ptr共享的对象。
- 在改变底层对象之前,需要检查自己释放是当前对象仅有的用户,如果不是,在改变之前要制作一份新的拷贝:
if(!p.unique())
p.reset(new string(*p));
*p += newVal;
4. 智能指针和异常
- 程序需要确保异常发生后资源能被正确释放,如果使用智能指针,即使程序过早结束也能确保在不需要时将其释放。
void f(){
shared_ptr sp(new int(42)); //分配一个新对象
// 这段代码抛出一个异常,且在f中未被捕获
} // 在函数结束时shared_ptr自动释放内存
- 当发生异常时,直接管理的内存是不会自动释放的,如果使用内置指针管理内存,且在new之后在对应的delete之前发生了异常,则内存不会被释放:
void f(){
int *ip = new int(42);
// 这段代码抛出一个异常,且在f中未被捕获
delete ip; // 在退出之前释放内存
}
1. 智能指针和哑类
- 分配了资源,而又没有定义析构函数来释放这些资源的类,可能会遇到与使用动态内存相同的错误:忘记释放资源。如果在资源分配和释放之间发生了异常,程序也会发送资源泄漏。
struct destination; // 表示我们正在连接什么
struct connection; // 使用连接所需的信息
connection connect(destination*); // 打开连接
void f(destination &d){
//获得一个连接,记住使用完要关闭
connection c = connect(&d);
//使用连接
//如果在f退出前忘记调用disconect,就无法关闭c
}
- 如果connection有一个析构函数,就可以在f结束时由析构函数自动关闭连接。但connection没有析构函数,可以使用shared_ptr来保证connection被正确关闭。
2. 使用我们自己的释放操作
- 默认情况下,shared_ptr假定它们指向的是动态内存,因此,当一个shared_ptr被销毁时,它默认对它管理的指针进行delete操作。
- 为了用shared_ptr来管理一个connection,必须定义一个函数来代替delete。这个删除函数必须能够完成对shared_ptr中保存的指针进行释放的操作。
- 我们的删除器必须接受单个类型为connection *的参数:
void end_connection(connection *p) {disconect(*p);}
- 当创建一个shared_ptr时,可以传递一个指向删除器函数的参数:
void f(destination &d){
connection c = connect(&d);
shared_ptr p(&c, end_connection);
//使用连接
//当f退出时(及时是由于异常而退出),connection会被正确关闭
}
- 当p被销毁时,不会对自己保存的指针执行delete,而是调用end_connection。
- 如果f正常退出,那么p的销毁会作为结束处理的一部分,如果发生了异常,p同样会被销毁,从而连接被关闭。
1. 智能指针陷阱
- 正确使用智能指针必须坚持的基本规范
- 不使用相同的内置指针值初始化(或reset)多个智能指针
- 不delete get()返回的指针
- 不适用get()初始化或reset另一个智能指针
- 如果使用get()返回的指针,最后一个对应的智能指针销毁后,指针就变为无效了。
- 如果智能指针管理的资源不是new分配的内存,需要传递一个删除器
5. unique_ptr
- 一个unique_ptr拥有它所指向的对象,与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。
- 当unique_ptr被销毁时,所指的对象也被销毁。
- 当定义一个unique_ptr时,需要将unique_ptr绑定到一个new返回的指针上。初始化unique_ptr必须采用直接初始化形式。
unique_ptr<double> p1; // 可以指向一个double的unique_ptr
unique_ptr<int> p2(new int(42)); // p2指向一个值为42的int
- 由于unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作
unique_ptr<string> p1(new string("hello));
unique_ptr<string> p2(p1); //错误:unique_ptr不支持拷贝
unique_ptr<string> p3;
p3 = p2; //错误:unique_ptr不支持赋值
-
unique_ptr操作
- unique_ptr u1:空unique_ptr,可以指向类型为T的读写,u1会使用delete来释放它的指针。
- unique_ptr<T, D> u2:u2会使用一个类型为D的可调用对象来释放它的指针
- unique_ptr<T, D> u(d):空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete
- u = nullptr:释放u指向的对象,将u置为空
- u.release():u放弃对指针的控制权,返回指针,并将u置为空
- u.reset():释放u指向的对象
- u.reset(q):如果提供了内置指针q,令u指向这个对象,否则将u置为空(u.reset(nullptr)
-
虽然不能拷贝或赋值unique_ptr,但可以通过调用release或reset将指针所有权从一个(非const)unique_ptr转移给另一个unique_ptr:
// 将所有权从p1(指向hello)转移给p2
unique_ptr<string> p1(new string("hello"));
unique_ptr<string> p2(p1.release()); // release将p1置为空
unique_ptr<string> p3(new string("trex"));
p2.reset(p3.release()); //将所有权从p3转移给p2,reset释放了p2原来指向的内存
- release成员返回unique_ptr当前保存的指针并将其置为空。因此,p2被初始化为p1原来保存的指针,而p1被置为空。
- reset成员接受一个可选的指针参数,令unique_ptr重新指向给定的指针。如果unique_ptr不为空,它原来指向的对象被释放。因此,对p2调用reset释放了用“hello”初始化的string所使用的内存,将p3对指针的所有权转移给p2,并将p3置为空。
- 调用release会切断unique_ptr和它原来管理的对象间的联系,release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。
- 如果我们不用另一个智能指针来保存release返回的指针,程序就要负责资源的释放。
p2.release(); //错误,p2不会释放内存,而且我们丢失了指针。
auto p = p2.release(); //正确,但我们必须记得delete(p)
1. 传递unique_ptr参数和返回unique_ptr
- 不能拷贝unique_ptr的规则有一个例外:可以拷贝或赋值一个将要被销毁的unique_ptr。
unique_ptr<int> clone(int p){
//正确:从int* 创建一个unique_ptr<int>
return unique_ptr<int>(new int(p));
}
- 还可以返回一个局部对象的拷贝
unique_ptr<int> clone(int p){
unique_ptr<int> ret(new int(p));
// ...
return ret;
}
- 向后兼容:auto_ptr
- 标准库的较早版本包含一个名为auto_ptr的类,具有unique_ptr的部分特性,但不是全部,特别是:不能在容器中保存auto_ptr,也不能从函数中返回auto_ptr
- 虽然auto_ptr是标准库的一部分,但编写程序时应该使用unique_ptr
2. 向unique_ptr传递删除器
- 类似shared_ptr,unique_ptr默认情况下用delete释放它指向的对象,我们可以重载一个unique_ptr默认的删除器。
- 重载的删除器必须在尖括号中unique_ptr指向类型之后提供删除器类型,在创建或reset一个unique_ptr类型的对象时,必须提供一个指定类型的可调用对象(删除器)
//p指向一个类型为objT的对象,并使用一个类型为delT的对象释放objT对象
//它会调用一个名为fcn的delT类型对象
unique_ptr<objT, delT> p(new objT, fcn);
void f(destination &d){
connection c = connect(&d); // 打开连接
unique_ptr<connection, decltype(end_connection)*> p(&c,end_connection);
//使用连接
//当f退出时(即使是由于异常而退出),connection会被正常关闭
}
- decltype用来指明函数指针类型,由于decltype(end_connection)返回一个函数类型,所以需要添加一个*来指出我们正在使用该类型的一个指针。
6. weak_ptr
-
weak_ptr是一种不控制所指向对象生存期的智能指针,指向一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shread_ptr不会改变shared_ptr的引用计数。
-
一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,也会被释放。
-
weak_ptr表
- weak_ptr w:空weak_ptr可以指向类型为T的对象
- weak_ptr w(sp):与shared_ptr sp指向相同对象的weak_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,否则返回一个指向w的对象的shared_ptr
-
当创建一个weak_ptr时,要用一个shared_ptr来初始化它:
auto p = make_shared(42);
weak_ptr wp§; // wp共享p;p的引用计数未改变
- wp指向的对象可能被释放掉,由于对象可能不存在,所以不能使用weak_ptr直接访问对象,而必须调用lock来检查weak_ptr指向的对象是否存在,如果存在,lock返回一个指向共享对象的shared_ptr。
if(shared_ptr np = wp.lock()){ //如果np不为空则条件成立
// 在if中,np与p共享对象。
}