动态内存和智能指针

由编译器自动分配的内存都有着严格的生存期。全局对象在程序启动时分配,在程序结束时销毁。对于局部自动对象,当我们进入其定义所在的程序块时被创建,在离开块时销毁。局部static对象在第一次使用前分配,在程序结束时销毁。

除了自动和static对象外,C++还支持动态分配对象。动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时,这些对象才会销毁。

 

静态内存用来保存局部static对象,类static数据成员以及定义在任何函数之外的变量。栈内存用来保存定义在函数内的非static对象。分配在静态或栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在其定义的程序块运行时才存在,static对象在使用之前分配,在程序结束时销毁。

除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作自由空间或堆。程序用堆来存储动态分配的对象——即,那些在程序运行时分配的对象。动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,我们的代码必须显式地销毁它们。

动态内存和智能指针

在C++中,动态内存的管理是通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针。我们可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。

动态内存的使用很容易出问题,因为确保在正确的时间释放内存是及其困难的。有时我们会忘记释放内存,在这种情况下就会产生内存泄漏;有时在尚有指针引用内存的情况下我们就释放它了,在这种情况下就会产生引用非法内存的指针。

为了更容易(同时也安全)地使用动态内存,新的标准库提供了两种智能指针类型来管理动态对象。只能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准库提供的这两种只能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一对象;unique_ptr则“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。

shared_ptr类

类似vector,只能指针也是模板。因此,当我们创建一个只能指针时,必须提供额外的信息——指针可以指向的类型。与vector一样,我们在尖括号内给出类型,之后是所定义的这种指针的名字:

shared_ptr<string> p1 ; //shared_ptr,可以指向string

shared_ptr<list<int> > p2;  //shared_ptr,可以指向int的list

默认初始化的智能指针中保存着一个空指针。

只能指针的使用方式与普通指针类似。解引用一个智能指针返回它所指的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空:

//如果p1不为空,检查它是否指向一个空string

if(p1&&p1->empty())

  *p1="hi";

下表列出了shared_ptr和unique_ptr都支持的操作。只适用shared_ptr的操作列入下面。

shared_ptr和unique_ptr都支持的操作

shared_ptr<T> sp     空智能指针,可以指向类型为T的对象

unique_ptr<T> up    

p          将p用作一个条件判断,若p指向一个对象,则为true

*p        解引用p,获得它指向的对象

p->mem     等价于(*p).mem

p.get()      返回p中保存的指针,要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了

swap(p,q)     交换p和q中的指针

p.swap(q) 

 

shared_ptr独有的操作

make_shared<T>(args)     返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化此对象

shared_ptr<T> p(q)       p是shared_ptr q的一个拷贝,此操作会递增q中的计数器。q中的指针必须都能转换为T*

p=q              p和q都是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指向一个值为"99999"的string
shared_ptr<string> p4=make_shared<string> (5,'9');

//p5指向一个值初始化的int,即,值为0
shared_ptr<int> p5=make_shared<int> ();

类似顺序容器的emplace成员,make_shared用其参数来构造给定类型的对象。例如,调用make_shared<string>时传递的参数必须与string的某个构造函数相匹配,调用make_shared<int> 时传递的参数必须能用来初始化一个int,依次类推。如果我们不传递任何参数,对象就会进行值初始化。

当然,我们通常用auto定义一个对象来保存make_shared的结果,这种方式较简单:

//p6指向一个动态分配的空vector<string>

auto p6=make_shared<vector<string>> ();

 

shared_ptr的拷贝和赋值

当进行拷贝或赋值时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象。

auto p=make_shard<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指向的int只有一个引用者

r=q;   //给r赋值,令它指向另一个地址,递增q指向的对象的引用计数,递减r原来指向的对象的引用计数,r原来指向的对象已没有引用者,会自动释放

此例中我们分配了一个int,将其指针保存在r中。接下来,我们将一个新值赋予r。在此情况下,r是唯一执行此int的shared_ptr,在把q赋给r的过程中,此int白自动释放。

shared_ptr自动销毁所管理的对象

当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象。它是通过另一个特殊的成员函数——析构函数完成销毁工作的。类似与构造函数,每个类都有一个析构函数。就像构造函数控制初始化一样,析构函数控制此类型的对象销毁时做什么操作。

析构函数一般用来释放对象分配的资源。例如,string的构造函数(以及其他string成员)会分配内存来保存构成string的字符。string的析构函数就负责释放这些内存。类似的,vector的若干操作都会分配内存来保存其元素。vector的析构函数就负责销毁这些元素,并释放它们所占用的内存。

shared_ptr的析构函数会递减它所指对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它所占用的内存。

shared_ptr还会自动释放相关联的内存 

当动态对象不再被使用时,shared_ptr类会自动释放动态对象,这一特性使得动态内存的使用变得非常容易。例如,我们可能有一个函数,它返回一个shared_ptr,指向一个Foo类型的动态分配的对象,对象是通过一个类型为T的参数进行初始化的:

//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(T arg)
{
    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,程序仍会正确执行,但会浪费内存。shared_ptr在无用之后仍然保留的一种可能情况是,你将shared_ptr存放在一个容器中,随后重排了容器,从而不再需要某种元素。在这种情况西下,你应该确保erase删除哪些不再需要的shared_ptr元素。

注意:如果你将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。

使用了动态生存期的资源的类

程序使用动态内存处于以下三种原因之一:

1 程序不知道自己需要使用多少个对象;

2 程序不知到所需的准确类型

3 程序需要在多个对象间共享数据

容器类是处于第一种原因而使用动态内存的典型例子。

使用动态内存的一个常见原因是运行多个对象共享相同的状态。

 

posted @ 2014-10-27 10:58  Jessica程序猿  阅读(1342)  评论(0编辑  收藏  举报