C++ 下标运算符重载
什么是下标运算符
在C++中,下标运算符[]
是我们在使用数组时最常见的运算符之一。通过下标运算符,我们可以访问数组中的特定元素:
cpp
int numbers[5] = {10, 20, 30, 40, 50};
int third = numbers[2]; // 访问索引为2的元素,值为30
但是,当我们创建自己的类时,如果希望该类也能像数组一样使用下标访问元素,该怎么办呢?这时就需要用到下标运算符重载。
为什么要重载下标运算符
重载下标运算符可以让我们的自定义类型实现类似数组的行为,使代码更直观、更易读。常见的应用场景包括:
- 创建自定义容器类(如向量、矩阵、哈希表等)
- 封装底层数组,提供更安全的访问方式
- 实现特殊的索引行为,如负索引、多维索引等
下标运算符重载的语法
下标运算符[]
的重载有以下几个特点:
- 必须作为成员函数重载
- 通常有两个版本:非常量版本和常量版本
- 返回类型通常是对元素的引用,以便支持修改操作
基本语法如下:
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
在这个例子中,我们实现了:
- 两个版本的下标运算符重载
- 索引检查以防止越界访问
- 对常量对象的支持
高级应用:自定义索引类型
下标运算符的参数不一定是整数类型,可以是任何类型。例如,我们可以创建一个使用字符串作为索引的简单字典类:
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++中不存在这样的运算符。
最佳实践与注意事项
在实现下标运算符重载时,请注意以下几点:
- 始终进行边界检查:防止非法访问内存
- 提供常量版本:允许常量对象使用下标运算符
- 返回引用:允许修改元素值
- 考虑抛出异常:对于严重的错误情况,比如越界访问,可以考虑抛出异常而不是返回默认值
- 避免副作用:下标运算符应该保持简单,避免复杂的副作用
应用实例:安全数组类
以下是一个更完整的安全数组类示例,演示了下标运算符重载的实际应用:
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]...
捕获异常: 数组索引越界
这个例子展示了一个更完善的安全数组实现,包括:
- 异常处理以报告错误
- 完整的复制/移动构造函数和赋值运算符
- 模板类,支持任何数据类型
- 严格的边界检查
总结
下标运算符重载是C++中非常实用的特性,它使我们能够:
- 创建行为像数组的自定义类型
- 提供安全的索引访问机制
- 实现复杂的数据结构,如多维数组、映射等
- 提高代码的可读性和直观性
正确实现下标运算符重载时,应注意边界检查、常量对象支持、返回引用等最佳实践,以确保代码的安全性和灵活性。
练习题
- 创建一个
CircularArray
类,使用下标运算符重载实现循环数组(索引超出范围时自动回绕) - 扩展
SafeArray
类,支持负索引(类似Python),如arr[-1]
表示最后一个元素 - 实现一个简单的稀疏矩阵类,只存储非零元素,但通过下标运算符允许像普通矩阵一样访问
- 创建一个时间序列类,允许使用日期或时间戳作为索引访问数据点
进一步阅读
- 《C++ Primer》第14章:重载运算与类型转换
- 《Effective C++》条款21:必须返回引用的运算符
- 《The C++ Programming Language》第18章:重载运算符