23-6 容器类
现实生活中,我们时刻都在使用容器。早餐麦片装在盒子里,书页被封面和装订固定在一起,车库里的物品也存放在各种容器中。若没有容器,处理这些物品将极其不便。试想阅读一本没有装订的书,或不使用碗直接吃盒装麦片——场面将一片狼藉。容器的核心价值在于其组织和存储内部物品的能力。
同理,容器类是专门设计用于容纳并组织其他类型(另一类或基本类型)多个实例的类。容器类种类繁多,每种都具备不同的优势、劣势及使用限制。编程中最常用的容器当属数组,你已见过许多相关示例。尽管C++内置数组功能,但程序员常选择数组容器类(如std::array或std::vector),因其能提供额外优势。与内置数组不同,数组容器类通常支持动态调整大小(当元素增删时)、在传递给函数时记住其大小,并执行边界检查。这不仅使数组容器类比普通数组更便捷,也更安全。
容器类通常实现一套标准化的基础功能集。大多数规范容器都包含以下操作:
- 通过构造函数创建空容器
- 向容器插入新对象
- 从容器移除对象
- 报告容器当前对象数量
- 清空容器内所有对象
- 访问存储对象
- 排序元素(可选)
某些容器类可能省略部分功能。例如数组容器常省略插入和移除函数,因其效率低下且设计者不鼓励使用。
容器类实现成员关系。例如数组元素是数组的成员(属于数组)。需注意此处“成员”采用常规含义,而非C++类成员的特定含义。
容器类型
容器类通常分为两种类型。值容器Value containers是存储其持有对象副本的组合体(因此负责创建和销毁这些副本)。引用容器是存储指向其他对象的指针或引用的聚合体(因此不负责创建或销毁这些对象)。
与现实中容器可容纳任意类型对象不同,C++中的容器通常仅存储单一类型数据。例如,整型数组仅能存储整数。与某些语言不同,多数C++容器不支持随意混合类型。若需同时存储整数和双精度数,通常需编写两个独立容器(或使用模板——这是C++的高级特性)。尽管存在使用限制,容器仍极具实用价值,能显著提升编程的便捷性、安全性与效率。
数组容器类
在此示例中,我们将从零开始编写一个整型数组类,实现容器应具备的大部分常用功能。该数组类将作为值容器,存储其所组织元素的副本。顾名思义,该容器将持有整型数组,类似于 std::vector
首先创建 IntArray.h 文件:
#ifndef INTARRAY_H
#define INTARRAY_H
class IntArray
{
};
#endif
我们的IntArray需要同时记录两个值:数据本身和数组的大小。由于我们希望数组能够动态调整大小,因此必须采用动态分配机制,这意味着需要使用指针来存储数据。
#ifndef INTARRAY_H
#define INTARRAY_H
class IntArray
{
private:
int m_length{};
int* m_data{};
};
#endif
现在我们需要添加一些构造函数,以便能够创建 IntArray 数组。我们将添加两个构造函数:一个用于构造空数组,另一个用于构造预先确定大小的数组。
#ifndef INTARRAY_H
#define INTARRAY_H
#include <cassert> // for assert()
#include <cstddef> // for std::size_t
class IntArray
{
private:
int m_length{};
int* m_data{};
public:
IntArray() = default;
IntArray(int length):
m_length{ length }
{
assert(length >= 0);
if (length > 0)
m_data = new int[static_cast<std::size_t>(length)]{};
}
};
#endif
我们还需要一些函数来帮助清理 IntArrays。首先,我们将编写一个析构函数,它仅需释放所有动态分配的数据。其次,我们将编写一个名为 erase() 的函数,该函数将清除数组并将其长度设置为 0。
~IntArray()
{
delete[] m_data;
// we don't need to set m_data to null or m_length to 0 here, since the object will be destroyed immediately after this function anyway
}
void erase()
{
delete[] m_data;
// We need to make sure we set m_data to nullptr here, otherwise it will
// be left pointing at deallocated memory!
m_data = nullptr;
m_length = 0;
}
现在让我们重载[]运算符,以便访问数组元素。我们需要确保索引参数具有有效值,可通过使用assert()函数实现。同时将添加一个access函数来返回数组长度。以下是当前代码:
#ifndef INTARRAY_H
#define INTARRAY_H
#include <cassert> // for assert()
#include <cstddef> // for std::size_t
class IntArray
{
private:
int m_length{};
int* m_data{};
public:
IntArray() = default;
IntArray(int length):
m_length{ length }
{
assert(length >= 0);
if (length > 0)
m_data = new int[static_cast<std::size_t>(length)]{};
}
~IntArray()
{
delete[] m_data;
// we don't need to set m_data to null or m_length to 0 here, since the object will be destroyed immediately after this function anyway
}
void erase()
{
delete[] m_data;
// We need to make sure we set m_data to nullptr here, otherwise it will
// be left pointing at deallocated memory!
m_data = nullptr;
m_length = 0;
}
int& operator[](int index)
{
assert(index >= 0 && index < m_length);
return m_data[index];
}
int getLength() const { return m_length; }
};
#endif
至此,我们已拥有可直接使用的IntArray类。它支持按指定大小分配数组,并能通过[]运算符读取或修改元素值。
然而,当前的IntArray仍存在若干局限:无法调整数组大小,无法插入或删除元素,也无法进行排序。复制数组时会引发问题,因为这仅会浅拷贝数据指针。
首先编写可调整数组大小的代码。为此将实现两个不同函数:reallocate() 函数在调整大小时会销毁数组中所有现有元素,但速度较快;resize() 函数在调整大小时会保留数组中所有现有元素,但速度较慢。
#include <algorithm> // for std::copy_n
// reallocate resizes the array. Any existing elements will be destroyed. This function operates quickly.
void reallocate(int newLength)
{
// First we delete any existing elements
erase();
// If our array is going to be empty now, return here
if (newLength <= 0)
return;
// Then we have to allocate new elements
m_data = new int[static_cast<std::size_t>(newLength)];
m_length = newLength;
}
// resize resizes the array. Any existing elements will be kept. This function operates slowly.
void resize(int newLength)
{
// if the array is already the right length, we're done
if (newLength == m_length)
return;
// If we are resizing to an empty array, do that and return
if (newLength <= 0)
{
erase();
return;
}
// Now we can assume newLength is at least 1 element. This algorithm
// works as follows: First we are going to allocate a new array. Then we
// are going to copy elements from the existing array to the new array.
// Once that is done, we can destroy the old array, and make m_data
// point to the new array.
// First we have to allocate a new array
int* data{ new int[static_cast<std::size_t>(newLength)] };
// Then we have to figure out how many elements to copy from the existing
// array to the new array. We want to copy as many elements as there are
// in the smaller of the two arrays.
if (m_length > 0)
{
int elementsToCopy{ (newLength > m_length) ? m_length : newLength };
std::copy_n(m_data, elementsToCopy, data); // copy the elements
}
// Now we can delete the old array because we don't need it any more
delete[] m_data;
// And use the new array instead! Note that this simply makes m_data point
// to the same address as the new array we dynamically allocated. Because
// data was dynamically allocated, it won't be destroyed when it goes out of scope.
m_data = data;
m_length = newLength;
}
呼!这有点棘手!
我们还需添加复制构造函数和赋值运算符,以便复制数组。
IntArray(const IntArray& a): IntArray(a.getLength()) // use normal constructor to set size of array appropriately
{
std::copy_n(a.m_data, m_length, m_data); // copy the elements
}
IntArray& operator=(const IntArray& a)
{
// Self-assignment check
if (&a == this)
return *this;
// Set the size of the new array appropriately
reallocate(a.getLength());
std::copy_n(a.m_data, m_length, m_data); // copy the elements
return *this;
}
许多数组容器类到此为止就结束了。不过,为了让你了解插入和删除功能的实现方式,我们还将继续编写这些功能。这两种算法都与 resize() 非常相似。
void insertBefore(int value, int index)
{
// Sanity check our index value
assert(index >= 0 && index <= m_length);
// First create a new array one element larger than the old array
int* data{ new int[static_cast<std::size_t>(m_length+1)] };
// Copy all of the elements up to the index
std::copy_n(m_data, index, data);
// Insert our new element into the new array
data[index] = value;
// Copy all of the values after the inserted element
std::copy_n(m_data + index, m_length - index, data + index + 1);
// Finally, delete the old array, and use the new array instead
delete[] m_data;
m_data = data;
++m_length;
}
void remove(int index)
{
// Sanity check our index value
assert(index >= 0 && index < m_length);
// If this is the last remaining element in the array, set the array to empty and bail out
if (m_length == 1)
{
erase();
return;
}
// First create a new array one element smaller than the old array
int* data{ new int[static_cast<std::size_t>(m_length-1)] };
// Copy all of the elements up to the index
std::copy_n(m_data, index, data);
// Copy all of the values after the removed element
std::copy_n(m_data + index + 1, m_length - index - 1, data + index);
// Finally, delete the old array, and use the new array instead
delete[] m_data;
m_data = data;
--m_length;
}
以下是我们的 IntArray 容器类完整代码。
IntArray.h:
#ifndef INTARRAY_H
#define INTARRAY_H
#include <algorithm> // for std::copy_n
#include <cassert> // for assert()
#include <cstddef> // for std::size_t
class IntArray
{
private:
int m_length{};
int* m_data{};
public:
IntArray() = default;
IntArray(int length):
m_length{ length }
{
assert(length >= 0);
if (length > 0)
m_data = new int[static_cast<std::size_t>(length)]{};
}
~IntArray()
{
delete[] m_data;
// we don't need to set m_data to null or m_length to 0 here, since the object will be destroyed immediately after this function anyway
}
IntArray(const IntArray& a): IntArray(a.getLength()) // use normal constructor to set size of array appropriately
{
std::copy_n(a.m_data, m_length, m_data); // copy the elements
}
IntArray& operator=(const IntArray& a)
{
// Self-assignment check
if (&a == this)
return *this;
// Set the size of the new array appropriately
reallocate(a.getLength());
std::copy_n(a.m_data, m_length, m_data); // copy the elements
return *this;
}
void erase()
{
delete[] m_data;
// We need to make sure we set m_data to nullptr here, otherwise it will
// be left pointing at deallocated memory!
m_data = nullptr;
m_length = 0;
}
int& operator[](int index)
{
assert(index >= 0 && index < m_length);
return m_data[index];
}
// reallocate resizes the array. Any existing elements will be destroyed. This function operates quickly.
void reallocate(int newLength)
{
// First we delete any existing elements
erase();
// If our array is going to be empty now, return here
if (newLength <= 0)
return;
// Then we have to allocate new elements
m_data = new int[static_cast<std::size_t>(newLength)];
m_length = newLength;
}
// resize resizes the array. Any existing elements will be kept. This function operates slowly.
void resize(int newLength)
{
// if the array is already the right length, we're done
if (newLength == m_length)
return;
// If we are resizing to an empty array, do that and return
if (newLength <= 0)
{
erase();
return;
}
// Now we can assume newLength is at least 1 element. This algorithm
// works as follows: First we are going to allocate a new array. Then we
// are going to copy elements from the existing array to the new array.
// Once that is done, we can destroy the old array, and make m_data
// point to the new array.
// First we have to allocate a new array
int* data{ new int[static_cast<std::size_t>(newLength)] };
// Then we have to figure out how many elements to copy from the existing
// array to the new array. We want to copy as many elements as there are
// in the smaller of the two arrays.
if (m_length > 0)
{
int elementsToCopy{ (newLength > m_length) ? m_length : newLength };
std::copy_n(m_data, elementsToCopy, data); // copy the elements
}
// Now we can delete the old array because we don't need it any more
delete[] m_data;
// And use the new array instead! Note that this simply makes m_data point
// to the same address as the new array we dynamically allocated. Because
// data was dynamically allocated, it won't be destroyed when it goes out of scope.
m_data = data;
m_length = newLength;
}
void insertBefore(int value, int index)
{
// Sanity check our index value
assert(index >= 0 && index <= m_length);
// First create a new array one element larger than the old array
int* data{ new int[static_cast<std::size_t>(m_length+1)] };
// Copy all of the elements up to the index
std::copy_n(m_data, index, data);
// Insert our new element into the new array
data[index] = value;
// Copy all of the values after the inserted element
std::copy_n(m_data + index, m_length - index, data + index + 1);
// Finally, delete the old array, and use the new array instead
delete[] m_data;
m_data = data;
++m_length;
}
void remove(int index)
{
// Sanity check our index value
assert(index >= 0 && index < m_length);
// If this is the last remaining element in the array, set the array to empty and bail out
if (m_length == 1)
{
erase();
return;
}
// First create a new array one element smaller than the old array
int* data{ new int[static_cast<std::size_t>(m_length-1)] };
// Copy all of the elements up to the index
std::copy_n(m_data, index, data);
// Copy all of the values after the removed element
std::copy_n(m_data + index + 1, m_length - index - 1, data + index);
// Finally, delete the old array, and use the new array instead
delete[] m_data;
m_data = data;
--m_length;
}
// A couple of additional functions just for convenience
void insertAtBeginning(int value) { insertBefore(value, 0); }
void insertAtEnd(int value) { insertBefore(value, m_length); }
int getLength() const { return m_length; }
};
#endif
现在,让我们测试一下,以证明它确实有效:
#include <iostream>
#include "IntArray.h"
int main()
{
// Declare an array with 10 elements
IntArray array(10);
// Fill the array with numbers 1 through 10
for (int i{ 0 }; i<10; ++i)
array[i] = i+1;
// Resize the array to 8 elements
array.resize(8);
// Insert the number 20 before element with index 5
array.insertBefore(20, 5);
// Remove the element with index 3
array.remove(3);
// Add 30 and 40 to the end and beginning
array.insertAtEnd(30);
array.insertAtBeginning(40);
// A few more tests to ensure copy constructing / assigning arrays
// doesn't break things
{
IntArray b{ array };
b = array;
b = b;
array = array;
}
// Print out all the numbers
for (int i{ 0 }; i<array.getLength(); ++i)
std::cout << array[i] << ' ';
std::cout << '\n';
return 0;
}
这产生了以下结果:

尽管编写容器类可能相当复杂,但好消息是只需编写一次。一旦容器类正常运行,您就可以随心所欲地使用和复用它,无需额外编程工作。
可改进之处:
- 模板化设计:应将其设计为模板类,使之支持任意可复制类型而非仅限整型。
- 常量支持:需为各成员函数添加常量重载版本,以正确支持常量数组。
- 移动语义支持:应通过添加移动构造函数和移动赋值操作符实现移动语义。
- 优化操作效率:执行调整大小或插入操作时,可采用元素移动替代复制。
进阶读者参考:
与异常处理相关的进阶改进:
- 执行调整大小或插入操作时,仅当元素的移动构造函数为noexcept时才进行移动,否则采用复制(27.10节——std::move_if_noexcept)。
- 为调整大小或插入操作提供强异常安全保证(27.9节——异常规范与noexcept)。
最后补充: 若标准库中的类能满足需求,请优先使用而非自行创建。例如,比起使用 IntArray,更推荐使用 std::vector

浙公网安备 33010602011771号