在操作数组时,我们通常使用下标运算符([])来索引数组中的特定元素:
myArray[0] = 7; // 将 7 存入数组的第一个元素
然而,考虑以下 IntList 类,其成员变量为数组:
class IntList
{
private:
int m_list[10]{};
};
int main()
{
IntList list{};
// 如何访问 m_list 中的元素?
return 0;
}
由于成员变量 m_list 为私有,我们无法通过变量 list 直接访问它,因此无法直接对 m_list 数组进行读取或赋值。那么,如何向列表中存取元素?
未使用运算符重载时的典型做法
通常做法是编写访问函数:
class IntList
{
private:
int m_list[10]{};
public:
void setItem(int index, int value) { m_list[index] = value; }
int getItem(int index) const { return m_list[index]; }
};
虽然可行,但并不友好。例如:
int main()
{
IntList list{};
list.setItem(2, 3);
return 0;
}
我们究竟是将元素 2 赋值为 3,还是将元素 3 赋值为 2?在未查看 setItem() 定义的情况下,难以判断。
也可以返回整个数组再使用 operator[] 访问:
class IntList
{
private:
int m_list[10]{};
public:
int* getList() { return m_list; }
};
但语法怪异:
int main()
{
IntList list{};
list.getList()[2] = 3;
return 0;
}
重载 operator[]
更佳方案是重载下标运算符([]),以允许直接访问 m_list 元素。下标运算符必须重载为成员函数。重载函数始终接受一个参数:用户置于方括号内的下标。对于 IntList,该参数应为整型索引,函数返回对应整型值。
#include <iostream>
class IntList
{
private:
int m_list[10]{};
public:
int& operator[] (int index)
{
return m_list[index];
}
};
/*
// 亦可在类外实现
int& IntList::operator[] (int index)
{
return m_list[index];
}
*/
int main()
{
IntList list{};
list[2] = 3; // 设置值
std::cout << list[2] << '\n'; // 获取值
return 0;
}
此后,对类对象使用下标运算符时,编译器返回 m_list 对应元素,实现直接读写。
语法直观:表达式 list[2]
中,编译器先检查是否存在重载 operator[],若存在,则将 2 作为参数传入。
注意:虽可为函数参数提供默认值,但语法上不允许
operator[]
后无下标,因此无意义。
为何 operator[]
返回引用
考察 list[2] = 3
求值过程:下标运算符优先级高于赋值运算符,故 list[2]
先求值。operator[] 返回 list.m_list[2]
的引用,因此表达式变为 list.m_list[2] = 3
,完成整数赋值。
在operator[]
可用于赋值左侧,故其返回值须为左值。引用恒为左值,因其只能绑定到有地址的变量。因此返回引用即可满足要求。
若 operator[] 按值返回整数,则 list[2]
返回 m_list[2] 的值(如 6),表达式变为 6 = 3
,非法!编译器报错:
error C2106: '=' : 左操作数必须为左值
为 const
对象重载 operator[]
上例中 operator[] 非 const,可用于修改非 const 对象状态。若 IntList 对象为 const,则无法调用非 const 版本,否则可能修改 const 对象。
可分别定义 const 与非 const 版本:非 const 版本用于非 const 对象,const 版本用于 const 对象。
#include <iostream>
class IntList
{
private:
int m_list[10]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // 初始状态
public:
// 非 const 版本:可用于赋值
int& operator[] (int index)
{
return m_list[index];
}
// const 版本:仅用于读取
// 若类型拷贝开销小,亦可按值返回
const int& operator[] (int index) const
{
return m_list[index];
}
};
int main()
{
IntList list{};
list[2] = 3; // 合法:调用非 const 版本
std::cout << list[2] << '\n';
const IntList clist{};
// clist[2] = 3; // 编译错误:clist[2] 返回 const 引用,不可赋值
std::cout << clist[2] << '\n';
return 0;
}
消除 const
与非 const
重载间的重复代码
上例中,int& IntList::operator[](int)
与 const int& IntList::operator[](int) const
实现完全一致,仅返回类型不同。若实现简单(单条语句),少量冗余可接受。
若实现复杂(如需验证索引合法性),重复代码则成问题。可将 const 版本实现逻辑集中,非 const 版本调用 const 版本并使用 const_cast 移除 const:
#include <iostream>
#include <utility> // for std::as_const
class IntList
{
private:
int m_list[10]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
public:
int& operator[] (int index)
{
// 使用 std::as_const 获取 const 引用,调用 const 版本
// 再用 const_cast 移除返回引用的 const
return const_cast<int&>(std::as_const(*this)[index]);
}
const int& operator[] (int index) const
{
return m_list[index];
}
};
通常应避免用 const_cast 移除 const,但此处可接受:若调用非 const 重载,说明操作对象非 const,移除 const 安全。
C++23 进阶写法(供高级读者)
C++23 可用显式对象参数与 auto&&
区分 const 与非 const:
#include <iostream>
class IntList
{
private:
int m_list[10]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
public:
auto&& operator[](this auto&& self, int index)
{
// 复杂逻辑可放于此
return self.m_list[index];
}
};
检测索引有效性
重载下标运算符的另一优势是可增强安全性。直接访问数组时,下标运算符不检查索引合法性:
int list[5]{};
list[7] = 3; // 索引 7 越界!
若已知数组大小,可在重载函数中校验索引:
#include <cassert> // for assert()
#include <iterator> // for std::size()
class IntList
{
private:
int m_list[10]{};
public:
int& operator[] (int index)
{
assert(index >= 0 && static_cast<std::size_t>(index) < std::size(m_list));
return m_list[index];
}
};
上例使用 assert()
(<cassert>
提供)验证索引。若条件为假,程序终止并打印错误信息,远优于内存损坏。
若不想使用 assert
(其在非调试构建中将被移除),可用 if
与自定义错误处理(如抛异常、调用 std::exit
等):
#include <iterator> // for std::size()
class IntList
{
private:
int m_list[10]{};
public:
int& operator[] (int index)
{
if (!(index >= 0 && static_cast<std::size_t>(index) < std::size(m_list)))
{
// 处理无效索引
}
return m_list[index];
}
};
对象指针与重载 operator[]
不兼容
对对象指针调用 operator[]
时,C++ 会认为你在索引该类型对象数组:
#include <cassert>
#include <iterator>
class IntList
{
private:
int m_list[10]{};
public:
int& operator[] (int index)
{
return m_list[index];
}
};
int main()
{
IntList* list{ new IntList{} };
list[2] = 3; // 错误:编译器认为访问 IntList 数组索引 2
delete list;
return 0;
}
由于无法将整数赋给 IntList,此代码无法编译;若赋值合法,则行为未定义。
规则
勿对对象指针调用重载 operator[]
。
正确写法为先解引用指针(注意括号,因 []
优先级高于 *
),再调用 operator[]
:
int main()
{
IntList* list{ new IntList{} };
(*list)[2] = 3; // 先获取对象,再调用重载 operator[]
delete list;
return 0;
}
此写法丑陋且易错,若无必要,避免使用指向对象的指针。
形参不限于整型
如前所述,C++ 将方括号内内容作为实参传递。通常此值为整数,但并非必须——你可令重载 operator[]
接受任意类型:double、std::string 等。
示例(仅为演示):
#include <iostream>
#include <string_view> // C++17
class Stupid
{
private:
// 空
public:
void operator[] (std::string_view index);
};
// 重载 operator[] 打印内容虽无意义,但可展示非整型参数
void Stupid::operator[] (std::string_view index)
{
std::cout << index;
}
int main()
{
Stupid stupid{};
stupid["Hello, world!"];
return 0;
}
输出:
Hello, world!
令 operator[]
接受 std::string 参数,可在以单词为索引的类中发挥作用。
测验
问题 1
Map 是一种以键值对存储元素的类。键必须唯一,用于访问对应值。本测验将编写一个简单 Map 类,实现按姓名为学生赋分。姓名作为键,分数(char 类型)作为值。
a) 首先,编写名为 StudentGrade 的结构体,包含学生姓名(std::string)和分数(char)。
b) 添加 GradeMap 类,内含名为 m_map 的 std::vector
c) 为该类重载 operator[]。函数接受 std::string 参数,返回 char 引用。函数体内先检查学生是否存在(可用 <algorithm>
中的 std::find_if)。若存在,返回分数引用即可;否则使用 std::vector::emplace_back() 或 push_back() 添加新 StudentGrade。添加后,用 std::vector::back() 获取刚加入的学生并返回其分数引用。
以下程序应能运行:
#include <iostream>
// ...
int main()
{
GradeMap grades{};
grades["Joe"] = 'A';
grades["Frank"] = 'B';
std::cout << "Joe has a grade of " << grades["Joe"] << '\n';
std::cout << "Frank has a grade of " << grades["Frank"] << '\n';
return 0;
}
问题 2
加分题 1:上述 GradeMap 类及示例程序存在多种低效原因。请描述一种改进 GradeMap 类的方法。
问题 3
加分题 2:为何以下程序可能不按预期运行?
#include <iostream>
int main()
{
GradeMap grades{};
char& gradeJoe{ grades["Joe"] }; // 触发 emplace_back
gradeJoe = 'A';
char& gradeFrank{ grades["Frank"] }; // 触发 emplace_back
gradeFrank = 'B';
std::cout << "Joe has a grade of " << gradeJoe << '\n';
std::cout << "Frank has a grade of " << gradeFrank << '\n';
return 0;
}