C++_动态内存与智能指针知识点总结

C++动态内存与智能指针

     为了更容易同时也更安全地使用动态内存,新的标准库提供了两种智能指针(smart pointer)类型来管理对象。智能指针的行为类似于常规指针,重要的区别是它负责自动释放内存。

      新标准库提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory文件中。

shared_ptr和unique_ptr都支持的操作

shared_ptr<T> sp

unique_ptr<T> 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独有的操作

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,则将其管理的原内存释放。

     我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数(reference count),无论何时我们拷贝一个shared_ptr,计数器都会递增。例如,当用一个shared_ptr初始化另一个shared_ptr,或者将它作为参数传递给一个函数,以及作为函数的返回值时,它所关联的计数器就会递增。

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

 

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

(1).程序不知道自己要使用多少对象;

(2).程序不知道所需对象的实际类型;

(3).程序需要在多个对象间共享数据。

 

直接管理内存

      C++语言定义了两个运算符来分配和释放动态内存。运算符new分配内存,delete释放new分配的内存。

Best Practices:使用智能指针来管理内存,否则不要分配动态内存。

1.   使用new动态分配和初始化对象

在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针:

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

       默认初始化:默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化。

       直接初始化:我们可以使用传统的构造方式(使用圆括号),在新标准下,也可以使用列表初始化(使用花括号):

int *pi = newint(1024);

string *ps = newstring(10, ‘0’);

类似于其他任何const对象,一个动态分配的const对象必须进行初始化。由于分配的对象是const的,new返回的指针是一个指向const的指针。

const int *pci =new const int(1024);

const string *pcs= new const string;

       内存耗尽:默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。我们可以改变使用new的方式来阻止它抛出异常:

       int *pi = new int; //如果分配失败,new会抛出std::alloc异常

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

       bad_alloc和nothrow都定义在头文件new中。

2.   释放动态内存

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

Warning:由内置指针(而不是智能指针)管理的动态内存在被显式释放前一直都会存在。

使用new和delete管理内存存在三个常见问题:

(1).忘记delete内存;

(2).使用已经释放的对象;

(3).同一内存释放两次。

定义和改变shared_ptr的其他方法

shared_ptr<T> p(q)

p管理内置指针q所指向的对象;q必须指向new分配的内存,且能够转换为T*类型。

shared_ptr<T> p(u)

p从unique_ptr u那里接管了对象的所有权;将u置为空。

shared_ptr<T> p(q, d)

p接管内置指针q所指向的对象的所有权。q必须能转换为T*类型。p将使用可调用对象d来代替delete。

shared_ptr<T> 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 F()

{

      shared_ptr<int>sp( new int(42) );

      //这段代码抛出异常,且在F中未被捕获

      //在函数结束时,shared_ptr自动释放内存

}

函数退出有两个可能,正常处理结束或者发生了异常,无论发生了哪种情况,局部对象都会被销毁。在上面的程序中,sp是一个shared_ptr,因此sp销毁时会检查引用计数。在此例中,sp是指向这块内存的唯一指针,因此内存会被释放。

void F()

{

      int *ip = new int(42);  //动态分配一个新对象

      //这段代码抛出一个异常,且在f中未被捕获

      delete ip; //在退出之前释放内存

}

如果在new和delete之间发生异常,且异常未在F中被捕获,则内存就永远不会被释放了。在函数F之外没有指向这块内存,因此就无法释放了。

 

unique_ptr

     一个unique_ptr拥有它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。

      由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作:

unique_ptr<string>p(new string(“maple”));

unique_ptr<string>p2(p);   //错误,unique_ptr不支持拷贝

unique_ptr<string>p3;

p3 = p2;  //错误,unique_ptr不支持赋值

unique_ptr操作

unique_ptr<T> u1;

空unique_ptr,可以指向类型为T的对象。u1会使用delete释放它的指针

unique_ptr<T, D> u2

空unique_ptr,u2会用一个类型为D的可调用对象来释放它的指针。

unique_ptr<T, D> u(d)

空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete

u = nullptr

释放u指向的对象,将u置位空

u.release()

u放弃对指针的控制权,返回指针,并将u置为空。但是,如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放。

u.reset()

释放u指向的对象

u.reset(q)

如果提供了内置指针q,令u指向这个对象;否则将u置为空。

u.reset(nullptr)

 

传递unique_ptr和返回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;

}

智能指针陷阱

      智能指针可以提供对动态分配的内存安全而有方便的管理,但这建立在正确使用的前提下。为了正确使用智能指针,我们必须坚持一些基本规范:

1.   不使用相同的内置指针值初始化(或reset)多个智能指针。

2.   不delete get()返回的指针。

3.   不使用get()初始化或reset另一个智能指针。

4.   如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变得无效了。

5.   如果你使用的智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。

动态数组

为了支持为很多对象分配内存的功能,C++语言和标准库提供了两种一次分配一个对象数组的方法:C++语言定义了另一种new表达式语法,可以分配并初始化一个对象数组。标准库中包含一个名为allocator的类,允许我们将分配和初始化隔离。

Best Practice:大多数应用应该使用标准库容器而不是动态分配的数组。使用容器更为简单,更不容易出现内存管理错误而且可能有更好的性能。

1.   new和delete

为了让new分配一个对象数组,我们要在类型名之后跟一对方括号,在其中指明要分配的对象的数目。在下例中,new分配要求数量的对象并(假定分配成功后)返回指向第一个对象的指针:

int *pia = newint[get_size()];  //方括号中的大小必须是整型,但不必是常量

Warning:要记住我们所说的动态数组不是数组类型,这是很重要的。

初始化动态分配的数组:

默认情况下,new分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。可以对数组中的元素进行值初始化,方法是在大小之后跟一对空括号。

int *pia = newint[10];  //10个未初始化的int

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

动态分配一个空数组是合法的:

可以用任意表达式来确定要分配的对象的数目:

size_t n =get_size();

int *p = newint(n);

for(int *q = p; q!= p+n; q++);

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

char arr[0]; //错误,不能定义长度等于0的数组

char *cp = newchar[0];  //正确,但cp不能解引用

释放动态数组:

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

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

      delete [] p;  //p必须指向一个动态分配的数组或为空,而且数组中的元素按逆序销毁,即,最后一个元素首先被销毁,然后是倒数第二个,依次类推。

2.   allocator类

new将内存分配和对象构造组合在了一起,delete将对象析构和内存释放组合在了一起。此时,有两个弊端:一是我们分配了大块的内存,可能存在浪费;二是每个元素都被赋值了两次,第一次是在默认初始化时,随后是在赋值时。

标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。

标准库allocator类及其操作

allocator<T> a

定义了一个名为a的allocator对象,它可以对类型为T的对象分配内存。

a.allocate(n);

分配一段原始的未构造的内存,保存n个类型为T的对象

a.deallocate(p, n);

释放从T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象;p必须是一个先前有allocate返回的指针。

a.construct(p, args);

p必须是一个类型为T*的指针,指向一块原始内存;args被传递到类型为T的构造函数,用来在p指向的内存中构造一个对象。

a.destroy(p)

p为T*类型的指针,此算法对p指向的对象执行析构函数。

 使用allocator类操作的顺序:

(1).alloc.allocate(n);

(2).alloc.construct(p,args);

(3).alloc.destroy(p);

(4).alloc.deallocate(p,n);

Warning:

(1).为了使用allocate返回的内存,我们必须用construct构造对象。使用未构造的内存,其行为是未定义的。

(2).我们只能对真正构造了的元素进行destroy操作。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值