动态内存、智能指针(shared_ptr、unique_ptr、weak_ptr)、动态数组

文章目录

  • 三种对象的分类
  • 三种内存的区别
  • 动态内存
    • 概念
    • 智能指针允许的操作
    • 智能指针的使用规范
  • new
    • 概念
    • 内存耗尽/定位new
    • 初始化
      • 默认初始化
      • 直接初始化
      • 值初始化
  • delete
    • 概念
    • 手动释放动态对象
    • 空悬指针
  • shared_ptr类
    • 格式
    • 独有的操作
    • make_shared函数
    • shared_ptr的计数器
    • 通过new用普通指针初始化shared_ptr
  • unique_ptr
    • 概念、初始化、特性
    • 支持的操作
  • weak_ptr
    • 概念
  • 关于普通指针和智能指针
    • 不能使用内置指针来访问shared_ptr所指向的内存
    • get()函数
    • reset函数
    • 处理异常
  • 动态数组
    • 概念
    • new分配对象数组
      • 两种方法
      • new的返回值
    • 初始化
    • 动态分配一个空数组是合法的
    • 释放动态数组
    • 动态数组和unique_ptr
    • 动态数组和shared_ptr
  • allocator类
    • new的局限性(使用allocator的原因)
    • 概念即创建销毁操作
      • construct
      • destroy
      • deallocate
    • 两个伴随算法
  • 使用了动态生存期的资源的类
    • 实例


三种对象的分类

三种对象:

  • 全局对象在程序启动时分配,在程序结束时销毁。
  • 局部自动对象,当我们进入其定义所在的程序块时被创建,在离开块时销毁。
  • 局部static对象在第一次使用前分配,在程序结束时销毁。

三种内存的区别

  1. 静态存储区:主要存放static静态变量、全局变量、常量。这些数据内存在编译的时候就已经为他们分配好了内存,生命周期是整个程序从运行到结束。
  2. 栈区:存放局部变量。在执行函数的时候(包括main这样的函数),函数内的局部变量的存储单元会在栈上创建,函数执行完自动释放,生命周期是从该函数的开始执行到结束。线性结构。
  3. 堆区:程序员自己申请的任意大小的内存。一直存在直到被释放。链表结构。

前两种内存中的将对象由编译器自动创建和销毁。堆也被称作自由空间,被用来存储动态分配的对象(程序运行时分配的对象),动态对象的生存期由程序来控制——当动态对象不被使用时,必须显式地消灭他们。

动态内存

概念

为什么要使用动态内存:

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

动态内存的分配与释放通过一对运算符来完成:

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

使用动态内存时容易出现的问题:

  • 忘记释放内存,产生的内存泄漏。这种内存永远不可能被归还给自由空间了。查找本错误是非常困难的,通常应用程序运行很长时间之后,真正耗尽内存时,才能检测到这种错误。
  • 在尚有指针引用内存的情况下释放内存,产生引用非法内存的指针
  • 释放一个已经被delete的内存,产生double free的问题。出现此操作时,自由空间就可能被破坏。

为了避免上述问题,c++11提供了两种智能指针(smart pointer)类型来管理动态对象,两种指针的区别在于管理底层指针的方式:

  • shared_ptr:允许多个指针指向同一个对象
  • unique_ptr:独占所指向的对象

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

智能指针允许的操作

在这里插入图片描述


智能指针的使用规范

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


new

概念

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

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

当然用new分配const的对象也是合法的,但是一个动态分配的const对象必须进行初始化:

  • 定义了默认构造函数的类类型,const动态对象可以隐式初始化
  • 其他类型必须显式初始化
  • 由于分配的对象是const的,因此new返回的指针是一个指向const的指针
    在这里插入图片描述

内存耗尽/定位new

值得一提的是,如果一个程序用光了它所有可用的内存,new表达式就会失败。 默认情况下。如果new不能分配所要求的内存空间,就会抛出一个bad_alloc的异常。可以改变使用new的方式来阻止它抛出异常:
在这里插入图片描述
我们称上面形式的new未定位new(placement new),定位new表达式允许我们向new传递额外的参数。上例中,我们传递给它一个由标准库定义的名为nothrow的对象,将nothrow传递给new,意图是告诉它不能抛出异常。

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


初始化

默认初始化

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

直接初始化

为了避免未定义行为,最好使用直接初始化的方式来初始化一个动态分配的对象:

  • 可以用圆括号
  • 可以用列表初始化

在这里插入图片描述

值初始化

也可以使用值初始化,只需要直接在类型名之后跟一对空括号即可:
在这里插入图片描述
值得一提的是:

  • 对于定义了自己的构造函数的类类型来说,要求值初始化是没有意义的——不管采用什么形式,对象都会通过默认构造函数来初始化。
  • 对于内置类型,两种形式的差别就很大了;值初始化的内置类型对象有着良好定义的值,而默认初始化的对象的值则是未定义的。类似的,对于类中那些依赖于编译器合成的默认构造函数的内置类型成员,如果它们未在类内被初始化,那么它们的值也是未定义的。


delete

概念

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

默认情况下shared_ptr和unique_ptr都使用delete释放指向的对象,但也都允许重载默认的删除器(delete)。

delete执行两个动作:

  • 销毁给定的指针指向的对象
  • 释放对应的内存

delete表达式接受一个指针,指向我们想要释放的对象,该指针必须指向动态分配的内存,或者是一个空指针。

通常情况下,

  • 编译器不能分辨一个指针指向静态还是动态分配的对象
  • 编译器不能分辨一个指针所指向的内存是否已经被释放

对于上述两种情况,大多数编译器会编译通过,尽管他们是错误的。

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

在这里插入图片描述

另外,const对象的值虽然不能够被改变,但是其本身可以被销毁:

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

手动释放动态对象

智能指针可以在计数值为0时自动释放动态对象,而delete是一种手动释放动态对象的方式,这就要求程序员不能忘记delete这一步骤。

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

举个例子:

foo *factory(T arg){return new Foo(arg); // 调用factory的对象负责释放动态内存
}void use_factory(T arg){Foo *p = factory(arg);
} // p离开了它的作用域,但实际所指向的内存没有被释放

本例中,一旦use_factory返回,程序就没有办法释放这块内存了。修正这个错误的唯一方法是在use_factory中记得释放内存:

void use_factory(T arg){Foo *p = factory(arg);delete p;
}

空悬指针

执行delete p;后,p并不指向空指针,相反的,p的值(指向的地址)不变,但不能再使用p处理该地址的内容(指针失效),也不能重复delete p。 此时p就变成了空悬指针(dangling pointer),即指向一块曾经保存数据对象但现在已经无效的内存的指针。

不能重复delete p

在这里插入图片描述

但是可以重复 delete 空指针:

在这里插入图片描述

避免空悬指针有两种方法:

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

但重置指针地方法仍然不是完美的,动态内存的一个基本问题是可能有多个指针指向相同的内存。在delete内存之后重置指针的方法只对这个指针有效,对其他任何仍指向(已释放的)内存的指针是没有作用的,然而在实际中,查找只想相同内存地所有指针也是异常困难的:
在这里插入图片描述



shared_ptr类

格式

shared_ptr<类型>

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


独有的操作

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
关于上面两表的具体操作将在下面的普通指针和智能指针中指出。


make_shared函数

shared_ptr可以协调对象的析构,但这仅限于其自身的拷贝(也是shared_ptr)之间。因此最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数,而不是new。这样,我们就能在分配对象的同时就将shared_ptr与之绑定,从而避免了无意中将同一块内存绑定到多个独立创建的shared_ptr上。

make_shared函数定义在头文件memory中。

功能:在动态内存中分配一个对象并初始化它,返回此对象的shared_ptr。

实例:
在这里插入图片描述
调用make_shared<T>时传递的参数必须与T的某个构造函数相匹配,换言之,调用make_shared的行为的底层操作其实是调用对应类型的构造函数。

当然,用auto定义一个对象来保存make_shared的结果也是可以的:

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

shared_ptr的计数器

因为shared_ptr允许多个指针指向同一个对象。因此每个shared_ptr都有一个关联的计数器,通常称其为引用计数(reference count),用来记录有多少个其他shared_ptr指向相同的对象。

  • 用一个shared_ptr初始化另一个shared_ptr
  • shared_ptr作为参数传递给一个函数
  • shared_ptr作为函数的返回值

时,shared_ptr所关联的计数器就会递增。

  • 给shared_ptr赋予一个新值(旧值计数器递减,新值计数器递增)
  • shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)

时,shared_ptr所关联的计数器就会递减。

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

由于在最后一个shared_ptr销毁前内存都不会释放, 保证shared_ptr在无用之后不再保留就非常重要了。如果你忘记了销毁程序不再需要的shared_ptr,程序仍会正确执行,但会浪费内存。

share_ptr在无用之后仍然保留的一种可能情况是,你将shared_ptr存放在一个容器中,随后重排了容器,从而不再需要某些元素。在这种情况下,你应该确保用erase删除那些不再需要的shared_ptr元素。


通过new用普通指针初始化shared_ptr

可以使用new返回的指针来初始化智能指针。接受指针参数的智能指针构造函数是explicit的。因此,我们不能进行内置指针到智能指针间的隐式转换,必须使用直接初始化形式来初始化一个智能指针:
在这里插入图片描述
p1的初始化隐式地要求编译器将一个new返回的int*隐式转换成一个shared_ptr,这是不被允许的。

同样的,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针:
在这里插入图片描述
必须将shared_ptr显式绑定到一个想要返回的指针上:
在这里插入图片描述



unique_ptr

概念、初始化、特性

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

unique_ptr没有类似make_shared的标准库函数。定义一个unique_ptr时,需要将其绑定到一个new返回的指针上,且必须采用直接初始化形式:

unique_ptr<double> pb;
unique_ptr<int> pi(new int(2));

根据“独占”的特性,unique_ptr不支持普通的拷贝或赋值操作:
在这里插入图片描述

不能拷贝unique_ptr的规则有个例外:可以拷贝或赋值一个将要被销毁的unique_ptr。

常见的例子是从函数返回一个unique_ptr:
在这里插入图片描述

或者返回一个局部对象的拷贝:
在这里插入图片描述


支持的操作

在这里插入图片描述
可以通过release或reset起到类似拷贝或赋值的作用:
在这里插入图片描述

release会切断unique_ptr和它原来管理的对象间的联系,返回的指针常被用来初始化另一个智能指针或给另一个智能指针赋值。但是,如果不用另一个智能指针来保存release返回的指针,就要记得手动释放资源:
在这里插入图片描述



weak_ptr

概念

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

具有以下特点:

  • 将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数
  • 引用计数归零时,即使仍有weak_ptr指向对象,对象还是会被释放

由于对象可能不存在,我们不能使用weak_ptr直接访问对象,必须调用lock。因此可以这样使用:
在这里插入图片描述
在这里插入图片描述



关于普通指针和智能指针

不能使用内置指针来访问shared_ptr所指向的内存

当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr,不应该再使用内置指针来访问shared_ptr所指向的内存了。

使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。

举例:
在这里插入图片描述
对于上面的函数,以智能指针作为参数以传值方式传递是安全的,当process结束时,ptr的引用计数为1,因此虽然局部ptr被销毁,但是ptr指向的内存不会被释放:
在这里插入图片描述
但同时也可以传递给process一个用内置指针显式构造的临时shared_ptr。但是这样做的风险是很大的:
在这里插入图片描述
process(x);结束时,临时对象被销毁,其引用计数为0,指向的内存会被释放,此时x变成了空悬指针。


get()函数

get函数返回一个内置指针,指向智能指针管理的对象。

函数是为了这种情况设计的:我们需要向不能使用智能指针的代码传递一个内置指针。

使用get返回的指针的代码不能delete此指针。

虽然编译器不会给出错误信息,但是将另一个智能指针也绑定到get返回的指针上是错误的:
在这里插入图片描述
上述代码中,shared_ptr<int>(q);将另一个指针绑定到get返回的指针上,会导致程序块结束时p指向的内存被释放,p变成空悬指针。


reset函数

reset将一个新的指针赋予shared_ptr:

shared_ptr<int> p = new int(1024); // error:不能将一个普通指针赋予shared_ptr
p.reset(new int(1024)); // 正确:p指向一个新对象

与赋值类似,reset会更新引用计数,在需要的时候,可以释放p指向的对象。

reset成员经常与unique一起使用,来控制多个shared_ptr共享的对象。在改变底层对象之前,我们检查自己是否是当前对象仅有的用户。如果不是,在改变之前要制作一份新的拷贝:

  if(!p.unique()){p.reset(new string(*p)); // 不是旧对象仅有的指针,分配新拷贝}*p += newVal; // 是旧对象仅有的指针,直接改变对象的值,因为不会再有别的指针访问旧对象

处理异常

当处理异常时,经常会使程序块过早结束,也就是如果使用普通指针管理内存,可能在遇到detele之前推出程序块:

void f()
{int *ip = new int(2);// throw一个异常且在f中未被捕获delete ip; //没能正常退出因此无法调用本句释放内存
}

如果ip是shared_ptr类型则不会出现内存泄漏的情况,在程序块结束时,自动释放内存。



动态数组

概念

动态数组并不是数组类型。

new和delete运算符一次分配/释放一个对象,但某些应用需要一次为很多对象分配内存的功能。

  • 使用容器的类可以使用默认版本的拷贝、赋值和析构操作。
  • 分配动态数组的类必须定义自己版本的操作,在拷贝、复制以及销毁对象时管理所关联的内存。

new分配对象数组

两种方法

方法一:在类型名之后跟一对方括号,在其中指明要分配的对象的数目,方括号中的大小必须是整形,但不必是常量。

例如:

// 调用get_size确定分配多少个int
int *pia = new int[get_size()]; // pia指向第一个int

方法二:也可以用一个表示数组类型的类型别名来分配一个数组,这样,new表达式中就不需要方括号了:

typedef int arrT[10]; // arrT表示10个int的数组类型
int *p = new arrT; // 分配一个10个int的数组;p指向第一个int

但编译器在执行这个表达式时还是会用new[]:

int *p = new int[42];

new的返回值

当用new分配一个数组时,我们并未得到一个数组类型对象,而是得到一个数组元素类型的指针。因此:

  • 不能对动态数组调用begin或end。这些函数使用数组维度来返回指向首元素和尾后元素的指针。
  • 不能用范围for语句来处理动态数组中的元素

初始化

默认情况下,new分配的对象,不管是单个的还是数组中的,都是默认初始化的。也可以通过一对空括号进行值初始化:
在这里插入图片描述

以及提供一个元素初始化器的花括号列表:
在这里插入图片描述

  • 初始化器数目小于元素数目,剩余元素进行值初始化。
  • 初始化器数目大于元素数目,new表达式失败,不会分配任何内存。

new表达式失败时会抛出一个类型为bad_array_new_length的异常。类似bad_alloc,定义在头文件new中。

值得一提的是: 虽然我们用空括号对数组中元素进行值初始化,但不能在括号中给出初始化器,这意味着不能用auto分配数组。 因为auto是编译器根据初始化值来判断类型的,使用auto就必须有初始值,没有初始值(这里是初始化器)auto自然也就不可以用了。

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

虽然我们不能创建一个大小为0的静态数组对象,但当n等于0时,调用new[n]是合法的:
在这里插入图片描述
在这里插入图片描述
当我们用new分配一个大小为0的数组时,new返回一个合法的非空指针。此指针保证与new返回的其他任何指针都不相同。对于零长度的数组来说,此指针就像尾后指针一样,我们可以像使用尾后迭代器一样使用这个指针。可以用此指针进行比较操作,但此指针不能解引用——毕竟它不指向任何元素。


释放动态数组

为了释放动态数组,可以使用一种特殊的delete——在职阵前加上一个方括号对。
在这里插入图片描述
第二条语句销毁pa指向的数组中的元素,并释放对应的内存。数组中的元素按逆序销毁,即,最后一个元素首先被销毁,然后是倒数第二个,依此类推。

当我们释放一个指向数组的指针时,空方括号对是必需的:它指示编译器此指针指向一个对象数组的第一个元素。如果我们在delete一个指向数组的指针时忽略了方括号(或者在delete一个指向单一对象的指针时使用了方括号),其行为是未定义的。


动态数组和unique_ptr

在这里插入图片描述

  • 当一个unique_ptr指向一个数组时,我们不能使用点和箭头成员运算符。毕竟unique_ptr指向的是一个数组而不是单个对象,因此这些运算符是无意义的。
  • 我们可以使用下标运算符来访问数组中的元素

实例:
在这里插入图片描述
类型说明符中的方括号(<int[]>)指出up指向一个int数组而不是一个int。由于up指向一个数组,当up销毁它管理的指针时,会自动使用delete[]。


动态数组和shared_ptr

shared_ptr不直接支持管理动态数组。如果希望用shared_ptr管理,必须提供自己的删除器:
在这里插入图片描述
shared_ptr不直接支持动态数组管理这一特性会影响我们如何访问数组中的元素:
在这里插入图片描述
shared_ptr未定义下标运算符,而且智能指针类型不支持指针算术运算。 因此,为了访问数组中的元素,必须用get获取一个内置指针,然后用它来访问数组元素。



allocator类

new的局限性(使用allocator的原因)

  • new将内存分配和对象构造组合在了一起。
  • delete将对象析构和内存释放组合在了一起

上述特性在灵活性上是有一定局限性的。这样在分配单个对象时当然是好的,可以明确知道对象应该有什么值。但是分配大块内存时,我们希望将内存分配和对象构造分离。这意味着我们可以先分配大块内存,只有在真正需要时才执行对性的创建操作。

实例:
在这里插入图片描述

有如下问题:

  • 我们可能不需要n个string,可能只用到了少量的string。因此我们可能创建了一些永远也用不到的对象。
  • 对于确实需要使用的对象,每个都被赋值了两次:第一次是在默认初始化时,第二次是在赋值时。
  • 没有默认构造函数的类就不能动态分配数组了。

概念即创建销毁操作

  • allocator定义在头文件memory中。

在这里插入图片描述

  • allocator分配的内存是未构造的。还未构造对象的情况下就是用原始内存是错误的。

construct

构造对象是通过construct完成的:

  • construct成员函数接受一个指针和零个或多个额外参数, 在给定位置构造一个元素。
  • 额外参数用来初始化构造的对象。类似make_shared的参数,这些额外参数必须是与构造的对象的类型相匹配的合法的初始化器:
allocator<string> alloc;
auto const p = alloc.allocate(20);
auto q = p; 
// q指向最后构造的元素之后的位置
// p指向分配的内存的首地址
alloc.construct(q++, 5, 'x'); // *p为xxxxx	
cout << *p << endl; // 正确:使用string的输出运算符
cout << *q << endl; // 灾难:q指向未构造的内存

为了理解上面的代码,用下面的代码查看一下构造对象前后p和q分别指向的地址:
在这里插入图片描述
可以看到,p一直指向分配的内存的首地址,q指向最后构造的元素之后的地址,因此 *p 可以访问已经构造的对象;而 *p 访问的是未构造对象的原始内存,这种行为是错误的。


destroy

用完对象后,必须对每个构造的元素调用destory来销毁它们。

destory接受一个指针,对指向的对象执行析构函数:

while (q != p) {alloc.destroy(--q); // 释放我们真正构造的string
}

不妨来查看一下执行上述代码之后的地址指向情况即内存分配的对象的值:
在这里插入图片描述
可以看到,执行完while之后,q指向的地址已经和p一样了,而再访问p中的对象——执行*p也无法输出”xxxxx“了。

但是在atom里面尝试运行的时候发现和预期的不一样。。。。destroy之后解引用p仍然能得到”xxxxx“:
在这里插入图片描述
(吐槽:同一段代码在不同的编译器上得到的结果不同,猜测可能是底层的编译环境不同导致的。 emmmmm……还是更倾向于相信vs的运行结果,如果有大佬看到这个问题知道原因的话,请不吝赐教,孩子实在不知道为什么会这样。)

  1. 我们只能对真正构造了的元素进行destory操作。
  2. 一旦元素被销毁后,可以重新使用这部分内存来保存其他的string,也可将内存归还给系统。

deallocate

释放内存通过deallocate来完成:

alloc.deallocate(p, 20);
  • 传递给deallocate的指针不能为空,必须指向由allocate分配的内存。
  • 传递给deallocate的大小参数必须与调用allocate分配内存时提供的大小参数具有一样的值。

两个伴随算法

  • 用来初始化内存中创建的对象

在这里插入图片描述
实例:
在这里插入图片描述

  • uninitialized_copy返回递增后的目的位置迭代器,指向最后一个构造元素之后的位置。


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

大多数类中,分配的资源都与对应对象生存期一致。 例如:每个vector(对象)“拥有”其自己的元素(分配的资源)。当我们拷贝一个vector时,原vector和副本vector中的元素是相互分离的。

某些类分配的资源具有与原对象相独立的生存期(可能一个资源被两个对象共同引用)。 换言之,如果两个对象共享底层的数据,当某个对象被销毁时,我们不能单方面地销毁底层数据。

实例

构建一个类A,用share_ptr管理vector<string>

#pragma once
#include <vector>
#include <string>
#include <memory>using namespace std;class A{
public:A(): vs(make_shared<vector<string>>()){} // 分配一个空的vectorA(initializer_list<string> il): vs(make_shared<vector<string>>(il)){}// 接受一个初始化器的花括号列表,将il当作make_shared的参数初始化vs// 通过调用底层vector的成员函数来完成size、empty、push_back、pop_backvector<string>::size_type size() const{return vs->size();}bool empty() const{return vs->empty();}void push_back(const string& s){vs->push_back(s);}// pop_back、front、back操作需要先检查操作对象是否为空void pop_back(){check(0, "pop_back on empty A");vs->pop_back();}string& front(){check(0, "front on empty A");return vs->front();}string& back(){check(0, "back on empty A");return vs->back();}// 针对const的A对象的front和back函数重载const string& front() const;const string& back() const;
private:shared_ptr<vector<string>> vs;// check函数提供判空功能,如果操作对象为空抛出一个异常void check(vector<string>::size_type si, const string &s) const{if(si >= vs->size()){throw out_of_range(s);}}
};const string& A::front() const{check(0, "front on empty A");return vs->front();
}
const string& A::back() const{check(0, "back on empty A");return vs->back();
}

用一个简单的A的使用程序。测试类的正确性:

#include <iostream>using namespace std;#include "my_A.h"int main(int argc, char const *argv[]) {A a1;{A a2 = {"a", "an", "the"};a1 = a2;a2.push_back("about");cout << a2.size() << endl;}cout << a1.size() << endl;cout << a1.front() << " " << a1.back() << endl;const A a3 = a1;cout << a3.front() << " " << a3.back() << endl;return 0;
}

输出结果:
在这里插入图片描述



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

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

相关文章

动态数组的简单应用

文章目录连接两个字符串字面常量题目注意代码输出结果处理输入的变长字符串题目注意代码连接两个字符串字面常量 题目 连接两个字符串字面常量&#xff0c;将结果保存在一个动态分配的char数组中。重写&#xff0c;连接两个标准库string对象。 注意 使用头文件cstring的str…

二分查找算法实现

文章目录思路代码以二分区间作为while判定条件以给定值作为while判定条件主函数思路 while判定条件的选择&#xff0c;注意最外层的return的内容就好。下面提供了两个代码版本。计算 mid 时需要防止溢出&#xff08;对应类型【如本例中的int】可能存不下&#xff09;&#xff…

根据中序、前序遍历重建二叉树

文章目录题目递归思路细节易错代码复杂度分析迭代思路细节易错代码复杂度分析题目 输入某二叉树的前序遍历和中序遍历的结果&#xff0c;请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。 例如&#xff0c;给出 前序遍历 preorder [3,9,20,15,7] 中…

深搜+剪枝

文章目录题目思路注意代码复杂度分析题目 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 单词必须按照字母顺序&#xff0c;通过相邻的单元格内的字母构成&#xff0c…

搜索+回溯问题(DFS\BFS详解)

文章目录题目思路DFS思路代码复杂度分析BFS思路代码复杂度分析题目 地上有一个m行n列的方格&#xff0c;从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动&#xff0c;它每次可以向左、右、上、下移动一格&#xff08;不能移动到方格外&#xff09;&am…

快速幂实现pow函数(从二分和二进制两种角度理解快速幂)

文章目录迭代实现快速幂思路int的取值范围快速幂从二进制的角度来理解从二分法的角度来理解代码复杂度分析进阶——超级次方思路倒序快速幂正序快速幂代码复杂度分析迭代实现快速幂 实现 pow(x, n) &#xff0c;即计算 x 的 n 次幂函数&#xff08;即&#xff0c;xn&#xff0…

n位数的全排列(需要考虑大数的情况)

文章目录题目思路代码题目 输入数字 n&#xff0c;按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3&#xff0c;则打印出 1、2、3 一直到最大的 3 位数 999。 示例 1: 输入: n 1 输出: [1,2,3,4,5,6,7,8,9] 说明&#xff1a; 用返回一个整数列表来代替打印 n 为正整数 …

正则表达式匹配(动规)

文章目录题目思路转移方程特征再探 i 和 j代码题目 请实现一个函数用来匹配包含 . 和 * 的正则表达式。模式中的字符 . 表示任意一个字符&#xff0c;而 * 表示它前面的字符可以出现任意次&#xff08;含0次&#xff09;。在本题中&#xff0c;匹配是指字符串的所有字符匹配整…

在循环递增一次的数组中插入元素

文章目录题目思路如何建立左右区间&#xff1f;如何查找最高点&#xff1f;那我们怎么判断 num 到底处于什么样的位置呢&#xff1f;如何确定插入位置&#xff1f;插入元素代码题目 给一个只循环递增一次的数组 res&#xff0c;res 满足首元素大于等于尾元素&#xff0c;形如&…

表示数值的字符串(有限状态自动机与搜索)

文章目录题目思路一代码一思路二代码二题目 思路一 考察有限状态自动机&#xff08;参考jyd&#xff09;&#xff1a; 字符类型&#xff1a; 空格 「 」、数字「 0—9 」 、正负号 「 」 、小数点 「 . 」 、幂符号 「 eE 」 。 状态定义&#xff1a; 按照字符串从左到右的…

树的子结构

文章目录题目深搜深搜代码广搜广搜代码题目 输入两棵二叉树A和B&#xff0c;判断B是不是A的子结构。(约定空树不是任意一个树的子结构) B是A的子结构&#xff0c; 即 A中有出现和B相同的结构和节点值。 例如: 给定的树 A: 给定的树 B&#xff1a; 返回 true&#xff0c;因为…

写题过程中碰见的小问题

文章目录和--vector二维vector的初始化数组中最大的数max_element()数组所有元素之和accumulate()vector数组去重对pair类型的vector排序对元素都为正整数的vector利用sort默认的升序排列进行降序排列一维数组转二维数组size_t和int如何不用临时变量交换两个数?将类函数的形参…

LeetCode——二叉树序列化与反序列化

文章目录题目思路问题一问题二代码实现题目 请实现两个函数&#xff0c;分别用来序列化和反序列化二叉树。 设计一个算法来实现二叉树的序列化与反序列化。不限定序列 / 反序列化算法执行逻辑&#xff0c;你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序…

jsp中生成的验证码和存在session里面的验证码不一致的处理

今天在调试项目的时候发现&#xff0c;在提交表单的时候的验证码有问题&#xff0c;问题是这样的&#xff1a;就是通过debug模式查看得知&#xff1a;jsp页面生成的验证码和表单输入的页面输入的一样&#xff0c;但是到后台执行的时候&#xff0c;你会发现他们是不一样的&#…

求1~n这n个整数十进制表示中1出现的次数

文章目录题目思路代码复杂度分析题目 输入一个整数 n &#xff0c;求1&#xff5e;n这n个整数的十进制表示中1出现的次数。 例如&#xff0c;输入12&#xff0c;那么1&#xff5e;12这些整数中包含1 的数字有1、10、11和12。可得1一共出现了5次。 思路 将个位、十位……每位…

求数字序列中的第n位对应的数字

文章目录题目思路代码复杂度分析致谢题目 数字以0123456789101112131415…的格式序列化到一个字符序列中。在这个序列中&#xff0c;第5位&#xff08;从下标0开始计数&#xff09;是5&#xff0c;第13位是1&#xff0c;第19位是4&#xff0c;等等。 请写一个函数&#xff0c…

一学就废的归并排序

文章目录其他与排序有关的文章原理代码实现复杂度分析其他与排序有关的文章 一学就废的三种简单排序【冒泡、插入、选择】 原理 归并排序&#xff08;Merge sort&#xff09;&#xff1a; 归并排序对元素 递归地 进行 逐层折半分组&#xff0c;然后从最小分组开始&#xff0c…

树状数组的相关知识 及 求逆序对的运用

文章目录树状数组概念前缀和和区间和树状数组原理区间和——单点更新前缀和——区间查询完整代码离散化sort函数unique函数去重erase函数仅保留不重复元素通过树状数组求逆序对树状数组概念 树状数组又名二叉索引树&#xff0c;其查询与插入的复杂度都为 O(logN)&#xff0c;其…

二叉搜索树相关知识及应用操作

文章目录概念查找二叉搜索树的第k大节点概念 二叉查找树&#xff08;Binary Search Tree&#xff09;&#xff0c;&#xff08;又名&#xff1a;二叉搜索树&#xff0c;二叉排序树&#xff09;——它或者是一棵空树&#xff0c;或者是具有下列性质的二叉树&#xff1a; 若它的…

二叉树相关知识及求深度的代码实现

文章目录树二叉树满二叉树和完全二叉树二叉树的性质代码实现求二叉树的深度树 树是一种非线性的数据结构&#xff0c;它是由n个有限结点组成一个具有层次关系的集合。 树的相关名词&#xff1a; 根节点&#xff1a;没有前驱结点的结点。父节点&#xff0c;子节点&#xff1a…