C++智能指针

直接管理内存

什么时候需要直接管理

简而言之,当内存分配在栈上时,不需要直接管理,而当内存分配在堆上时则需要手动回收,或者等到堆上内存分配满了触发了自动回收机制。
关于堆和栈,这篇文章讲得浅显易懂:http://blog.csdn.net/hairetz/article/details/4141043
一个由C/C++编译的程序占用的内存分为以下几个部分

  1. 栈区(stack)—— 由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  2. 堆区(heap)—— 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
  3. 全局区(静态区)(static)——全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
  4. 文字常量区——常量字符串就是放在这里的,程序结束后由系统释放 。
  5. 程序代码区——存放函数体的二进制代码。

例子程序

这是一个前辈写的,非常详细

//main.cpp    
int   a   =   0;   全局初始化区    
char   *p1;   全局未初始化区    
main() { 
int   b;   栈    
char   s[]   =   "abc";   栈    
char   *p2;   栈    
char   *p3   =   "123456";   123456/0在常量区,p3在栈上。    
static   int   c   =0;   全局(静态)初始化区    
p1   =   (char   *)malloc(10);    
p2   =   (char   *)malloc(20);    
分配得来得1020字节的区域就在堆区。    
strcpy(p1,   "123456");   123456/0放在常量区,编译器可能会将它与p3所指向的"123456" 优化成一个地方。    
}

注意,除了上文的malloc,new分配的内存也在堆中需要手动销毁。

动态内存

由上文看出,分配在堆上的内存需要手动进行动态分配和释放,我们将之称为动态内存。C++中,动态内存是通过new和delete来进行分配和释放的。
new:在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化。
delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针。

int *pi=new int;        //pi指向一个动态分配的,未初始化的无名对象

可以是使用直接初始化方式来初始化一个动态分配的对象。

int *pi=new int(1024);
string *ps=new string(10,’9’);

也可以对动态分配的对象进行值初始化,只需在类型名之后跟一对空括号即可:

string *ps1=new string;         //默认初始化为空string
string *ps=new string();            //值初始化为空string

动态分配const对象
类似于其他任何const对象,一个动态分配的const对象必须进行初始化。对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。

内存耗尽

一旦一个程序用光了它所有可用的内存,new表达式就会失败。默认情况下,如果new不能分配所要求的内存空间,他会抛出一个类型为bad_alloc的异常。我们可以改变使用new的方式来阻止他抛出异常:

int *p1=new int;            //如果分配失败,new抛出std::bad_alloc
int *p2=new (nothrow) int   //如果分配失败,new返回一个空指针

释放动态内存

delete p;                   //p必须指向一个动态分配的对象或是一个空指针

释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的。

int i,*pil=&i,*pi2=nullptr;
double *pd=new double(33),*pd2=pd;
delete i;               //错误:i不是一个指针
delete pil;         //未定义:pil指向一个局部变量
delete pd;          //正确
delete pd2;         //未定义:pd2指向的内存已经被释放了
delete pi2;         //正确:释放一个空指针总是没有错误的

对于通过内置指针类型来管理的动态对象,直到被显式释放之前他都是存在的。
在delete之后,指针就变成了空悬指针,即,指向一块曾经保存数据对象但现在已经无效的内存的指针。
如果我们需要保留指针,可以在delete之后将nullptr赋予指针,这样就清除地指出指针不指向任何对象。

智能指针

关于智能指针,这一篇讲的不错:http://blog.csdn.net/xt_xiaotian/article/details/5714477
智能指针与常规指针的重要区别是它负责自动释放所指向的对象,两种智能指针的区别在于管理底层指针的方式:
shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。标准库还定义了一个名为week_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。

shared_ptr和unique_ptr都支持的操作

操作 说明
shared_ptr sp , unique_ptr up 空智能指针,可以指向类型为T的对象
p 将p用作一个条件判断,若p指向一个对象,则为true
*p 解引用p,获得它指向的对象
p->mem 等价于(*p).mem
p.get() 返回p中保存的指针。要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了
swap(p,q) ,p.swap(q) 交换p和q中的指针

shared_ptr类

创建一个智能指针时,必须提供额外的信息——指针可以指向的类型。

shared_ptr<string> p1;
shared_ptr<list<int>> p2;

shared_ptr独有的操作

操作 说明
make_shared(args) 返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化此对象
shared_ptrp(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。

shared_ptr<int> p3=make_shared<int>(42);
auto p6=make_shared<vector<string>>();

shared_ptr的拷贝和赋值
当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象。
shared_ptr自动销毁所管理的对象
shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存。
shared_ptr还会自动释放相关联的内存
return会对shared_ptr指针的引用次数进行递增操作。
使用了动态生存期的资源的类
程序使用动态内存出于以下三种原因之一:
1. 程序不知道自己需要使用多少对象
2. 程序不知道所需对象的准确类型
3. 程序需要在多个对象间共享数据

shared_ptr和new结合使用

接受指针参数的智能指针构造函数是explicit的,我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:

shared_ptr<int> p1=new int(1024);       //错误
shared_ptr<int> p2(new int(1024));      //正确
shared_ptr<int> clone(int p){
    return new int(p);                  //错误
}
shared_ptr<int> clone(int p){
    return shared_ptr<int>(new int(p)); //正确
}

定义和改变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,reset(q)p,reset(q,d) 若p是唯一指向其对象的shared_ptr,reset会释放此对象。若传递了可选的参数内置指针q,会令p指向q,否则将p置为空。若还传递了参数d,将会调用d而不是delete来释放q

不要混合使用普通指针和智能指针

void process(shared_ptr<int> ptr){}

process的参数是传值方式传递的,因此实参会被拷贝到ptr中。拷贝一个shared_ptr会递增其引用计数,因此,在process运行过程中,引用计数值至少为2.当process结束时,ptr的引用计数会递减,但不会变为0.因此当局部变量ptr被销毁时,ptr指向的内存不会被释放。
正确方式是传递给它一个shared_ptr:

shared_ptr<int> p(new int(42));     //引用计数为1
process(p);                     //

虽然不能传递给process一个内置指针,但可以传递给它一个(临时的)shared_ptr,这个shared_ptr是用一个内置指针显式构造的。但是,这样做很可能会导致错误:

int *x(new int(1024));              //危险:x是一个普通指针,不是一个智能指针
process(x);                     //错误
process(shared_ptr<int>(x));        //合法的,但内存会被释放
int j=*x;                           //未定义的:x是一个空悬指针

不要使用get初始化另一个智能指针或为智能指针赋值
智能指针类型顶一个了一个名为get的函数,它返回一个内置指针,指向智能指针管理的对象。此函数是为了这样一种情况儿设计的:我们需要向不能使用智能指针的代码传递一个内置指针。使用get返回的指针的代码不能delete此指针。

shared_ptr<int> p(new int(42)); //引用计数为1
int *q=p.get();             //正确:但使用q时要注意,不要让它管理的指针被释放
{
//未定义:两个独立的shared_ptr指向相同的内存
    shared_ptr<int>(q);
}//程序块结束,q被销毁,它指向的内存被释放
int foo=*p;                 //未定义:p指向的内存已经被释放了

智能指针和异常

如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放。
智能指针和哑类
使用我们自己的释放操作
为了正确使用智能指针,我们必须坚持一些基本规范:

  • 不适用相同的内置指针初始化(或reset)多个智能指针。
  • 不delete get()返回的指针。
  • 不使用get()初始化或reset另一个智能指针。
  • 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。
  • 如果你用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。

unique_ptr

一个unique_ptr“拥有”它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_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操作

操作 说明
unique_ptr u1,unique_ptr u2 空unique_ptr,可以指向类型为T的对象,u1会使用delete来释放它的指针;u2会使用一个类型为b的可调用对象来释放它的指针
unique_ptr u(d) 空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete
u=nullptr 释放u指向的对象,将u置为空
u.release() u放弃对指针的控制权,返回指针,并将u置为空
u.reset() 释放u指向的对象
u.reset(q) ,u.reset(nullptr) 如果提供了内置指针q,令u指向这个对象;否则将u置为空
//将所有权从p1转移给p2
unique_ptr<string> p2(p1.release());        //release将p1置为空
unique_ptr<string> p3(new string(“Trex”));
//将所有权从p3转移给p2
p2.reset(p3.release());;                    //reset释放了p2原来指向的内存

传递unique_ptr参数和返回unique_ptr
不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptr。
向unique_ptr传递删除器

weak_ptr

weak_ptr指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_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

动态数组

new和delete运算符一次分配/释放一个对象,但某些应用需要一次为很多对象分配内存的功能。例如,vector和string都是在连续内存中保存它们的元素,因此,当容器需要重新分配内存时,必须一次性为很多元素分配内存。
为了支持这种需求,C++语言和标准库提供了两种一次分配一个对象数组的方法:C++语言定义了动态数组的new方式;标准库中包含了一个名为allocator的类。

new和数组

int *pia=new int[get_size()];       //pia指向第一个int

分配一个数组会得到一个元素类型的指针
虽然我们通常称new T[]分配的内存为“动态数组”,但我们用new分配一个数组时,并未得到一个数组类型的对象,而是一个数组元素类型的指针。
由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin或end。这些函数使用数组维度来返回指向首元素和尾后元素的指针。出于相同的原因,也不能用范围for语句来处理动态数组中的元素。
初始化动态分配对象的数组
可以对数组中的元素进行值初始化,方法是在大笑之后跟一对空括号:

int *pia=new int[10];           //10个未初始化的int
int *pia2=new int[10]();            //10个值初始化为0的int

动态分配一个空数组是合法的
虽然我们不能创建一个大小为0的静态数组对象,但当n等于0时,调用new[n]是合法的:

char arr[0];                //错误
char *cp=new char[0];       //正确

释放动态数组
为了释放动态数组,我们使用一种特殊形式的delete——在指针前加上一个空方括号对:

delete p;               //p必须指向一个动态分配的对象或为空
delete [] pa;           //pa必须指向一个动态分配的数组或为空

数组中的元素按逆序被销毁。
智能指针和动态数组
为了用一个unique_ptr管理动态数组,我们必须在对象类型后面跟一对空方括号:

//up指向一个包含10个未初始化int的数组
unique_ptr<int[]> up(new int[10]);
up.release();                       //自动用delete[]销毁其指针

指向数组的unique_ptr
指向数组的unique_ptr不支持成员访问运算符(点和箭头运算符)
其他unique_ptr操作不便

操作 说明
unique_ptr u u可以指向一个动态分配的数组,数组元素类型为T
unique_ptr u(p) u指向内置指针p所指向的动态分配的数组。p必须能转换为类型T*
u[i] 返回u拥有的数组中的位置i处的对象,u必须指向一个数组

allocator类

allocator类

标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。
类似vector,allocator是一个模板。为了定义一个allocator对象,我们必须指明这个allocator可以分配的对象类型。当一个allocator对象分配内存时,他会根据给定的对象类型来确定恰当的内存大小和对齐位置:

allocator<string> alloc;                //可以分配string的allocator对象
auto const p=alloc.allocate(n);     //分配n个未初始化的string

标准库allocator类及其算法

操作 说明
allocator a 定义了一个名为a的allocator对象,它可以为类型为T的对象分配内存
a.allocate(n) 分配一段原始的、为构造的内存,保存n个类型为T的对象
a.deallocate(p,n) 释放从T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象;p必须是一个先前由allocate返回的指针,且n必须是p创建时所要求的大小。在调用deallocate之前,用户必须对每个在这块内存中创建的对象调用destroy
a.construct(p,args) p必须是一个类型为T*的指针,指向一块原始内存;arg被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象
a.destroy(p) p为T*类型的指针,此算法对p指向的对象执行西沟函数

allocator分配为构造的内存
allocator分配的内存是未构造的,我们按需要在此内存中构造对象。

auto q=p;                   //q指向最后构造的元素之后的位置
alloc.construct(q++);           //*q为空字符串
alloc.construct(q++,10,’c’);    //*q为cccccccccc
alloc.construct(q++,”hi”);      //*q位hi!

为了使用allocate返回的内存,我们必须用construct构造对象。使用为构造的内存,其行为是未定义的。
我们只能对真正构造了的元素进行destroy操作
拷贝和填充未初始化内存的算法
它们都定义在头文件memory中

allocator算法

这些函数在给定目的位置创建元素,而不是由系统分配内存给它们。

操作 说明
uninitialized_copy(b,e,b2) 从迭代器b和e指出的输入范围中拷贝元素到迭代器b2指定的为构造的原始内存中。b2指向的内存必须足够大,能容纳输入序列中元素的拷贝
uninitialized_copy(b,n,b2) 从迭代器b指向的元素开始,拷贝n个元素到b2开始的内存中
uninitialized_fill(b,e,t) 在迭代器b和e指定的原始内存范围中创建对象,对象的值均为t的拷贝
uninitialized_fill_n(b,n,t) 从迭代器b指向的内存地址开始创建n个对象。b必须指向足够大的为构造的原始内存,能够容纳给定数量的对象

转载请注明出处:http://blog.csdn.net/ylbs110/article/details/51049586

你可能感兴趣的:(C++)