C++ STL 中 deque::operator= 和 deque::operator[] 之间的区别

2025年5月13日 | 阅读17分钟

标准模板库(STL)是现代C++软件开发的核心部分,它提供了一组强大、有用、通用的数据结构来简化开发。在各种STL容器中,std::deque(双端队列的缩写)是一种特别高效且灵活的序列元素处理方式。std::deque是一个快速随机访问的数据结构,它提供动态重排大小以及在两端高效插入和删除元素的能力,使其兼具vector和链表的特点。结合这些特性,它成为寻求速度和灵活性的开发者的必备选择。

与deque相关的两个关键运算符是deque的赋值运算符(operator=)和下标运算符(operator[])。这两个运算符在实现的功能上大相径庭,但它们都是有效使用deque所必需的。在本文中,我们将讨论C++中deque::operator=deque::operator[]之间的区别。在讨论它们的区别之前,我们必须先了解C++中的std::deque。

什么是std::deque?

在深入探讨这些运算符的具体细节之前,了解std::deque的结构和行为会很有帮助。与std::vector不同,std::deque将其元素组织成一个分段结构,由固定大小的块链接在一起。std::vector将元素存储在一整块连续内存中。std::deque允许在两端高效地增长,而无需进行大规模的内存重新分配。其结果是一个容器,提供了

  • 两端高效的插入/删除: push_frontpush_back操作速度很快,因此std::deque非常适合用作缓冲区、队列和堆栈等用例。
  • 随机访问: std::deque支持通过索引在常数时间内访问元素,类似于std::vector。
  • 动态重排大小: 会自动添加新元素,使容器调整其大小。

运算符在std::deque中的作用

STL容器包含运算符,它们具有精妙的语法和自动行为。

deque::operator=(赋值运算符): 用于为deque分配新内容。它支持

  • 将一个deque的内容复制到另一个deque。通过移动语义在deque之间传输资源。
  • 我使用它来快速将值传递给初始化列表,使代码更易读懂。

deque::operator[] (下标运算符): 允许deque提供对其元素的随机访问。它以类似数组的直观语法读取或修改一个元素。

这些运算符的必要性?

C++ STL 的 std::deque 容器是一个方便的数据结构,当我们需要动态调整数据大小、在数据两端快速插入或删除,或执行随机访问时非常有用。其功能基于两个基本运算符(operator=、operator[]),它们决定和访问容器数据。

  • 更好的代码和更清晰的代码
    通过实践,开发者可以编写简洁明了的代码。赋值运算符可以轻松地从副本、移动操作或初始化列表中的已分配值来更新deque的内容(及其内容)。它允许我们像直接访问数组一样访问元素,使其易于读取或修改数据,而无需担心开销。
  • 避免常见陷阱
    理解这些运算符的能力有助于开发者避免常见问题。上面的例子展示了如果赋值运算符没有使用移动语义正确实现,可能会发生低效的复制。同样,如果 operator[] 用于访问超出有效范围的元素,它也会导致问题,因为它不执行边界检查,从而导致未定义行为。
  • 最大化std::deque的潜力
    这些运算符可以解锁std::deque的全部潜力,让您在多个应用领域利用其强大功能。

1. deque::operator=

C++标准模板库(STL)中,std::deque的赋值运算符(operator=)为我们提供了一种简单高效的方式来更改deque容器的内容。它是用新元素重新分配deque的必备运算符,但会完全替换其当前内容,并确保容器根据新数据更新其大小。

deque::operator=让我们能够毫无问题地将一个deque的内容复制到另一个deque。deque还支持移动语义,它允许通过复制一个deque的资源到另一个deque来高效地重新分配,而无需额外复制底层数据。特别是在性能要求极高的应用程序中,避免不必要的复制变得至关重要。

除了复制和移动之外,这两种形式还接受来自初始化列表的赋值,该功能在C++11中引入。它允许以非常简短易读的方式从预定义值的列表中声明性地构造deque,摆脱了使用现代C++对容器进行常规重新分配的麻烦,使过程更加简洁。

deque::operator= 的复杂性很重要,因为它允许高效地处理各种赋值场景。该运算符提供了一种简单灵活的方式,可以将一个std::deque的内容复制或转移到另一个std::deque。由于它是处理容器的基本操作,因此理解deque::operator= 对于在实际编程任务中有效使用std::deque至关重要。

std::deque中的复制赋值

std::deque的复制赋值运算符在另一个deque中创建一个deque内容的独立副本。该运算符会将源deque的所有元素分配给目标deque,这意味着目标deque将包含与源deque相同的元素,顺序也相同。操作后,目标deque的内容将被覆盖,大小等于源deque,deque将调整为包含该数量的元素。

关键属性

  • 此操作在两个独立的deque对象之间创建深拷贝。当一个deque被修改时,它不会影响另一个。
  • 对于小到中等大小的deque,它是一个高效的复制赋值运算符,但当deque很大时,由于元素级别的复制,它会很昂贵。

用例

  • 如果我们想创建一个独立的deque副本而不与原始副本有任何引用关系,那么复制赋值正是我们想要的。例如,当备份deque或使用不应影响原始副本的算法时,它很有帮助。

std::deque中的移动赋值

在std::deque的移动赋值运算符中,一个deque可以将其中元素的“内容”转移到另一个deque。它将资源从源deque移动到目标deque,因此在操作后,原始源deque处于有效但未指定的(unspecified)状态。它通常是空的,但请注意,这是未指定的。

关键属性

  • 高效传输: 移动赋值将内部资源(即内存和数据)的所有权从源转移到目标。它比复制赋值快得多,并且不涉及元素级别的复制。
  • 源状态: 移动赋值后,源deque处于有效但未指定的状态。通常是空的,但内容可能已被“移走”,没有特定的顺序,也无数据保证。
  • 无重新分配: 因此,资源已移动,所以选择器不为目标deque的元素重新分配内存。没有数据被传输。它只是指向旧的工作区。

用例

移动赋值最适合以下情况

  • 转移所有权:如果您想在不创建额外副本的情况下,快速地将临时deque或未使用deque的内容移动到另一个deque,则可以转移所有权以最大化性能。
  • 性能关键型操作:如果我们有一个正在处理的大型deque,复制操作会很昂贵,但转移后我们不再需要原始容器,那么移动赋值非常方便。

std::deque中的初始化列表赋值

可以使用初始化列表({}中的值列表)赋值运算符为deque赋值。C++11引入了这个运算符,它使得对deque的赋值或修改更加简洁易读。

关键属性

  • 直接初始化:初始化列表的内容将替换deque中的内容,deque将直接从列表中填充。
  • 效率:这是一个将许多值放入deque的好方法,但只有当值是预先知道的情况下才高效。

用例

初始化列表赋值最适合以下情况

  • 初始化容器:有时,我们希望以紧凑的方式创建具有特定值的deque。
  • 重新分配值:例如,我们需要用一组新值完全替换deque的元素,例如重新初始化或更改deque的内容。

std::deque中的自赋值检查

自赋值检查发生在deque被赋值给自己(源deque和目标deque是同一个对象)的任何时候。一般来说,将deque赋值给自己不会改变其内容或状态;一切都与之前相同。但是,赋值运算符必须小心处理自赋值,以免产生额外工作或导致错误。

关键属性

  • 无变化:通常,自赋值不起作用,因为deque被赋值给自己。deque的状态和内容在操作后不会改变。
  • 内部优化:std::deque和其他容器赋值运算符的现代实现会妥善处理自赋值,而不会引起内部内存重新分配或元素复制。
  • 异常安全性:可能抛出异常并因此破坏容器内部状态的赋值操作是不允许的,而自赋值检查永远不会允许这种情况发生。

用例

自赋值是某些情况下需要考虑的边缘情况

  • 边缘情况处理:如果代码中由于逻辑条件deque可能被重新赋值给自己,赋值运算符可确保不会执行多余的操作。
  • 优化:自赋值检查可以节省性能关键代码中的冗余操作,或者它们可能加剧性能瓶颈。

这些赋值操作中的每一种,如复制、移动、初始化列表等,都针对特定的情况和用法进行了优化。了解何时以及如何使用每个运算符可以大大提高管理std::deque容器的代码效率和正确性。

程序

让我们通过一个示例来说明C++中的std::deque::operator=

输出

 
The initial state of deque1 and deque2 before Copy Assignment:
deque1 = { 1 2 3 4 5 } (Size: 5)
deque2 = { } (Size: 0)
After Copy Assignment:
deque1 = { 1 2 3 4 5 } (Size: 5)
deque2 = { 1 2 3 4 5 } (Size: 5)
After modifying deque2 to ensure independence:
deque1 = { 1 2 3 4 5 } (Size: 5)
deque2 = { 99 2 3 4 5 } (Size: 5)
Initial state of deque3 and deque4 before Move Assignment:
deque3 = { 6 7 8 9 10 } (Size: 5)
deque4 = { } (Size: 0)
After Move Assignment:
deque3 = { } (Size: 0)
deque4 = { 6 7 8 9 10 } (Size: 5)
State of deque5 before Initializer List Assignment:
deque5 = { } (Size: 0)
After Initializer List Assignment:
deque5 = { 11 12 13 14 15 } (Size: 5)
After Re-assigning using Initializer List:
deque5 = { 16 17 18 } (Size: 3)
State of deque6 before Self-Assignment:
deque6 = { 19 20 21 } (Size: 3)
After Self-Assignment:
deque6 = { 19 20 21 } (Size: 3)
Initial state of deque7 and deque8 before Combining Operations:
deque7 = { 22 23 24 25 } (Size: 4)
deque8 = { } (Size: 0)
After Copy Assignment (deque8 = deque7):
deque7 = { 22 23 24 25 } (Size: 4)
deque8 = { 22 23 24 25 } (Size: 4)
After Move Assignment (deque7 = std::move(deque8)):
deque7 = { 22 23 24 25 } (Size: 4)
deque8 = { } (Size: 0)
After Initializer List Assignment (deque8 = {26, 27, 28}):
deque8 = { 26 27 28 } (Size: 3)
Final State of deque7 and deque8:
deque7 = { 22 23 24 25 } (Size: 4)
deque8 = { 26 27 28 } (Size: 3)   

解释

在第一部分,程序展示了将deque1的内容复制到deque2的复制赋值。deque1最初包含5个元素,而deque2为空。当deque2被复制赋值(deque2 = deque1)时,deque1的内容被简单地复制到deque2。但是,这意味着我们可以对deque2 = deque1执行操作,然后从任何点修改deque1;这不会改变deque2,因为它是独立的副本。程序验证了这种独立性,它修改了deque2中的一个元素;同时,deque1保持不变,这表明deque是相互独立的。

之后,程序展示了资源移动赋值。在这里,我们用元素填充deque3,但deque4最初是空的。当使用移动赋值(deque4 = std::move(deque3))将deque3的内容分配给deque4时,deque3中元素的归属权被转移到deque4。移动后,deque3变为空,因为它不再拥有这些元素。复制赋值非常低效。它们必须复制所有元素,而不是转移deque的所有内部资源(例如,内存指针)。

我们在程序中使用初始化列表赋值来为deque5分配一组新元素,然后展示初始化列表赋值。初始化列表(此处提供的元素列表)被提供给赋值(deque5 = {11, 12, 13, 14, 15}),并用其包含的元素替换deque5的原始内容。通过这种方式,如果事先知道要插入的确切值,我们可以轻松高效地为deque分配一组新值。

最后,程序展示了一个自赋值检查的例子,其中一个deque被赋值给自己。此操作可以防止在deque被赋值给自己时发生不必要的卡顿(确保不会发生重新分配或元素复制)。但是,实际上,赋值运算符会高效地处理自赋值,并且与标签不同,自赋值不会改变deque的状态。

2. deque::operator[]

std::deque::operator[]中,deque可以在给定索引处提供对其元素的直接、高效访问。Std::deque,一个双端队列,是一个序列容器,它可以在两端高效地插入和删除元素。当我们需要频繁地从前端或后端添加或删除元素时,它非常有用。operator[]是std::deque容器的一个关键特性,它允许开发人员明智地访问和修改容器内任意位置的元素。

就像数组索引运算符一样,它也是一个类似的运算符,允许对deque元素进行随机访问。它“返回”指定索引处的元素,这意味着它可以读写。提供的索引必须是有效索引,即它必须在0到deque.size() - 1之间。operator[]不像at()这样的其他成员函数那样执行边界检查,但与operator[]不同的是,这里进行的边界检查仅针对数组。它比以前更快,但在此过程中,如果使用无效索引,也会带来未定义行为的风险。

换句话说,通过索引访问deque中的任何元素都需要常数时间O(1),因此非常快。deque以块的形式存储其元素,并且该运算符允许直接访问这些块,确保快速访问,而与deque的大小无关。但是,没有自动检查越界访问,因此您需要确保索引在边界内。如果要安全地检查越界访问,应使用deque::at()方法,该方法会执行边界检查。

程序

让我们通过一个示例来说明C++中的std::deque::operator[]

输出

 
Original deque1:
deque1[0] = 10
deque1[1] = 20
deque1[2] = 30
deque1[3] = 40
deque1[4] = 50
After modifying deque1[2]:
deque1[0] = 10
deque1[1] = 20
deque1[2] = 35
deque1[3] = 40
deque1[4] = 50
Trying to access deque1[10] (out of bounds):
Const deque2 elements:
deque2[0] = 100
deque2[1] = 200
deque2[2] = 300
deque2[3] = 400
deque2[4] = 500
Modified deque3:
deque3[0] = 10
deque3[1] = 2
deque3[2] = 3
deque3[3] = 4
deque3[4] = 50
Large deque, first 5 elements:
largeDeque[0] = 0
largeDeque[1] = 10
largeDeque[2] = 20
largeDeque[3] = 30
largeDeque[4] = 40
All elements of largeDeque (first 10 shown for brevity):
largeDeque[0] = 0
largeDeque[1] = 10
largeDeque[2] = 20
largeDeque[3] = 30
largeDeque[4] = 40
largeDeque[5] = 50
largeDeque[6] = 60
largeDeque[7] = 70
largeDeque[8] = 80
largeDeque[9] = 90
Modified last element of largeDeque:
largeDeque[999] = 10000
Nested deque (accessing elements):
nestedDeque[1][2] = 6
After modification, nestedDeque[0][1] = 20   

解释

  1. 基本元素访问和修改
    我们首先创建一个名为deque1的deque<int>,其中包含五个整数。程序使用operator[]访问deque中的元素并打印它们。元素按顺序显示,索引为0。打印deque的内容,然后用deque1[2] = 35修改索引为2的元素。operator []展示了如何通过索引直接读取和写入元素。
  2. 越界元素
    例如,operator[]不执行边界检查,这是其工作的重要组成部分。在这里,本节中的程序试图访问deque1 [10],这是越界的。operator[]越界索引会导致未定义行为,如果我们在访问operator[]时不能确定我们的索引是否有效/正确,这可能导致代码崩溃。
  3. 常量deque访问
    我们初始化一个const deque<int>(deque2),程序展示了如何使用operator[]来访问const deque中的元素。std::deque2是const的,因此只能读取(不能写入)。代码展示了如何使用operator[]获取和打印const deque的元素,并且正如它所示,该运算符也可以用于对不可修改的deque进行读取操作。
  4. 修改非const deque中的元素。
    通过这一点,我们使用operator[]初始化并修改了第二个非const deque,deque3。deque3[0] = 10,deque3[4] = 50,然后打印deque3。它展示了operator []如何提供一种直接的方式来修改非const deque中的元素。
  5. 大型deque
    程序的一个好处是它向我们展示了如何使用operator[]处理一个包含1000个元素的巨大deque(largeDeque)。它打印出前5个元素,并修改最后一个元素。它展示了operator[]如何快速处理大型数据并在常数时间内访问元素。
  6. 嵌套deque
    NestedDeque是一个嵌套deque,其中deque中的一个元素存储另一个deque。通过单个operator[](读写操作)即可访问外部和底层deque的元素,从而展示了operator[]在复杂数据结构中的通用性。
  7. 访问空deque
    最后,它还展示了使用operator[]访问空deque的潜在陷阱。代码会发出警告,因为访问空deque中的任何元素都是未定义行为。

std::deque::operator[] 和 std::deque::operator= 在 C++ 中的主要区别

std::deque::operator[] 和 std::deque::operator= 在 C++ 中有几个主要区别。一些主要区别如下:

特点std::deque::operator[]std::deque::operator=
目的它提供对指定索引处元素的直接访问。它将一个deque的内容分配给另一个deque。
边界检查无边界检查;越界访问会导致未定义行为。它执行源deque的深拷贝,但不涉及边界检查。
时间复杂度O(1) - 对元素的常数时间访问。O(n) - 线性时间,其中 n 是正在复制的元素数量。
空间复杂度O(1) - 不使用额外空间(直接访问)。O(n) - 根据源deque的大小,为目标deque分配空间。
修改它适用于const和非const deque(在const deque中不能修改)。它用于赋值给非const deque(const deque不能被赋值)。