专栏简介:本专栏主要面向C++初学者,解释C++的一些基本概念和基础语言特性,涉及C++标准库的用法,面向对象特性,泛型特性高级用法。通过使用标准库中定义的抽象设施,使你更加适应高级程序设计技术。希望对读者有帮助!
目录
- 12.1动态内存与智能指针
- shared_ptr类
- make_shared函数
- shared_ptr的拷贝和赋值
- shared_ptr自动销毁所管理的对象
- shared_ptr还会自动释放相关联的内存
- 使用了动态生存期的资源的类
- 定义StrBlob类
- StrBlob构造函数
- StrBlob的拷贝、赋值和销毁
- 直接管理内存
- 使用new动态分配和初始化对象
- 动态分配的const对象
- 内存耗尽
- 释放动态内存
- 指针值和delete
- 动态对象的生存期直到被释放时为止
- 动态内存管理非常容易出错
- delete之后重置指针值
- shared_ptr和new结合使用
- 不要混合使用普通指针和智能指针
- 也不要使用get初始化另一个智能指针或为智能指针赋值
- 其他shared_ptr操作
- 智能指针和异常
- 智能指针和哑类
- 使用我们自己的释放操作
- unique_ptr
- 传递unique_ptr参数和返回unique_ptr
- 向unique_ptr传递删除器
- weak_ptr
- 核查指针类
- 指针操作
12.1动态内存与智能指针
在C++中,动态内存的管理是通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
动态内存的使用很容易出问题,因为确保在正确的时间释放内存是极其困难的。有时我们会忘记释放内存,在这种情况下就会产生内存泄漏;有时在尚有指针引用内存的情况下我们就释放了它,在这种情况下就会产生引用非法内存的指针。
为了更容易(同时也更安全)地使用动态内存,新的标准库提供了两种智能指针(smart pointer)类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准库提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占“所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。
shared_ptr类
类似vector,智能指针也是模板。因此,当我们创建一个智能指针时,必须提供额外的信息一一指针可以指向的类型。与vector一样,我们在尖括号内给出类型,之后是所定义的这种智能指针的名字:
shared_ptr<string> p1; //shared_ptr,可以指向string
shared_ptr<list<int>> p2;//shared_ptr,可以指向int的list
默认初始化的智能指针中保存着一个空指针。
智能指针的使用方式与普通指针类似。解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空:
//如果p1不为空,检查它是否指向一个空string
if(pl&&pl->empty())
*pl="hi";//如果pl指向一个空string,解引用pl,将一个新值赋孙string
表12.1列出了shared_ptr和unique_ptr都支持的操作。只适用于shared_ptr的操作列于表12.2中。
表12.1:shared_ptr和unique_ptr都支持的操作
shared_ptrsp | 空智能指针,可以指向类型为0的对象 |
unique_ptrup | |
p | 将p用作一个条件判断,若p指向一个对象,则为true |
*p | 解引用p,获得它指向的对象 |
p->mem | 等价于(*p).mem |
p-get() | 返回p中保存的指针。要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了 |
swap(p,q) | 交换p和q中的指针 |
p.swap(q) |
表12.2:shared_ptr独有的操作
make_shared(args) | 返回一个shared_ptr,指向一个动态分配的类型为0的对象。使用args初始化此对象 |
shared_ptrp(q) | p是shared_ptr q的拷贝;此操作会递增q中的计数器。q中的指针必须能转换为T* |
p=q | p和a都是shared_ptr,所保存的指针必须能相互转换。此操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放 |
p.unique() | 若p.use_count()为1,返回true:枣则返回false |
p.use_count() | 返回与p共享对象的智能指针数量;可能很慢,主要用于调试 |
make_shared函数
最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。与智能指针一样,make_shared也定义在头文件memory中。
当要用make_shared时,必须指定想要创建的对象的类型。定义方式与模板类相同,在函数名之后跟一个尖括号,在其中给出类型:
//指向一个值为42的int的shared_ptr
shared_ptr<int>p3=make_shared<int>(42);
//p4指向一个值为“9999999999“的string
shared_ptr<string>p4=make_shared<string>(10,'9');
//P5指向一个值初始化的int,即,值为0
shared_ptr<int>p5=make_shared<int>();
类似顺序容器的emplace成员,make_shared用其参数来构造给定类型的对象。例如,调用make_shared时传递的参数必须与string的桅个构造函数相匹配,调用make_shared时传递的参数必须能用籼初始化一个int,依此类推。如果我们不传递任何参数,对象就会进行值初始化。
当然,我们通常用auto定义一个对象来保存make_shared的结果,这种方式较为简单:
//p6指向一个动态分配的空vector<string>
auto p6=make_shared<vector<string>>();
shared_ptr的拷贝和赋值
当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象:
auto p=make_shared<int>(42);//p指向的贞象只有p一个引用者
auto q(p);//p和a指向相同对象,此对象有两个引用者
我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数(reference count)。无论何时我们拷口一个shared_ptr,计数器都会递增。例如,当用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增。当我们给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域时,计数器就会递减。一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象:
auto r = make_shared<int>(42); // r指向的int只有一个引用者
r=q;//给r赋值,令它指向另一个地址
//通增q指向的对象的引用计数
//通减r原来指向的对象的引用计数
//r原来指向的对象已没有引用者,会自动释放
此例中我们分配了一个int,将其指针保存在r中。接下来,我们将一个新值赋予r。在此情况下,r是唯一指向此int的shared_ptr,在把q赋给r的过程中,此int被自动释放。
shared_ptr自动销毁所管理的对象
当指向一个对象的最后一个shared_ptr被销毁时,shared_ptz类会自动销毁此对象。它是通过另一个特殊的成员函数一一析构函数(destructor)完成销毁工作的。类似于构造函数,每个类都有一个析构函数。就像构造函数控制初始化一样,析构函数控制此类型的对象销毁时做什么操作。
析构函数一航用来释放对象所分配的资源。例如,string的构造函数(以及其他string成员)会分配内存来保存构成string的字符。string的析构函数就负责释放这些内存。类似的,vector的若干操作都会分配内存来保存其元素。vector的析构函数就负责销毁这些元素,并释放它们所占用的内存。
shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,
shared_ptr的析构函数就会销毁对象,并释放它占用的内存。
shared_ptr还会自动释放相关联的内存
当动态对象不再被使用时,shared_ptr类会自动释放动态对象,这一特性使得动态内存的使用变得非常容易。例如,我们可能有一个函数,它返回一个shared_ptr,指向一个Foo类型的动态分配的对象,对象是通过一个类型为0的参数迹行初始化的:
//factory返回一个shared_ptr,指向一个动态分配的对象
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也指向这块内存,它就不会被释放掉:
void use_factory(Targ)
{
shared_ptr<Foo>p=factory(arg);
//使用p
return p;//当我们返回p时,引用计数进行了通增操作
}//p离开了作用域,但它指向的内存不会被释放
在此版本中,use_factory中的return语句向此函数的调用者返回一个p的拷贝。拷贝一个shared_ptr会增加所管理对象的引用计数值。现在当p被销毁时,它所指向的内存还有其他使用者。对于一块内存,shared_ptr类保证只要有任何shared_ptr对象引用它,它就不会被释放掉。
由于在最后一个shared_ptr销毁前内存都不会释放,保证shared_ptr在无用之后不再保留就非常重要了。如果你忘记了销毁程序不再需要的shared_ptr,程序仍会正确执行,但会浪费内存。share_ptr在无用之后仍然保留的一种可能情况是,你将shared_ptr存放在一个容器中,随后重排了容器,从而不再需要某些元素。在这种情况下,你应该确保用erase删除那些不再需要的shared_ptr元素。
如果你将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。
使用了动态生存期的资源的类
程序使用动态内存出于以下三种原因之一:
- 程序不知道自己需要使用多少对象
- 程序不知道所需对象的准确类型
- 程序需要在多个对象间共享数据
容器类是出于第一种原因而使用动态内存的典型例子,我们将在第15章看到出于第二种原因而使用动态内存的例子。在本节中,我们将定义一个类,它使用动态内存是为了让多个对象能共享相同的底层数据。
到目前为止,我们使用过的类中,分配的资源都与对应对象生存期一致。例如,每个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仍然在使用它们。
使用动态内存的一个常见原因是允许多个对象共享相同的状.
定义StrBlob类
最终,我们会将Blob类实现为一个模板,但我们直到才会学习模板的相关知识。因此,现在我们先定义一个管理string的类,此版本命名为StrBLob。
实现一个新的集合类型的最简单方法是使用某个标准库容器来管理元素。采用这种方法,我们可以借助标准库类型来管理元素所使用的内存空间。在本例中,我们将使用vector来保存元素。
但是,我们不能在一个Blob对象内直接保存vector,因为一个对象的成员在对象销毁时也会被销毁。例如,假定b1和b2是两个Blob对象,共享相同的vector。如果此vector保存在其中一个Blob中一一例如b2中,那么当b2离开作用域时,此vector也将被销毁,也就是说其中的元素都将不复存在。为了保证vector中的元素继续存在,我们将vector保存在动态内存中。
为了实现我们所希望的数据共享,我们为每个StrBlob设置一个shared_ptr来管理动态分配的vector。此shared_ptr的成员将记录有多少个StrBlob共享相同的vector,存在vector的最后一个使用者被销毁时释放vector。
我们还需要确定这个类应该提供什么操作。当前,我们将实现一个vector操作的小的子集。我们会修改访问元素的操作(如front和back):在我们的类中,如果用户试图访问不存在的元素,这些操作会抛出一个异常。
我们的类有一个默认构造函数和一个构造函数,接受单一的initializer_list类型参数。此构造函数可以接受一个初始化器的花括号列表。
class StrBlob{
public:
typedef std::vector<std::string>::size_type size_type;
StrBLob();
StrBlob(std::initialtzer_list<std::strtng>i1);
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[]不合法,抛出一个异常
void check(size_type i,const std::string&msg)const;
};
在此类中,我们实现了size、empty和push_back成员。这些成员通过指向底层vector的aata成员来完成它们的工作。例如,对一个strBlob对象调用size()会调用data->size(),依此类推。
StrBlob构造函数
两个构造函数都使用初始化列表来初始化其data成员,令它指向一个动态分配的vector。默认构造函数分配一个空vector:
StrBLob::StrBLob():data(make_shared<vector<string>>()){}
StrBLob::StrBLob(initialtzer_;ist<string>i1):
data(make_shared<vector<string>>(i11){}
接受一个initializer_list的构造函数将其参数传递给对应的vector构造函数。此构造函数通过拷贝列表中的值来初始化vector的元素。元素访问成员函数pop_back、front和back操作访问vector中的元素。这些操作在试图访问元素之前必须检查元素是否存在。甾于这些成员函数需要做相同的检查操作,我们为StrBlob定义了一个名为cneck的private工具函数,它检查一个给定索引是否在合法范围内。
除了索引,check还接受一个string参数,它会将此参数传递给异常处理程序,这个string揩述了错误内容:
void StrBLob::check(size_type,const string&msg)const
{
if(0>=data->size())throw out_of_range(msg);
}
pop_back和元素访问成员函数首先调用check。如果check成功,这些成员函数继续利用底层vectozr的操作来完成自己的工作:
string& StrBLob::front()
{// 如果vector为空,check会抛出一个异常check(0,"front on empty StrBlob");return data->front();
}string&StrBLob::back()
{check(0,"back on empty StrBLob");returndata->back();
}void StrBLob::pop_back()
{check(0,"pop_back on empty StrBlob");data->pop_back();
}
front和back应该对const进行重载,这些版本的定义留作练习。
StrBlob的拷贝、赋值和销毁
类似Sales_data类,StrBlob使用默认版本的拷贝、赋值和销毁成员函数来对此类型的对象迹行这些操作。默认情况下,这些操作拷贝、赋值和销毁类的数据成员。我们的StrBLob类只有一个数据成员,它是shared_ptr类型。因此,当我们拷贝、赋值或销毁一个StrBlob对象时,它的shared_ptr成员会被拷贝、赋值或销毁。
如前所见,拷贝一个shared_ptr会递增其引用计数;将一个shared_ptr赋予另一个shared_ptr会递增赋值号右侧shared_ptr的引用计数,而递减左侧shared_ptr的引用计数。如果一个shared_ptr的引用计数变为0,它所指向的对象会被自动销毁。因此,对于由StrBlob构造函数分配的vector,当最后一个指向它的StrBlob对象被销毁时,它会随之被自动销毁。
直接管理内存
C++语言定义了两个运算符籼分配和释放动态内存。运算符new分配内存,delete释放new分配的内存。
相对于智能指针,使用这两个运算符管理内存非常容易出错,随着我们逐步详细介绍这两个运算符,这一点会更为清楚。而且,自己直接管理内存的类与使用智能指针的类不同,它们不能依赖类对象拷贝、赋值和销毁操作的任何默认定义。因此,使用智能指针的程序更容易编写和调试。
使用new动态分配和初始化对象
在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针:
int*pi=new int;//Pi指向一个动态分配的、札初始化的无名对象
此new表达式在自由空间构造一个int型对象,并返回指向该对象的指针。默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化:
string*ps=new string;//初始化为空string
int*pi = new int;//pi指向一个未初始化的int
我们可以使用直接初始化方式初始化一个动态分配的对象。我们可以使用传统的构造方式(使用圆括号),在新标准下,也可以使用列表初始化(使用花括号):
int*pi=new int(1024);//pi指向的对象的值为1024
string *ps=new string(10,“97);//*ps为“9999999999“
//vector有10个元素,值依次从0到9
vector<int>*pv=new vector<int>{0,1,2,3,4,5,6,7,8,9};
也可以对动态分配的对象进行值初始化,只需在类型名之后跟一对空括号即可:
string*psi=new string;//默认认初始化为空string
string*ps=new string();//值初始化为空string
int *pil=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*;依此类推。新分配的对象用obj的值进行初始化。
动态分配的const对象
用new分配const对象是合法的:
// 分配并初始化一个const int
const int*pci=new const int(1024);
//分配并默认初始化一个const的空string
const string*pcs=new const string;
类似其他任何const对象,一个动态分配的const对象必须迹行初始化。对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。由于分配的对象是const的,new返回的指针是一个指向const的指针。
内存耗尽
虽然现代计算机通常都配备大容量内存,但是自由空间被耗尽的情况还是有可能发生。一旦一个程序用光了它所有可用的内存,new表达式就会失败。默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。我们可以改变使用new的方式来阻止它抛出异常:
//如果分配失败,new返回一个空指针
int*pl=new int;//如果分配失败,new抛出std::bad_alloc
int*p2=new(nothrow)int;//如果分配失败,new返回一个空指针
我们称这种形式的new为定位new(placement new)。定位new表达式允许我们向new传递额外的参数。在此例中,我们传递给它一个由标准库定义的名为nothrow的对象。如果将nothrow传递给new,我们的意图是告诉它不能抛出异常。如果这种形式的new不能分配所需内存,它会返回一个空指针。bad_alloc和nothrow都定义在头文件new中。
释放动态内存
为了防止内存耗尽,在动态内存使用完毕后,必须将其归还给系统.我们通过delete表达式(delete expression)来将动态内存归还给系统。delete表达式接受一个指针,指向我们想要释放的对象:
delete p;// p必须指向一个动态分配的对象或是一个空指针
与new类型类似,delete表达式也执行两个动作:销毁给定的指针指向的对象;释放对应的内存。
指针值和delete
我们传递给delete的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的:
int i,*pi1= g*pi2 = nullptr;
double*pd=new double(33),*pd2=pd;
delete i;//错误:不是一个指针
delete pi1;//未定义:Pi1指向一个局部变量
delete pd;//正确
delete pd2;//未定义:pd2指向的内存已经被释放了
delete pi2;//正确:释放一个空指针总是没有错误的
对于delete i的请求,编译器会生成一个错误信息,因为它知道i不是一个指针。执行delete pi1和pd2所产生的错误则更具潜在危害:通常情况下,编译器不能分辨一个指针指向的是静态迦是动态分配的对象。类似的,编诙器也不能分辨一个指针所指向的内存是否已经被释放了。对于这些delete表达式,大多数编译器会编译通过,尽管它们是错误的。
虽然一个const对象的值不能被改变,但它本身是可以被销毁的。如同任何其他动态对象一样,想要释放一个const动态对象,只要delete指向它的指针即可:
const int*pci=new const int(1024);
delete pci;//正确:释放一个const对象
动态对象的生存期直到被释放时为止
由shared_ptr管理的内存在最后一个shared_ptr销毁时会被自动释放。但对于通过内置指针类型来管理的内存,就不是这样了。对于一个由内置指针管理的动态对象,直到被显式释放之前它都是存在的。
返回指向动态内存的指针(而不是智能指针)的函数给其调用者增加了一个额外负抱一一调用者必须记得释放内存:
//factory返回一个指针,指向一个动态分配的对象
Foo*factory(arg)
//视情况处理arg
return newFoo(arg);//调用者负责释放此内存
类似我们之前定义的factory函数,这个版本的factory分配一个对象,但并不delete它。factory的调用者负责在不需要此对象时释放它。不幸的是,调用者经常忘记释放对象:
void use_factory(arg)
{Foo*p=factory(arg);//使用p但不delete它
}//P离开了它的作用域,但它所指向的内存没有被释放!
此处,use_factory函数调用factory,后者分配一个类型为Foo的新对象。当use_factory返回时,局部变量p被销毁。此变量是一个内置指针,而不是一个智能指针。
与类类型不同,内置类型的对象被销毁时什么也不会发生。特别是,当一个指针离开其作用域时,它所指向的对象什么也不会发生。如果这个指针指向的是动态内存,那么内存将不会被自动释放。
由内置指针(而不是智能指针)管理的动态内存在被显式释放前一直都会存在。
在本例中,p是指向factory分配的内存的唯一指针。一旦use_factory返回,程序就没有办法释放这块内存了。根据整个程序的逻辑,修正这个错误的正确方法是在use_factory中记得释放内存:
void use_factory(T arg)
{
Foo *p=factory(arg);
//使用P
delete p;//现在记得释放内存,我们已经不需要它了
}
还有一种可能,我们的系统中的其他代码要使用use_factory所分配的对象,我们就应该修改此函数,让它返回一个指针,指向它分配的内存:
Foo* use_factory(arg)
{
Foo *p=factory(arg);
//使用p
return p;//调用者必须释放内存
}
动态内存管理非常容易出错
使用new和delete管理动态内存在三个常见问题:
- 忘记delete内存。忘记释放动态内存会导致人们常说的“内存泄露“问题,因为这种内存永远不可能被归还给自由空间了。查找内存泄露错误是非常困难的,因为通常应用程序运行很长时间后,真正耗尽内存时,才能检测到这种错误。
- 使用已经释放掉的对象。通过在释放内存后将指针置为空;有时可以检测出这种错误。
- 同一块内存释放两次。当有两个指针指向同一个动态分配对象时,可能发生这种错误。如果对其中一个指针进行了delete操作; 对象的内存就被归还给自由空间了。如果我们随后又delete第二个指针,自由空间就可能被破坏。
delete之后重置指针值
当我们delete一个指针后,指针值就变为无效了。虽然指针已经无效,但在很多机器上指针仍然保存着(已经释放了的)动态内存的地址。在delete之后,指针就变成了人们所说的空悬指针(dangling pointer),即,指向一块曾经保存数据对象但现在已经无效的内存的指针。未初始化指针的所有缺点空悬指针也都有。有一种方法可以避免宇悬指针的问题:在指针即将要离开其作用域之前释放掉它所关联的内存。这样,在指针关联的内存被释放掉之后,就没有机会继续使用指针了。如果我们需要保留指针,可以在delete之后将nullptr赋予指针,这样就清楚地指出指针不指向任何对象。
------这只是提供了有限的保护
动态内存的一个基本问题是可能有多个指针指向相同的内存。在delete内存之后重置指针的方法只对这个指针有效,对其他任何仍指向(已释放的)内存的指针是没有作用的。例如:
int*p(new int(42));//p指向动态内存
auto q=p;//p和q指向相同的内存
delete p;//p和口均变为无效
p=nullptr;//指出p不再绑定到任何对象
本例中p和q指向相同的动态分配的对象.我们delete此内存,然后将p置为nullptr,指出它不再指向任何对象。但是,重置p对q没有任何作用,在我们释放p所指向的(同时也是q所指向的1内存时,a也变为无效了。在实际系统中,查找指向相同内存的所有指针是异常阪难的。
shared_ptr和new结合使用
如前所述,如果我们不初始化一个智能指针,它就会被初始化为一个空指针。如表12.3所示,我们还可以用new返回的指针来初始化智能指针:
shared_ptr<double>p1;//shared_ptz可以指向一个double
shared_ptr<int>p2(new int(42));//P2指向一个值为42的int
接受指针参数的智能指针构造函数是explicit的。因此,我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:
shared_ptr<int>p1=new int(1024);//错误:必须使用直接初始化形式
shared_ptr<int>p2(new int(1024));//正确:使用了直接初始化形式
p1的初始化隐式地要求编详器用一个new返回的int*李创建一个shared_ptr。由于我们不能进行内置指针到智能指针间的隐式转换,因此这条初始化语句是错误的。出于相同的原因,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针:
shared_ptr<int>clone(int p){
return new int(p);//错误:隐式转换为shared_ptr<int>
}
我们必须将shared_ptr显式绑定到一个想要返回的指针上:
shared_ptr<int>clone(int p){
//正确:显式地用int*创建shared_ptr<int>
return shared_ptr<int>(new int(p));
}
默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。我们可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来替代delete。
表12.3:定义和改变shared_ptr的其他方法
shared_ptrp(q) | p管理内置指针q所指向的对象;q必须指向new分配的内存,且能够转换为0*类型 |
shared_ptrp(u) | p从unique_ptr u那里接管了对象的所有权:将u置为空 |
shared_ptrp(q,d) | p接管了内置指针q所指向的对象的所有权。a必须能转换为T*类型。p将使用可调用对象d来代替delete |
shared_ptrp(p2,d) | 如表12.2所示,p是shared_ptr p2的拷贝,唯一的区别是p将用可调用对象d来代替delete |
p.reset() | 若p是唯一指向其对象的shared_ptr,reset会释放此对 |
p->reset(q) | 象。若传递了可选的参数内置指针a,会令p指向q,否则会 |
p->reset(q,d) | 将p置为空。若还传递了参数d,将会调用d而不是delete来释放q |
不要混合使用普通指针和智能指针
shared_ptr可以协调对象的析构,但这仅限于其自身的拷贝(也是shared_ptr)之间。这也是为什么我们推荐使用make_shared而不是new的原因。这样,我们就能在分配对象的同时就将shared_ptr与之绑定,从而避免了无意中将同一块内存绑定多个独立创建的shared_ptr上。
考虑下面对shared_ptr进行操作的函数:
//在函数被调用时ptr被创建并初始化
void process(shared_ptr<int>ptr)
{
//使用ptr
}//ptr高开作用域,被销毁
process的参数是传值方式传递的,因此实参会被拷贝到ptr中.拷贝一个shared_ptr会递增其引用计数,因此,在process运行过程中,引用计数值至少为2。当process结束时,ptr的引用计数会递减,但不会变为0。因此,当局部变量ptr被销毁时,ptr指向的内存不会被释放。
使用此函数的正确方法是传递给它一个shared_ptr:
shared_ptr<int> p(new int(42));//引用计数为1
process(p);//指贝p会递增它的引用计数;在process中引用计数值为2
int i=*p;//正确:引用计数值为1
虽然不能传递给process一个内置指针,但可以传递给它一个(临时的shared_ptr,这个shared_ptr是用一个内置指针显式构造的。但是,这样做很可能会导致错误:
int*x(new int(1024));//危险:x是一个普通指针,不是一个智能指针
process(x);//错误:不能将int*转换为一个shared_ptr<int>
process(shared_ptr<int>(x));//合法的,但内存会被释放!
int j=*x;//未定义的:x是一个空悬指针!
在上面的调用中,我们将一个临时shared_ptr传递给process。当这个调用所在的表达式结束时,这个临时对象就被销毁了。销毁这个临时变量会递减引用计数,此时引用计数就变为0了。因此,当临时对象被销毁时,它所指向的内存会被释放。
但x继续指向(已经释放的)内存,从而变成一个空悬指针。如果试图使用x的值,其行为是未定义的。
当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。
也不要使用get初始化另一个智能指针或为智能指针赋值
智能指针类型定义了一个名为get的函数,它返回一个内置指针,指向智能指针管理的对象。此函数是为了这样一种情况而设计的:我们需要向不能使用智能指针的代码传递一个内置指针。使用get返回的指针的代码不能delete此指针。
虽然编详器不会给出错误信息,但将另一个智能指针也绑定到get返回的指针上是错误的:
shared_ptr<int> p(new int(42));//引用计数为1
int*q=p.get();//正确:但使用q时要注意,不要让它管理的指针被释放
{//新程序块//未定义:两个独立的shared_pt指向相同的内存shared_ptr<int>(d);
}//程序块结束,q被销毁,它指向的内存被释放
int foo=*p;//未定义:P指向的肉存已经被释放了
在本例中,p和q指向相同的内存。由于它们是相互独立创建的,因此各自的引用计数都是1。当q所在的程序块结束时,a被销毁,这会导致q指向的内存被释放。从而p变成一个穿悬指针,意味着当我们试图使用p时,将发生未定义的行为。而且,当p被销毁时,这块内存会被第二次delete。
get用来将指针的访问权限传通给代码,你只有在确定代码不会delete指针的情况下,才能使用get。特别是,永远不要用get初始化另一个只能指针或者给另一个一个智能指针赋值。
其他shared_ptr操作
shared_ptr还定义了其他一些操作。我们可以用reset来将一个新的指针赋予一个shared_ptr:
p=new int(1024);//错误:不能将一个指针赋予shared_ptr
p.reset(new int(1024))//正确:p指向一个新对象
与赋值类似,reset会更新引用计数,如果需要的话,会释放p指向的对象。reset成员经常与unique一起使用,来控制多个shared_ptr共享的对象。在改变底层对象之前,我们检查自己是否是当前对象仅有的用户。如果不是,在改变之前要制作一份新的拷贝:
if(!p.unidque())p.reset(new string(*p));//我们不是唯一用户;分配新的拷贝
*p+=newVal;//现在我们知道自己是唯一的用户,可以改变对象的值
智能指针和异常
程序需要确保在异常发生后资源能被正确地释放。一个简单的确保资源被释放的方法是使用智能指针。
如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放,:
void f()
{shared_ptr<int>sp(newint(42));//分配一个新对象//这段代码抛出一个异常,且在f中未被捕获
} //在函数结束时shared_ptr自动释放内存
函数的退出有两种可能,正常处理结束或者发生了异常,无论哪种情况,局部对象都会被销毁。在上面的程序中,sp是一个shared_ptr,因此sp销毁时会检查引用计数。在此例中,sp是指向这块内存的唯一指针,因此内存会被释放掉。
与之相对的,当发生异常时,我们直接管理的内存是不会自动释放的。如果使用内置指针管理内存,且在new之后在对应的delete之前发生了异常,则内存不会被释放:
void f()
{
int*ip=new int(42);//动态分配一个新对象//这段代码抛出一个异常,昆在f中未被捕获
delete ip;//在退出之前释放内存
)
如果在new和delete之间发生异常,且异常未在f中被捕获,则内存就永远不会被释放了。在函数5之外没有指针指向这块内存,因此就无法释放它了。
智能指针和哑类
包括所有标准库类在内的很多C++类都定义了析构函数,负责清理对象使用的资源。但是,不是所有的类都是这样良好定义的。特别是那些为C和C++两种语言设计的类,通常都要求用户显式地释放所使用的任何资源。
那些分配了资源,而又没有定义析构函数来释放这些资源的类,可能会遇到与使用动态内存相同的错误一一程序员非常容易忘记释放资源。类似的,如果在资源分配和释放之间发生了异常,程序也会发生资源泄漏。
与管理动态内存类似,我们通常可以使用类似的技术来管理不具有良好定义的析构函数的类。例如,假定我们正在使用一个C和C++都使用的网络库,使用这个库的代码可能是这样的:
struct destination;//表示我们正在连接什么
struct connection;//使用连接所需的信息
connection connect(destination*);//打开连接
void disconnect(connection);//关闭给定的连接
void(destination&d/*其他参数*/)
{
//获得一个连接;记住使用完后要关闭它
connectionc=connect(&d);
//使用连捷
//如果我们在E退出前志记调用disconnect,就无法关闭c了
}
如果connection有一个析构函数,就可以在f结束时由析构函数自动关闭连接.但是,connection没有析构函数。这个问题与我们上一个程序中使用shared_ptr避免内存泄漏几乎是等价的。使用shared_ptr来保证connection被正确关闭,已被证明是一种有效的方法。
使用我们自己的释放操作
默认情况下,shared_ptr假定它们指向的是动态内存。因此,当一个shared_ptr被销毁时,它默认地对它管理的指针进行delete操作。为了用shared_ptr来算理一个connection,我们必须首先定义一个函数来代替delete。这个删除器(deleter)函数必须能够完成对shared_ptr中保存的指针进行释放的操作。在本例中,我们的删除器必须接受单个类型为connection*的参数:
void end_connection(connection*p){disconnect(*p);
当我们创建一个shared_ptr时,可以传递一个(可选的)指向删除器函数的参数:
void f(destination &d/*其他参数*/)
{
connection c=connect(&d);
shared_ptr<connectton>p(&c,end_connection);
//使用连接
//当f退出时(即使是由于异常而退出),connection会被正确关闭
}
当p被销毁时,它不会对自己保存的指针执行delete,而是调用end_connection。接下来,end_connection会调用adisconnect,从而确保连接被关闭。如果f正常退出,那么p的销毁会作为结柬处理的一部分。如果发生了异常,p同样会被销毁,从而连接被关闭。
unique_ptr
一个unique_ptr“拥有“它所指向的对象。与shared_ptr不同,某个时刻只能有一个uniqueptz指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。表12.4列出了unique_ptr特有的操作。与shared_ptr相同的操作列在表。
与shared_ptr不同,没有类似make_shared的标准库函数返回一个unique_ptr。当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。类似shared_ptr,初始化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(“Stegosaurus“));
unique_ptr<string>p2(p1);//错误:unique_ptr不支持拷贝
unique_ptr<string>p3;
p3=p2;//错误:unique_ptr不支持赋值
表12.4:unique_ptr操作
unique_ptr u1 | 空unique_ptr,可以指向类型为的对象,u1会使用delete |
unique_ptr<T,D> u2 | 来释放它的指针;u2会使用一个类型为D的可调用对象来释放它的指针 |
unique_ptr<7,D> u(d) | “空unique_ptr,指向类型为7的对象,用类型为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:
//将所有权从p1(指向stringStegosaurus)转移给p2
unique_ptr<string>p2(pl.release());//release将p1置为空
unique_ptr<string>P3(new string("Trex"));
//将所有权从p3转移给p2
p2.reset(p3.release());//reset释放了p2原来指向的内存
release成员返回unique_ptr当前保存的指针并将其置为空。因此,p2被初始化为p1原来保存的指针,而p1被置为空。
reset成员接受一个可选的指针参数,令unique_ptr重新指向给定的指针。如果unique_ptr不为空,它原籼指向的对象被释放。因此,对p2调用reset释放了用“Stegosaurus“初始化的string所使用的内存,将p3对指针的所有权转移给p2,并将p3置为空。
调用release会切断unique_ptr和它原来管理的对象间的联系。release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。在本例中,管理内存的责任简单地从一个智能指针转移给另一个。但是,如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放:
p2.release();//错误:p2不会释放内存,而且我们丢失了指针
auto p=p2.release()}//正确,但我们必须记得delete(p)
传递unique_ptr参数和返回unique_ptr
不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的uniaque_ptr。最常见的例子是从函数返回一个unique_ptr:
unique_ptr<int>clone(intp){
//正确:从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。
向unique_ptr传递删除器
类似shared_ptr,unique_ptr默认情况下用delete释放它指向的对象。与shared_ptr一样,我们可以重载一个unique_ptr中默认的删除器。但是,unique_ptr管理删除器的方式与shared_ptr不同。
重载一个unique_ptr中的删除器会影响到unique_ptr类型以及如何构造(或reset)该类型的对象。与重载关联容器的比较操作类似,我们必须在尖括号中unique_ptr指向类型之后提供删除器类型。在创建或reset一个这种unique_ptr类型的对象时,必须提供一个指定类型的可调用对象(删除器):
// p指向一个类型为objT的对象,并使用一个类型为de1m的对象释放objT对象
// 它会调用一个名为fcn的delT类型对象
unique_ptr<objT,delT> p(new objT,fcn);
作为一个更具体的例子,我们将重写连接程序,用unique_ptr来代替shared_ptr,如下所示:
void(destination& d)/*其他需要的参数*/)
{connection c=connect(&d);//打开连接//当p被销毁时,连接将会关闭unique_ptr<connection,decltype(end_connection)->p(&c,end_connection);//使用连接//当f退出时(即使是由于异常而退出),connection会被正确关闭
}
在本例中我们使用了decltype来指明函数指针类型。由于decltype(end_connection)返回一个函数类型,所以我们必须添加一个*来指出我们正在使用该类型的一个指针。
weak_ptr
weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptz的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放,因此,weak_ptz的名字抓住了这种智能指针“弱“共享对象的特点。
表12.5:weak_ptr
weak_ptr w | 空weak_ptr可以指向类型为的对象 |
weak_tpr w(sp) | 与shared_ptr sp指向相同对象的weak_ptr。0必须能转换为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<int>(42);
weak_ptr<int>wp(p);//wp弱共享p;p的引用计数未改变
本例中wp和p指向相同的对象。由于是弱共享,创建wp不会改变p的引用计数;wp指向的对象可能被释放掉。
由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock。此函数检查weak_ptr指向的对象是否仍存在。如果存在,lock返回一个指向共享对象的shared_ptr。与任何其他shared_ptr类似,只要此shared_ptr存在,它所指向的底层对象也就会一直存在。例如:
if(shared_ptr<int> np = wp.lock()){//如果np不为空则条件成立
//在if中,np与p共享对象
}
在这段代码中,只有当lock调用返回true时我们才会进入if语句体。在if中,使用np访问共享对象是安全的。
核查指针类
作为weak_ptr用途的一个展示,我们将为strBlob类定义一个伴随指针类。我们的指针类将命名为StrBLobPtr,会保存一个weak_ptr,指向StrBLob的data成员,这是初始化时提供给它的。通过使用weak_ptr,不会影响一个给定的StrBlob所指向的vector的生存期。但是,可以阻止用户访问一个不再存在的vector的企图。
StrBlobPtr会有两个数据成员:wptr,或者为空,或者指向一个StrBLob中的vector; icurr,保存当前对象所表示的元素的下标。类似它的伴随类StrBLob,我们的指针类也有一个check成员来检查解引用StrBlobPtr是否安全:
//对于访问一个不存在元素的尝试,StrBLobPtr抛出一个异常
class StrBlobPtr{
public:
StrBlobPtr():curr(0){
StrBLobPtr(StrBLob &a,size_t sz=0):wptr(a.data),curr(sz){}
std::string & deref()const;
StrBlobPtr & incr();//前缀递增
private:
//若检查成功,check返回一个指向vector的shared_ptr
std::shared_ptr<std::vector<std::string>>check(std::size_t,const std::string&)const;
//保存一个weak_ptr,意味着底层vector可能会被销毁
std::weak_ptr<std::vector<std::string>>wptr;
std::size_t curr;//在数组中的当前位置
};默认构造函数生成一个空的StrBlobPtr。其构造函数初始化列表将curr显式初始化为0,并将wptr隐式初始化为一个空weak_ptr。第二个构造函数接受一个StrBlob引用和一个可选的索引值。此构造函数初始化wptr,令其指向给定StrBLob对象的shared_ptr中的vector,并将curr初始化为sz的值。我们使用了默认参数,表示默认情况下将curr初始化为第一个元素的下标。我们将会看到,StrBlob的end成员将会用到参数sz。值得注意的是,我们不能将StrBlobPtr绑定到一个const StrBLob对象。这个限制是由于构造函数接受一个非const StrBlob对象的引用而导致的。StrBlobPtr的check成员与StrBlob中的同名成员不同,它还要检查指针指向```cpp
std::shared_ptr<std::vector<std::string>>
StrBlobPtr::check(std::size_t i,const std::string &msg)const
{
auto ret=wptr.lock();//vector还存在吗?
if(!ret)throw std::runtime_error("unbound StrBLobPtr");
if(0>=ret->size())throw std::out_of_range(msg);
return ret;//否则,返回指向vector的shared_ptr
}
由于一个weak_ptr不参与其对应的shared_ptr的引用计数,StrBlobPtr指向的vector可能已经被释放了。如果vector已销毁,lock将返回一个空指针。在本例中,任何vector的引用都会失败,于是抛出一个异常。否则,check会检查给定索引,如果索引值合法,check返回从lock获得的shared_ptr。
指针操作
我们将定义名为deref和incr的函数,分别用来解引用和递增StrBLobPtr。
deref成员调用check,检查使用vector是否安全以及curr是否在合法范围内:
std::string&StrBlobPtr::deref()const
{
auto p=check(curr,"dereference past end");
return(*p)[curr];//(*p)是对象所指向的vector
}
如果cneck成功,p就是一个shared_ptr,指向StrBlobPtzr所指向的vector。表达式(*p)[curr]解引用shared_ptr来获得vector,然后使用下标运算符提取并返回curr位置上的元素。
incr成员也调用check:
//前缀递增:返回递增后的对象的引用
StrBLobPtr&_StrBLobPtr::incz()
{
//如果curz已经指向容器的尾后位置,就不能递增它
check(curr,"increment past end of StrBlobPtr");
++curr;//推进当前位置
return *this;
}
当然,为了访问data成员,我们的指针类必须声明为StrBlob的friend。我们还要为StrBlob类定义begin和end操作,返回一个指向它自身的StrBLobPtr:
//对于StrBlob中的友元声明来说,此前置声明是必要的
class StrBlobPtr;
class StrBlob{
friend class StrBlobPtr;//其他成员返回指向首元素和尾后元素的StrBlobPtr
StrBlobPtr begin(){return StrBlobPtr(*this);};
StrBlobPtr end()
{auto ret=StrBlobPtr(*this,data->size());return ret;
}