跳到主要内容

C++ 下标运算符重载

什么是下标运算符

在C++中,下标运算符[]是我们在使用数组时最常见的运算符之一。通过下标运算符,我们可以访问数组中的特定元素:

cpp
int numbers[5] = {10, 20, 30, 40, 50};
int third = numbers[2]; // 访问索引为2的元素,值为30

但是,当我们创建自己的类时,如果希望该类也能像数组一样使用下标访问元素,该怎么办呢?这时就需要用到下标运算符重载

为什么要重载下标运算符

重载下标运算符可以让我们的自定义类型实现类似数组的行为,使代码更直观、更易读。常见的应用场景包括:

  • 创建自定义容器类(如向量、矩阵、哈希表等)
  • 封装底层数组,提供更安全的访问方式
  • 实现特殊的索引行为,如负索引、多维索引等

下标运算符重载的语法

下标运算符[]的重载有以下几个特点:

  1. 必须作为成员函数重载
  2. 通常有两个版本:非常量版本和常量版本
  3. 返回类型通常是对元素的引用,以便支持修改操作

基本语法如下:

cpp
返回类型& operator[](参数类型 index);             // 非常量版本
const 返回类型& operator[](参数类型 index) const; // 常量版本
提示

提供常量版本的重载可以让常量对象也能使用下标运算符,但不能修改其中的元素。

简单的下标运算符重载示例

让我们创建一个简单的整数数组封装类:

cpp
#include <iostream>

class IntArray {
private:
int* data;
int size;

public:
// 构造函数
IntArray(int length) {
size = length;
data = new int[size];
for (int i = 0; i < size; i++) {
data[i] = 0;
}
}

// 析构函数
~IntArray() {
delete[] data;
}

// 非常量下标运算符重载
int& operator[](int index) {
if (index < 0 || index >= size) {
std::cout << "索引越界!" << std::endl;
// 返回第一个元素作为错误情况下的默认值
return data[0];
}
return data[index];
}

// 常量下标运算符重载
const int& operator[](int index) const {
if (index < 0 || index >= size) {
std::cout << "索引越界!" << std::endl;
return data[0];
}
return data[index];
}

// 获取数组大小
int getSize() const {
return size;
}
};

int main() {
IntArray arr(5);

// 使用重载的下标运算符赋值
for (int i = 0; i < arr.getSize(); i++) {
arr[i] = (i + 1) * 10;
}

// 使用重载的下标运算符读取值
for (int i = 0; i < arr.getSize(); i++) {
std::cout << "arr[" << i << "] = " << arr[i] << std::endl;
}

// 测试越界访问
std::cout << "越界访问: " << arr[10] << std::endl;

// 常量对象测试
const IntArray constArr(3);
std::cout << "常量数组元素: " << constArr[1] << std::endl;
// 下面这行会编译错误,因为constArr是常量对象
// constArr[1] = 100;

return 0;
}

输出结果:

arr[0] = 10
arr[1] = 20
arr[2] = 30
arr[3] = 40
arr[4] = 50
索引越界!
越界访问: 10
常量数组元素: 0

在这个例子中,我们实现了:

  1. 两个版本的下标运算符重载
  2. 索引检查以防止越界访问
  3. 对常量对象的支持

高级应用:自定义索引类型

下标运算符的参数不一定是整数类型,可以是任何类型。例如,我们可以创建一个使用字符串作为索引的简单字典类:

cpp
#include <iostream>
#include <string>
#include <map>

class Dictionary {
private:
std::map<std::string, std::string> data;

public:
// 非常量下标运算符重载
std::string& operator[](const std::string& key) {
return data[key];
}

// 常量下标运算符重载
const std::string& operator[](const std::string& key) const {
auto it = data.find(key);
if (it == data.end()) {
static std::string empty;
return empty;
}
return it->second;
}

// 检查键是否存在
bool hasKey(const std::string& key) const {
return data.find(key) != data.end();
}
};

int main() {
Dictionary dict;

// 使用重载的下标运算符添加/修改键值对
dict["apple"] = "苹果";
dict["banana"] = "香蕉";
dict["orange"] = "橙子";

// 使用重载的下标运算符获取值
std::cout << "apple 的中文是: " << dict["apple"] << std::endl;
std::cout << "banana 的中文是: " << dict["banana"] << std::endl;
std::cout << "orange 的中文是: " << dict["orange"] << std::endl;

// 修改值
dict["apple"] = "苹果果";
std::cout << "修改后,apple 的中文是: " << dict["apple"] << std::endl;

// 查询不存在的键
std::cout << "grape 的中文是: " << dict["grape"] << std::endl;
// 上面的操作会自动创建键 "grape" 并赋值为空字符串

return 0;
}

输出结果:

apple 的中文是: 苹果
banana 的中文是: 香蕉
orange 的中文是: 橙子
修改后,apple 的中文是: 苹果果
grape 的中文是:

实现多维数组访问

我们还可以重载下标运算符来实现多维数组的访问。例如,创建一个简单的矩阵类:

cpp
#include <iostream>
#include <vector>

class Matrix {
private:
std::vector<std::vector<double>> data;
int rows;
int cols;

public:
Matrix(int r, int c) : rows(r), cols(c) {
data.resize(rows);
for (int i = 0; i < rows; i++) {
data[i].resize(cols, 0.0);
}
}

// 返回行引用,允许使用第二个[]操作符
std::vector<double>& operator[](int row) {
if (row < 0 || row >= rows) {
std::cout << "行索引越界!" << std::endl;
return data[0];
}
return data[row];
}

// 常量版本
const std::vector<double>& operator[](int row) const {
if (row < 0 || row >= rows) {
std::cout << "行索引越界!" << std::endl;
return data[0];
}
return data[row];
}

// 打印矩阵
void print() const {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
std::cout << data[i][j] << " ";
}
std::cout << std::endl;
}
}
};

int main() {
Matrix mat(3, 4);

// 使用下标运算符设置值
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
mat[i][j] = i * 4 + j;
}
}

std::cout << "矩阵内容:" << std::endl;
mat.print();

// 使用下标运算符读取特定元素
std::cout << "mat[1][2] = " << mat[1][2] << std::endl;

return 0;
}

输出结果:

矩阵内容:
0 1 2 3
4 5 6 7
8 9 10 11
mat[1][2] = 6
警告

多维访问的实现中,我们的operator[]返回另一个可以使用[]的对象(在这个例子中是std::vector<double>),因此可以连续使用两个[]运算符。我们实际上并没有重载[][] 运算符,因为C++中不存在这样的运算符。

最佳实践与注意事项

在实现下标运算符重载时,请注意以下几点:

  1. 始终进行边界检查:防止非法访问内存
  2. 提供常量版本:允许常量对象使用下标运算符
  3. 返回引用:允许修改元素值
  4. 考虑抛出异常:对于严重的错误情况,比如越界访问,可以考虑抛出异常而不是返回默认值
  5. 避免副作用:下标运算符应该保持简单,避免复杂的副作用

应用实例:安全数组类

以下是一个更完整的安全数组类示例,演示了下标运算符重载的实际应用:

cpp
#include <iostream>
#include <stdexcept>

template <typename T>
class SafeArray {
private:
T* data;
int size;

public:
// 构造函数
SafeArray(int length) : size(length) {
if (size <= 0) {
throw std::invalid_argument("数组大小必须为正数");
}
data = new T[size]();
}

// 复制构造函数
SafeArray(const SafeArray& other) : size(other.size) {
data = new T[size];
for (int i = 0; i < size; i++) {
data[i] = other.data[i];
}
}

// 移动构造函数
SafeArray(SafeArray&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}

// 析构函数
~SafeArray() {
delete[] data;
}

// 赋值运算符
SafeArray& operator=(const SafeArray& other) {
if (this != &other) {
delete[] data;
size = other.size;
data = new T[size];
for (int i = 0; i < size; i++) {
data[i] = other.data[i];
}
}
return *this;
}

// 移动赋值运算符
SafeArray& operator=(SafeArray&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}

// 非常量下标运算符重载
T& operator[](int index) {
if (index < 0 || index >= size) {
throw std::out_of_range("数组索引越界");
}
return data[index];
}

// 常量下标运算符重载
const T& operator[](int index) const {
if (index < 0 || index >= size) {
throw std::out_of_range("数组索引越界");
}
return data[index];
}

// 获取数组大小
int getSize() const {
return size;
}
};

int main() {
try {
// 创建一个安全整数数组
SafeArray<int> numbers(5);

// 设置值
for (int i = 0; i < numbers.getSize(); i++) {
numbers[i] = (i + 1) * 100;
}

// 读取值
for (int i = 0; i < numbers.getSize(); i++) {
std::cout << "numbers[" << i << "] = " << numbers[i] << std::endl;
}

// 测试复制
SafeArray<int> copy = numbers;
copy[0] = 999;
std::cout << "原数组第一个元素: " << numbers[0] << std::endl;
std::cout << "复制数组第一个元素: " << copy[0] << std::endl;

// 尝试越界访问
std::cout << "尝试访问numbers[10]..." << std::endl;
int value = numbers[10]; // 这里会抛出异常

} catch (const std::exception& e) {
std::cerr << "捕获异常: " << e.what() << std::endl;
}

return 0;
}

输出结果:

numbers[0] = 100
numbers[1] = 200
numbers[2] = 300
numbers[3] = 400
numbers[4] = 500
原数组第一个元素: 100
复制数组第一个元素: 999
尝试访问numbers[10]...
捕获异常: 数组索引越界

这个例子展示了一个更完善的安全数组实现,包括:

  1. 异常处理以报告错误
  2. 完整的复制/移动构造函数和赋值运算符
  3. 模板类,支持任何数据类型
  4. 严格的边界检查

总结

下标运算符重载是C++中非常实用的特性,它使我们能够:

  1. 创建行为像数组的自定义类型
  2. 提供安全的索引访问机制
  3. 实现复杂的数据结构,如多维数组、映射等
  4. 提高代码的可读性和直观性

正确实现下标运算符重载时,应注意边界检查、常量对象支持、返回引用等最佳实践,以确保代码的安全性和灵活性。

练习题

  1. 创建一个CircularArray类,使用下标运算符重载实现循环数组(索引超出范围时自动回绕)
  2. 扩展SafeArray类,支持负索引(类似Python),如arr[-1]表示最后一个元素
  3. 实现一个简单的稀疏矩阵类,只存储非零元素,但通过下标运算符允许像普通矩阵一样访问
  4. 创建一个时间序列类,允许使用日期或时间戳作为索引访问数据点

进一步阅读

  • 《C++ Primer》第14章:重载运算与类型转换
  • 《Effective C++》条款21:必须返回引用的运算符
  • 《The C++ Programming Language》第18章:重载运算符