欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

C++学习——动态内存与智能指针

时间:2023-06-06
C++学习——动态内存与智能指针

动态内存与智能指针

shared_ptr 类直接内存管理shared_ptr 和 new 结合使用智能指针和异常unique_ptrweak_ptr 动态内存与智能指针

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

为了更容易(同时也更安全)地使用动态内存,新的标准库提供了两种智能指针类型来管理动态对象。

智能指针与常规指针的区别是它负责自动释放所指向的对象。

新标准库提供的这两种智能指针的区别在于管理底层指针的方式:

shared_ptr 允许多个指针指向同一个对象;unique_ptr 则“独占”所指向的对象。

还定义了一个名为 weak_ptr 的伴随类,它是一种弱引用,指向 shared_ptr 所管理的对象。
这三种类型都定义在 memory 头文件中。

shared_ptr 类

智能指针也是模板。

当创建一个智能指针时,必须提供额外的信息——指针可以指向的类型。
在尖括号内给出类型,之后是所定义的这种智能指针的名字:

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

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

智能指针的使用方式与普通指针类似。解引用一个智能指针返回它指向的对象。

如果有一个条件判断中使用智能指针,效果就是检测它是否为空:

// 如果p1不为空,检查它是否指向一个空 string if (p1 && P1->empty())*p1 = "hi"; // 如果p1指向一个空 string,解引用p1,将一个新值赋予 string

make_shared 函数

最安全的分配和使用动态内存的方法。

此函数在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptr。
与智能指针一样,make_shared 也定义在头文件 memory 中。

当要用 make_shared 时,必须指定想要创建的对象的类型。
定义方式与模板类相同,在函数名之后跟一个尖括号,在其中给出类型:

// 指向一个值为42的int的shared_ptrshared_ptr p3 = make_shared(42);// p4指向一个值为“9999999999”的stringshared_ptr p4 = make_shared(10, '9');//p5指向一个值初始化的 int,即,值为0shared_ptr p5 = make_shared();

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

// p6 指向一个动态分配的空 vactorauto p6 = make_shared>();

shared_ptr 的拷贝和赋值

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

auto p = make_shared(42); //p指向的对象只有p一个引用者auto q(p); // p 和 q指向相同对象,此对象有两个引用者

每个 shared_ptr 都有一个关联的计数器,通常称其为引用计数。

无论何时拷贝一个 shared_ptr ,计数器都会增加。
当给 shared_ptr 赋予一个新值或是 shared_ptr 被销毁时,计数器就会递减。

一旦一个 shared_ptr 的计数器变为0,它就会自动释放自己所管理的对象:

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

shared_ptr 自动销毁所管理的对象……

当指向一个对象的最后一个 shared_ptr 被销毁时,shared_ptr 类会自动销毁此对象。
它通过另一个特殊的成员函数——析构函数完成销毁工作。

析构函数一般用来释放对象所分配的资源。

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

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

当动态对象不再被使用时,shared_ptr 类会自动释放动态对象,这一特性使得动态内存的使用变得非常容易。

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

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

1.程序不知道自己需要使用多少对象2.程序不知道所需对象的准确类型3.程序需要在多个对象间共享数据 直接内存管理

运算符 new 分配内存,delete 释放 new 分配的内存。

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

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

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

此new表达式在自由空间构造一个 int 型对象,并返回指向该对象的指针。

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

string *ps = new string; // 初始化为空stringint *pi = new int; // pi 指向一个未初始化的 int

可以使用直接初始化方式来初始化一个动态分配的对象。
可以使用传统的构造方式(使用圆括号),在新标准下,也可以使用列表初始化(使用花括号):

int *pi = new int(1024); // pi指向的对象的值为1024string *ps = new string(10,'9'); // *ps 为"9999999999"//vector 有10个元素,值一次从0到9vector *pv = new vector{0,1,2,3,4,5,6,7,8,9}

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

string *ps1 = new string; //默认初始化为空 stringstring *ps = new string(); //值初始化为空 stringint *pi1 = new int; //默认初始化; *pi1的值未定义int *pi2 = new int(); //值初始化为0; *pi2为0

动态分配的 const 对象

用 new 分配 const 对象是合法的:

//分配并初始化一个 const intconst int *pci = new const int(1024);//分配并默认初始化一个 const 的空 stringconst string *pcs = new const string;

内存耗尽
默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为 bad_alloc 的异常。

可以改变使用new的方式来阻止它抛出异常:

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

称这种形式的new为定位new。
定位new表达式允许我们向 new 传递额外的参数。

释放动态内存

为了防止内存耗尽,在动态内存使用完毕后,必须将其归还个系统。
通过delete表达式来将动态内存归还给系统。

delete表达式接受一个指针,指向我们想要释放的对象:

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

delete 表达式也执行两个动作:销毁给定的指针指向的对象;释放对应的内存。

指针值和 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; // 正确:释放一个空指针总是没有错误的

想要释放一个 const 动态对象,只要 delete 指向它的指针即可:

const int *pci = new const int(1024);delete pci; // 正确:释放一个 const 对象

动态对象的生存期直到被释放时为止

返回指向动态内存的指针(而不是智能指针)的函数给其调用者增加了一个额外负担——调用者必须记得释放内存:

// factory 返回一个指针,指向一个动态分配的对象Foo* factory(T arg){// 视情况处理 argreturn new Foo(arg); // 调用者负责释放此内存}

factory 的调用者负责在不需要此对象时释放它。

与类类型不同,内置类型的对象被销毁时什么也不会发生。

特别是,当一个指针离开其作用域时,它所指向的对象什么也不会发生。如果这个指针指向的是动态内存,那么内存将不会被自动释放。

delete 之后重置指针值……

当 delete 一个指针后,指针值就变为无效了。
虽然指针已经无效,但在很多机器上指针仍然保存着(已经释放了的)动态内存的地址。
在delete 之后,指针就变成了人们所说的空悬指针,即,执行一块曾经保存数据对象但限制已经无效的内存的指针。

可避免空悬指针的问题:在指针即将要离开其作用域之前释放它所关联的内存。

……这只是提供了有限的保护

动态内存的一个基本问题是可能有多个指针指向相同的内存。
在 delete 内存之后重置指针的方法只对这个指针有效,对其它任何仍指向(已释放的)内存的指针是没有作用的。
例如:

int *p(new int(42)); // p指向动态内存auto q = p; // p和q指向相同的内存delete p; // p和q均变为无效p = nullptr; // 指出 p 不再绑定到任何对象

shared_ptr 和 new 结合使用

可以用 new 返回的指针来初始化智能指针:

shared_ptr p1; // shared_ptr 可以指向一个 doubleshared_ptr p2(new int(42)); // p2 指向一个值为42的int

接受指针参数的智能指针构造函数是 explicit 的。

不能将一个内置指针隐式转换成一个智能指针,必须使用直接初始化形式来初始化一个智能指针:

shared_ptr p1 = new int(1024); //错误:必须使用直接初始化形式shared_ptr p2(new int(1024)); //正确:使用了直接初始化形式

必须将shared_ptr 显式地绑定到一个想要返回的指针上:

shared_ptr clone(int p){//正确:显式地用 int* 创建 shared_ptrreturn shared_ptr(new int(p));}

默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用 delete 释放它所关联的对象。


其他 shared_ptr 操作

可以用 reset 来将一个新的指针赋予一个 shared_ptr:

p = new int(1024); // 错误:不能将一个指针赋予 shared_ptrp.reset(new int(1024)); // 正确:p 指向一个新对象

reset 会更新引用计数,如果需要的话,会释放 p 指向的对象。
reset 成员经常与 unique 一起使用,来控制多个 shared_ptr 共享的对象。

智能指针和异常

使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放:

void f(){shared_ptr sp(new int(42)); //分配一个新对象//这段代码抛出一个异常,且在 f 中未被捕获//在函数结束时 shared_ptr 自动释放内存}

当发生异常时,直接管理的内存是不会自动释放的,如果使用内置指针管理内存,且在 new 之后在对应的 delete 之前发生了异常,则内存不会被释放:

void f(){int *ip = new int(42); // 动态分配一个对象// 这段代码抛出一个异常,且在 f 中未被捕获delete ip; // 在退出之前释放内存}

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

unique_ptr

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

当定义一个 unique_ptr 时,需要将其绑定到一个 new 返回的指针上。

初始化 unique_ptr 必须采用直接初始化形式:

unique_ptr p1; // 可以指向一个 double 的 unique_ptrunique_ptr p2(new int(42)); // p2 指向一个值为 42 的 int

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

unique_ptr p1(new string("Stegosaurus"));unique_ptr p2(p1); // 错误:unique_ptr 不支持拷贝unique_ptr p3;p3 = p2; // 错误:unique_ptr 不支持赋值

可以通过调用 release 或 reset 将指针的所有权从一个(非 const )unique_ptr 转移给另一个 unique :

// 将所有权从 p1 (指向 string Stegosaurus)转移给 p2unique_prt p2(p1.release()); // release 将 p1 置为空unique_prt p3(new string("Trex"));// 将所有权从 p3 转移给 p2p2.reset(p3.release()); // reset 释放了 p2 原来指向的内存

传递 unique_ptr 参数和返回 unique_ptr

可以拷贝或赋值一个将要被销毁的 unique_ptr 。

例:从函数返回一个 unique_ptr:

unique_ptr clone(int p) {// 正确:从 int* 创建一个 unique_ptrreturn unique_ptr(new int(p));}

还可以返回一个局部对象的拷贝:

unique_ptr clone(int p) {unique_ptr ret(new int (p));// ...return ret;}

weak_ptr

weak_ptr 是一种不控制所指向对象生存期的智能指针,它指向由一个 shared_ptr 管理的对象。

将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数。


当创建一个 weak_ptr 时,要用一个 shared_ptr 来初始化它:

auto p = make_shared(42);weak_ptr wp(p); // wp 弱共享p;p的引用计数未改变

由于对象可能不存在,不能使用 weak_ptr 直接访问对象,而必须调用 lock 。
此函数检查 weak_ptr 指向的对象是否仍存在。如果存在,lock 返回一个指向共享对象的 shared_ptr 。
与任何其他 shared_ptr 类似,只要此 shared_ptr 存在,它所指向的底层对象也就会一直存在。
例:

if (shared_ptr np = wp.lock()) { // 如果 np 不为空则条件成立// 在 if 中, np 与 p 共享对象}

这段代码中,只有当 lock 调用返回 true 时我们才会进入 if 语句体。在 if 中,使用 np 访问共享对象是安全的。

学习参考资料:

C++ 中文版 Primer (第5版)

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。