C++标准库 -- 动态内存 (Primer C++ 第五版 · 阅读笔记)

news2024/11/24 17:17:14

C++标准库 --动态内存 (Primer C++ 第五版 · 阅读笔记)

  • 第12章 动态内存------(持续更新)
    • 12.1、动态内存与智能指针
      • 12.1.1、shared_ptr类
      • 12.1.2、直接管理内存
      • 12.1.3、shared_ptr和new结合使用
      • 12.1.4、智能指针和异常
      • 12.1.5、unique_ptr
      • 12.1.6、weak_ptr
    • 12.2、动态数组
      • 12.2.1、new和数组
      • 12.2.2、allocator类
    • 12.3、使用标准库:文本查询程序
      • 12.3.1、文本查询程序设计
      • 12.3.2、文本查询程序类的设计

第12章 动态内存------(持续更新)

我们的程序到目前为止只使用过静态内存或栈内存。

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

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

虽然使用动态内存有时是必要的,但众所周知,正确地管理动态内存是非常棘手的。

12.1、动态内存与智能指针

在C++中,动态内存的管理是通过一对运算符来完成的:

  • new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;
  • delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。

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

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

  • shared_ptr 允许多个指针指向同一个对象;
  • unique_ptr 则“独占”所指向的对象。
  • 标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared _ptr所管理的对象。
  • 这三种类型都定义在memory头文件中。

12.1.1、shared_ptr类

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

//默认初始化的智能指针中保存着一个空指针
shared ptr<string> pl;			// shared_ptr,可以指向string
shared_ptr<list<int>> p2;		// shared ptr,可以指向int的list

//如果p1不为空,检查它是否指向一个空string
if (pl && p1->empty())
	*p1 = "hi"; // 如果p1指向一个空string,解引用pl,将一个新值赋予string
  • 智能指针的使用方式与普通指针类似。解引用一个智能指针返回它指向的对象

表12.1列出了shared_ptrunique_ptr都支持的操作。只适用于shared_ptr

在这里插入图片描述
在这里插入图片描述

⭐️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指向一个值初始化的(参见3.3.1节)int,即,默认值为0
shared_ptr<int> p5 = make_shared<int>();

//当然,我们通常用auto(参见2.5.2节)定义一个对象来保存make_shared的结果,这种方式较为简单:
// p6指向一个动态分配的空vector<string>
auto p6 = make_shared<vector<string>>();

类似顺序容器的 emplace 成员(参见9.3.1节),make_shared其参数构造给定类型的对象。例如,

  • 调用make_shared<string>传递的参数必须能用来初始化一个的某个构造函数相匹配,
  • 调用make _sharea<int>传递的参数必须能用来初始化一个int,依此类推。

如果我们不传递任何参数,对象就会进行值初始化(默认值初始化)(参见3.3.1 节)。

⭐️shared_ptr 的拷贝和赋值

当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象。
我们可以认为每个shared_ptr指向的对象都有一个关联的计数器,通常称其为引用计数(reference count)。无论何时我们拷贝一个shared_ptr,计数器都会递增。例如:

  • 当用一个shared_ptr初始化另一个shared_ptr
  • 或将它作为参数传递给一个函数(参见6.2.1节)
  • 以及作为函数的返回值(参见6.3.2节)时,它所关联的计数器就会递增

当我们给shared_ptr赋予一个新值或是shared_ptr销毁(例如一个局部的shared_ptr离开其作用域时,计数器就会递减

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

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

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

到底是用一个计数器还是其他数据结构来记录有多少指针共享对象,完全由标准库的具体实现来决定。关键是智能指针类能记录有多少个 shared_ptr指向相同的对象,并能在恰当的时候自动释放对象。

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

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

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

  • 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离开了作用域,它指向的内存会被自动释放掉

但如果有其他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. 程序需要在多个对象间共享数据

容器类是出于第一种原因而使用动态内存的典型例子,我们将在第15章看到出于第二种原因而使用动态内存的例子。在本节中,我们将定义一个类,它使用动态内存是为了让多个对象能共享相同的底层数据。

到目前为止,我们使用过的类中,分配的资源都与对应对象生存期一致。例如,每个vector “拥有”其自己的元素。当我们拷贝一个vector时,原vector和副本vector中的元素是相互分离的:

vector<string> vl; //空vector
{//新作用域
	vector<string> v2 = {"a", "an", "the"};
	vl = 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创建的元素

在此例中,b1b2共享相同的元素。当b2离开作用域时,这些元素必须保留,因为b1仍然在使用它们。

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

:使用动态内存创建类

12.1.2、直接管理内存

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

  • 相对于智能指针,使用这两个运算符管理内存非常容易出错,随着我们逐步详细介绍这两个运算符,这一点会更为清楚。
  • 而且,自己直接管理内存的类与使用智能指针的类不同,它们不能依赖类对象拷贝、赋值和销毁操作的任何默认定义(参见7.1.4 节)。因此,使用智能指针的程序更容易编写和调试

在学习第13章之前,除非使用智能指针来管理内存,否则不要分配动态内存。

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

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

int是内置类型;而string是容器,是类类型。

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

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

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

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

int *pi = new int(1024);			// pi指向的对象的值为1024
string *ps = new string(10, '9');	// *ps为"9999999999"

// vector有10个元素,值依次从0到9
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};

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

string *psl = new string;		//默认初始化为空string
string *ps = new string();		//值初始化为空string
int *pil = new int;				//默认初始化; *pi1的值未定义
int *pi2 = new int();			//值初始化为0; *pi2为0
  • 对于定义了自己的构造函数类类型(例如string)来说,要求值初始化是没有意义的;不管采用什么形式,对象都会通过默认构造函数来初始化。
  • 但对于内置类型,两种形式的差别就很大了;
    • 值初始化的内置类型对象有着良好定义的
    • 默认初始化的对象的值则是未定义的。
  • 类似的,对于类中那些依赖于编译器合成的默认构造函数的内置类型成员,如果它们未在类内被初始化,那么它们的值也是未定义的(参见7.1.4节)。

出于与变量初始化相同的原因,对动态分配的对象进行初始化通常是个好主意。

如果我们提供了一个括号包围的初始化器,就可以使用auto(参见2.5.2节)从此初始化器来推断我们想要分配的对象的类型。但是,由于编译器要用初始化器的类型来推断要分配的类型,只有当括号中仅有单一初始化器时才可以使用auto

auto p1 = new auto(obj); 		//p1指向一个与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 *p1 = new int; // 如果分配失败,new抛出std::bad_alloc
int *p2 = new (nothrow) int; // 如果分配失败,new返回一个空指针
  • 我们称这种形式的new定位new (placement new), 定位new表达式允许我们向new传递额外的参数。
  • 在此例中,我们传递给它一个由标准库定义的名为nothrow的对象。如果将nothrow传递给new,我们的意图是告诉它不能抛出异常。
  • 如果这种形式的new不能分配所需内存,它会返回一个空指针。
  • bad_allocnothrow都定义在头文件new中。

⭐️释放动态内存

为了防止内存耗尽,在动态内存使用完毕后,必须将其归还给系统。我们通过 delete 表达式(delete expression)来将动态内存归还给系统。delete 表达式接受一个指针,指向我们想要释放的对象:

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

new类型类似,delete表达式也执行两个动作:

  1. 销毁给定的指针指向的对象;
  2. 释放对应的内存

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

对于delete i 的请求,编译器会生成一个错误信息,因为它知道 i 不是一个指针。
执行delete pi1pd2 所产生的错误则更具潜在危害:通常情况下,编译器不能分辨一个指针指向的是静态还是动态分配的对象。类似的,编译器也不能分辨一个指针所指向的内存是否已经被释放了。对于这些delete表达式,大多数编译器会编译通过,尽管它们是错误的。

虽然一个const对象的值不能被改变,但它本身是可以被销毁的。如同任何其他动态对象一样,想要释放一个const动态对象,只要 delete指向它的指针即可:

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

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

如由shared_ptr 管理的内存在最后一个shared_ptr 销毁时会被自动释放。但对于通过内置指针类型来管理的内存,就不是这样了。对于一个由内置指针管理的动态对象,直到被显式释放之前它都是存在的。

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

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

类似我们之前定义的 factory 函数(参见12.1.1节),这个版本的factory分配一个对象,但并不delete它。factory调用者负责在不需要此对象时释放它。不幸的是,调用者经常忘记释放对象:

void use_factory(T 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 (T arg)
{
	Foo *p = factory(arg);		//使用p
	return p;					//调用者必须释放内存
}

小心:动态内存的管理非常容易出错
使用newdelete管理动态内存存在三个常见问题:
1、忘记 delete内存。忘记释放动态内存会导致人们常说的“内存泄漏”问题,因为这种内存永远不可能被归还给自由空间了。查找内存泄露错误是非常困难的,因为通常应用程序运行很长时间后,真正耗尽内存时,才能检测到这种错误。
2、使用已经释放掉的对象。通过在释放内存后将指针置为空,有时可以检测出这种错误。
3、同一块内存释放两次。当有两个指针指向相同的动态分配对象时,可能发生这种错误。如果对其中一个指针进行了delete操作,对象的内存就被归还给自由空间了。如果我们随后又 delete 第二个指针,自由空间就可能被破坏。
相对于查找和修正这些错误来说,制造出这些错误要简单得多。

坚持只使用智能指针,就可以避免所有这些问题。对于一块内存,只有在没有任何智能指针指向它的情况下,智能指针才会自动释放它。

⭐️delete之后重置指针值……

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

未初始化指针的所有缺点空悬指针也都有。有一种方法可以避免空悬指针的问题:

  • 在指针即将要离开其作用域之前释放掉它所关联的内存。这样,在指针关联的内存被释放掉之后,就没有机会继续使用指针了。
  • 如果我们需要保留指针,可以在 delete 之后将 nullptr 赋予指针,这样就清楚地指出指针不指向任何对象。

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

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

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

本例中 pq 指向相同的动态分配的对象。

  • 我们delete此内存,然后将 p 置为 nullptr,指出它不再指向任何对象。
  • 但是,重置 pq 没有任何作用,在我们释放 p 所指向的(同时也是 q 所指向的!)内存时,q也变为无效了。在实际系统中,查找指向相同内存的所有指针是异常困难的

12.1.3、shared_ptr和new结合使用

如前所述,如果我们不初始化一个智能指针,它就会被初始化为一个空指针。如表12.3所示,我们还可以用 new 返回的指针来初始化智能指针:

shared ptr<double> pl; // shared_ptr可以指向一个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.1.4节介绍如何定义自己的释放操作。

在这里插入图片描述
在这里插入图片描述

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

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 的值,其行为是未定义的。

当将一个sshared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。
使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。

⭐️ ……也不要使用get初始化另一个智能指针或为智能指针赋值

智能指针类型定义了一个名为 get 的函数(参见表12.1),它返回一个内置指针,指向智能指针管理的对象。此函数是为了这样一种情况而设计的:

  • 我们需要向不能使用智能指针的代码传递一个内置指针
  • 使用 get 返回的指针的代码不能 delete此指针。

虽然编译器不会给出错误信息,但将另一个智能指针也绑定到 get 返回的指针上是错误的:

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

在本例中,pq 指向相同的内存。由于它们是相互独立创建的,因此各自的引用计数都是 1。当 q 所在的程序块结束时,q 被销毁,这会导致 q 指向的内存被释放。从而 p 变成一个空悬指针,意味着当我们试图使用 p 时,将发生未定义的行为。而且,当 p 被销毁时,这块内存会被第二次 delete

get 用来将指针的访问权限传递给代码,你只有在确定代码不会 delete 指针的情况下,才能使用get
特别是,永远不要用 get初始化另一个智能指针或者为另一个智能指针赋值

⭐️ 其他shared_ptr操作

shared_ptr 还定义了其他一些操作,参见表12.2和表12.3所示。我们可以用 reset 来将一个新的指针赋予一个shared_ptr:

p = new int(1024);			//错误:不能将一个指针赋予shared_ptr
p.reset(new int(1024)); 	//正确:p指向一个新对象
  • 与赋值类似,reset更新引用计数,如果需要的话,会释放 p 指向的对象。
  • reset 成员经常与 unique 一起使用,来控制多个shared_ptr共享的对象。在改变低层之前,我们检查自己是否是当前对象仅有的用户。如果不是,在改变之前要制作一份新的拷贝:
if(!p.unique())
	p.reset(new string (*p)); //我们不是唯一用户﹔分配新的拷贝
*p += newVal;	//现在我们知道自己是唯一的用户,可以改变对象的值

12.1.4、智能指针和异常

我们知道使用异常处理的程序能在异常发生后令程序流程继续,我们注意到,这种程序需要确保在异常发生后资源能被正确地释放一个简单的确保资源被释放的方法是使用智能指针

如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放,函数的退出有两种可能:

  • 正常处理结束或者发生了异常,无论哪种情况,局部对象都会被销毁。
  • 在下面的程序中,sp 是一个 shared_ptr,因此 sp 销毁时会检查引用计数。在此例中,sp是指向这块内存的唯一指针,因此内存会被释放掉。
void f()
{
	shared_ptr<int> sp (new int(42));	//分配一个新对象
	//这段代码抛出一个异常,且在f中未被捕获
}//在函数结束时shared_ptr自动释放内存

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

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

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

⭐️ 智能指针和哑类

包括所有标准库类在内的很多C++类都定义了析构函数,负责清理对象使用的资源。但是,不是所有的类都是这样良好定义的。特别是
那些为C和C++两种语言设计的类,通常都要求用户显式地释放所使用的任何资源

那些分配了资源,而又没有定义析构函数来释放这些资源的类,可能会遇到与使用动态内存相同的错误——程序员非常容易忘记释放资源。类似的,如果在资源分配和释放之间发生了异常,程序也会发生资源泄漏

与管理动态内存类似,我们通常可以使用类似的技术来管理不具有良好定义的析构函数的类。例如,假定我们正在使用一个C和C++都使用的网络库,使用这个库的代码可能是这样的:

struct destination;					//表示我们正在连接什么
struct connection;					//使用连接所需的信息
connection connect(destination*);	//打开连接
void disconnect (connection);		//关闭给定的连接
void f(destination &d /*其他参数*/)
{
	//获得一个连接;记住使用完后要关闭它
	connection c = connect(&d);
	//使用连接
	//如果我们在f退出前忘记调用disconnect,就无法关闭c了
}
  • 如果connection有一个析构函数,就可以在 f 结束时由析构函数自动关闭连接。
  • 但是,connection没有析构函数。这个问题与我们上一个程序中使用shared_ptr避免内存泄漏几乎是等价的。
  • 使用 shared_ptr 来保证 connection 被正确关闭,已被证明是一种有效的方法。

⭐️ 使用我们自己的释放操作

默认情况下,shared_ptr 假定它们指向的是动态内存。因此,当一个 shared_ptr 被销毁时,它默认地对它管理的指针进行 delete 操作。

  • 为了用shared _ptr来管理一个connection,我们必须首先定义一个函数来代替delete
  • 这个删除器deleter)函数必须能够完成对 shared_ptr 中保存的指针进行释放的操作。
  • 当我们创建一个 shared_ptr 时,可以传递一个(可选的)指向删除器函数的参数:

在本例中,我们的删除器必须接受单个类型为connection* 的参数:

//删除器
void end_connection(connection *p){ disconnect(*p);}

void f(destination &d /*其他参数*/)
{
	connection c = connect(&d);
	shared_ptr<connection> p(&c, end_connection);
	//使用连接
	//当f退出时(即使是由于异常而退出), connection会被正确关闭
}
  • p 被销毁时,它不会对自己保存的指针执行delete,而是调用end_connection
  • 接下来,end_connection会调用 disconnect,从而确保连接被关闭。
  • 如果 f 正常退出,那么 p 的销毁会作为结束处理的一部分。如果发生了异常,p 同样会被销毁,从而连接被关闭。

👉 注意:智能指针陷阱 👈
智能指针可以提供对动态分配的内存安全而又方便的管理,但这建立在正确使用的前提下。为了正确使用智能指针,我们必须坚持一些基本规范:
❗️ 不使用相同的内置指针值初始化(或reset)多个智能指针
❗️ 不 delete get()返回的指针
❗️ 不使用 get() 初始化或 reset 另一个智能指针
❗️ 如果你使用 get() 返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。
❗️ 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器(参见12.1.4节)。

12.1.5、unique_ptr

⭐️ 传递unique_ptr参数和返回unique_ptr

⭐️ 向unique_ptr传递删除器

12.1.6、weak_ptr

⭐️ 核查指针类

⭐️ 指针操作

12.2、动态数组

12.2.1、new和数组

12.2.2、allocator类

12.3、使用标准库:文本查询程序

12.3.1、文本查询程序设计

12.3.2、文本查询程序类的设计

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/478727.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网络通信之网络层与数据链路层

文章目录 讲在前面网络层网络层概述IP协议格式网段划分公有IP、私有IP、特殊IP理解路由 数据链路层MAC地址以及MAC帧&#xff08;以太网帧&#xff09;MTU协议MTU对IP和TCP协议的影响ARP协议及其作用 涉及到的相关协议DNS协议&#xff08;应用层&#xff09;NAT与NAPT协议 总结…

BEV (0)---DETR

1 DETR 1.1 DETR处理流程 1.1.1 将图像输入给Backbone获得图像特征与位置编码 ①. 对给定的输入图像通过resnet进行特征提取&#xff0c;最终得到特征图C5∈RBx2048xhxw,其中h、w为输入图像尺寸得1/32。随后再用一层11卷积压缩一下通道&#xff0c;得到特征图P5∈RBx256xhxw。…

jvm调优策略

jvm调优主要是内存管理方面的调优&#xff0c;包括各个代的大小&#xff0c;GC策略等。 代大小调优 JVM 中最大堆大小有三方面限制&#xff1a;相关操作系统的数据模型&#xff08;32-bt还是64-bit&#xff09;限制&#xff1b;系统的可用虚拟内存限制&#xff1b;系统的可用物…

数据结构学习记录——什么是堆(优先队列、堆的概念、最大堆最小堆、优先队列的完全二叉树表示、堆的特性、堆的抽象数据类型描述)

目录 优先队列 若采用数组或链表实现优先队列 数组 链表 有序数组 有序链表 总结 若采用二叉搜索树来实现优先队列 最大堆 堆的概念 优先队列的完全二叉树表示 堆的两个特性 结构性 有序性 【例】最大堆和最小堆 【例】不是堆 堆的抽象数据类型描述 优先队列…

安排超市 -- BFS分割搜索

4.安排超市 给定一个n*n的地图。地图是上下左右四联通的&#xff0c;不能斜向行走&#xff1a; *代表障碍&#xff0c;不可通行。 .代表路&#xff0c;可以通行。 #代表房子。房子也是可以通行的。 小红现在需要在一些地方安排一些超市&#xff08;不能安排在障碍物上&#xf…

山东专升本计算机第七章-计算机网络基础

计算机网络基础 计算机网络系统 考点 6 计算机网络硬件 主体设备 • 称为主机 • 一般可分为中心站&#xff08;又称服务器&#xff09;和工作站&#xff08;客户机&#xff09; 连接设备 • 网卡 • 工作在数据链路层 • 网卡又称网络适配器&#xff0c;是连接主机和网…

【C++初阶】引用

一.概念 引用就是取别名&#xff0c;在语法上它不会开空间&#xff0c;而是和它引用的变量共用同一块空间。对引用的操作也就是对原来变量的操作。就像现实生活中给人取外号一样&#xff0c;不管是喊外号还是本名&#xff0c;指的都是那个人。 二.引用特性 1.引用类型必须和引用…

Java8 新特性讲解

一、Lambda表达式 Lambda 是一个匿名函数&#xff0c;我们可以把 Lambda 表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。使用它可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格&#xff0c;使Java的语言表达能力得到了提升。 二、函数式接口 &#…

【网课平台】Day15.Devops:持续集成与持续交付

文章目录 一、Devops1、什么是Devops2、什么是CI/CD3、Devops方案参考 二、人工部署1、项目打jar包2、生成镜像、创建容器 三、自动化部署1、代码提交到git2、修改pom.xml文件3、前端部署 一、Devops 1、什么是Devops 一个软件的生命周期包括&#xff1a;需求分析阶、设计、开…

SpringCloud:ElasticSearch之集群

单机的elasticsearch做数据存储&#xff0c;必然面临两个问题&#xff1a;海量数据存储问题、单点故障问题。 海量数据存储问题&#xff1a;将索引库从逻辑上拆分为N个分片&#xff08;shard&#xff09;&#xff0c;存储到多个节点单点故障问题&#xff1a;将分片数据在不同节…

【原创】运维的终点是开发~chatGPT告诉你真相

文章目录 软件技术岗位鄙视链&#xff0c;你在哪层呢&#xff1f;让chatGPT告诉你运维工作好&#xff0c;还是开发工作好问它几个问题1. 一个三年运维成长的案例和薪资2. 一个三年开发成长的案例和薪资3. 一个五年运维成长的案例和薪资4. 一个五年开发成长的案例和薪资5. 一个十…

云分析迁移:顺应需求

云提供了对新分析功能、工具和生态系统的访问&#xff0c;可以快速利用这些功能、工具和生态系统来测试、试点和推出新产品。然而&#xff0c;尽管迫在眉睫&#xff0c;但企业在将分析迁移到云时仍感到担忧。组织正在寻找能够帮助他们分配资源和集成业务流程的服务提供商&#…

Linux 服务器上安装和使用 Redis,只需这 4 步!

一、使用 yum 安装 Redis 使用以下命令&#xff0c;直接将 redis 安装到 linux 服务器&#xff1a; yum -y install redis 二、配置远程连接 a&#xff09;首先第一步&#xff0c;将 redis 配置文件下载到本地&#xff08;如果你熟悉 vim 操作&#xff0c;直接用 vim 编辑即可…

论文阅读《PIDNet: A Real-time Semantic Segmentation Network Inspired by PID》

论文地址&#xff1a;https://arxiv.org/pdf/2206.02066.pdf 源码地址&#xff1a;https://github.com/XuJiacong/PIDNet 概述 针对双分支模型在语义分割任务上直接融合高分辨率的细节信息与低频的上下文信息过程中细节特征会被上下文信息掩盖的问题&#xff0c;提出了一种新的…

【五一创作】Springboot+多环境+多数据源(MySQL+Phoenix)配置及查询(多知识点)

文章目录 1. 背景2. 技术点3 子模块依赖SpringBoot设置4. 多环境配置4.1 application.yml4.2 application-pro.yml 5. 多数据源配置5.1 yml配置5.2 自定义数据源在Java中配置5.2.1 PhoenixDataSourceConfig5.2.2 MysqlDataSourceConfig 6. 完整的Pom6. 测试6.1 Mapper配置6.2 方…

字符、块、网络设备

设备模型&#xff08;的意义&#xff09; 降低设备多样性带来的Linux驱动开发的复杂度&#xff0c;以及设备热拔插处理、电源管理等&#xff0c;Linux内核提出了设备模型概念。设备模型将硬件设备归纳、分类&#xff0c;然后抽象出一套标准的数据结构和接口。驱动的开发&#…

Java项目上线之云服务器环境篇(四)——Redis的安装与配置

Java项目上线之云服务器环境篇&#xff08;四&#xff09;——Redis的安装与配置 在我们的项目里可能会用到Redis缓存&#xff0c;需要对Redis进行简单的配置。 1、我们的redis最好放在一个事先安装好的文件夹里&#xff0c;这样更方便于管理。 例如&#xff1a; redis我是放在…

前端开发:JS的事件循环执行机制详解

前言 在前端开发中&#xff0c;涉及到JS原生的使用原理是非常重要的知识点&#xff0c;尤其是在实际工作过程中会遇到各种复杂的业务需求场景&#xff0c;以及具体开发中可能会遇到一些涉及基于JS原理的使用&#xff0c;这都要求开发者能够很好的了解和掌握JS原生的常用原理。J…

jvm之类加载器

写在前面 当我们通过javac命令将java源代码编译为Java字节码后&#xff0c;必须通过类加载器将其加载到jvm中才能运行&#xff0c;所以类加载器是jvm中非常重要的一个组成部分&#xff0c;本文我们就一起来看下吧&#xff01; 1&#xff1a;类的生命周期 类的生命周期如下图…

leetcode刷题之回文链表and最长回文子串

234.回文链表 方法一:找中间结点,断开链表,后一段链表进行反转 思路:①找中间结点:使用快慢指针fast,slow,fast每次走两个,slow每次走一个; 如果链表的个数是奇数个,那么最后slow指向中间节点 如果链表的个数是偶数个,那么最后slow指向中间两个节点的后一个 ②使用prev指针保…