特殊运算符重载
定义与作用
除了算术和关系运算符,C++ 还支持重载一些具有特殊语义的运算符:函数调用运算符 () 创建仿函数、下标运算符 [] 实现类数组访问、箭头运算符 -> 实现智能指针、new/delete 定制内存分配,以及 C++11 引入的用户定义字面量。
核心原理
特殊运算符的独特规则
完整示例
示例一:仿函数与下标访问——简易矩阵类
场景说明:设计支持二维下标访问 matrix(i, j) 和 matrix[i][j] 的矩阵类。
#include <iostream>
#include <vector>
#include <stdexcept>
#include <initializer_list>
class Matrix {
public:
Matrix(size_t rows, size_t cols)
: rows_(rows), cols_(cols), data_(rows * cols, 0.0) {}
Matrix(std::initializer_list<std::initializer_list<double>> init) {
rows_ = init.size();
cols_ = rows_ > 0 ? init.begin()->size() : 0;
data_.reserve(rows_ * cols_);
for (const auto& row : init) {
data_.insert(data_.end(), row.begin(), row.end());
}
}
// ====== operator()——函数调用:二维下标访问 ======
double& operator()(size_t row, size_t col) {
check_bounds(row, col);
return data_[row * cols_ + col];
}
const double& operator()(size_t row, size_t col) const {
check_bounds(row, col);
return data_[row * cols_ + col];
}
// ====== operator()——仿函数:矩阵变换 ======
Matrix operator()(double (*func)(double)) const {
Matrix result(rows_, cols_);
for (size_t i = 0; i < data_.size(); ++i) {
result.data_[i] = func(data_[i]);
}
return result;
}
// ====== operator[]——返回代理对象以支持 [i][j] ======
class RowProxy {
public:
RowProxy(double* row, size_t cols) : row_(row), cols_(cols) {}
double& operator[](size_t col) {
if (col >= cols_) throw std::out_of_range("列索引越界");
return row_[col];
}
const double& operator[](size_t col) const {
if (col >= cols_) throw std::out_of_range("列索引越界");
return row_[col];
}
private:
double* row_;
size_t cols_;
};
RowProxy operator[](size_t row) {
if (row >= rows_) throw std::out_of_range("行索引越界");
return RowProxy(&data_[row * cols_], cols_);
}
size_t rows() const { return rows_; }
size_t cols() const { return cols_; }
void print() const {
for (size_t i = 0; i < rows_; ++i) {
std::cout << " [";
for (size_t j = 0; j < cols_; ++j) {
if (j > 0) std::cout << ", ";
std::cout << (*this)(i, j);
}
std::cout << "]" << std::endl;
}
}
private:
void check_bounds(size_t row, size_t col) const {
if (row >= rows_ || col >= cols_)
throw std::out_of_range("矩阵索引越界");
}
size_t rows_, cols_;
std::vector<double> data_;
};
// ====== 用户定义字面量(C++11)======
// 像素单位字面量
constexpr unsigned long long operator"" _px(unsigned long long n) {
return n;
}
// 弧度转角度
constexpr long double operator"" _deg(long double deg) {
return deg * 3.14159265358979323846L / 180.0L;
}
// 字节单位
constexpr unsigned long long operator"" _KB(unsigned long long n) {
return n * 1024;
}
constexpr unsigned long long operator"" _MB(unsigned long long n) {
return n * 1024 * 1024;
}
int main() {
std::cout << "===== 矩阵运算 =====" << std::endl;
// 用 operator() 访问
Matrix m(3, 3);
m(0, 0) = 1.0; m(0, 1) = 2.0; m(0, 2) = 3.0;
m(1, 0) = 4.0; m(1, 1) = 5.0; m(1, 2) = 6.0;
m(2, 0) = 7.0; m(2, 1) = 8.0; m(2, 2) = 9.0;
std::cout << "原始矩阵:" << std::endl;
m.print();
// 用 operator[] 访问
std::cout << "m[1][2] = " << m[1][2] << std::endl;
// 用 operator() 作为仿函数——对每个元素应用变换
auto squared = m([](double x) { return x * x; });
std::cout << "\n平方矩阵:" << std::endl;
squared.print();
// 列表初始化
Matrix identity = {
{1.0, 0.0, 0.0},
{0.0, 1.0, 0.0},
{0.0, 0.0, 1.0}
};
std::cout << "单位矩阵:" << std::endl;
identity.print();
std::cout << "\n===== 用户定义字面量 =====" << std::endl;
std::cout << "100_px = " << 100_px << " px" << std::endl;
std::cout << "180_deg = " << 180.0_deg << " 弧度" << std::endl;
std::cout << "1_KB = " << 1_KB << " 字节" << std::endl;
std::cout << "1_MB = " << 1_MB << " 字节" << std::endl;
return 0;
}
预期输出:
===== 矩阵运算 =====
原始矩阵:
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
m[1][2] = 6
平方矩阵:
[1, 4, 9]
[16, 25, 36]
[49, 64, 81]
单位矩阵:
[1, 0, 0]
[0, 1, 0]
[0, 0, 1]
===== 用户定义字面量 =====
100_px = 100 px
180_deg = 3.14159 弧度
1_KB = 1024 字节
1_MB = 1048576 字节
逐段分析:
operator()(size_t, size_t)实现二维下标访问——比operator[]更灵活(参数个数任意)operator()(double(*)(double))让矩阵对象可像函数一样调用——这是仿函数模式operator[]返回RowProxy代理对象——实现m[i][j]的级联下标- 用户定义字面量(
operator"" _px)让代码更具可读性——100_px比100 /* px */更自然 - 字面量运算符的参数只能是特定类型(
unsigned long long、long double、const char*等)
示例二:智能指针的箭头运算符
#include <iostream>
#include <memory>
// 包装类——演示 operator->
template <typename T>
class AccessLogger {
public:
explicit AccessLogger(T* ptr) : ptr_(ptr) {}
~AccessLogger() { delete ptr_; }
// operator-> 返回原始指针
T* operator->() {
std::cout << " [访问] operator-> 调用" << std::endl;
return ptr_;
}
const T* operator->() const {
std::cout << " [只读访问] operator-> 调用" << std::endl;
return ptr_;
}
T& operator*() {
std::cout << " [解引用] operator* 调用" << std::endl;
return *ptr_;
}
private:
T* ptr_;
};
struct Student {
std::string name;
int score;
void study() { std::cout << " " << name << " 在学习..." << std::endl; }
};
int main() {
std::cout << "===== operator-> 演示 =====" << std::endl;
AccessLogger<Student> logger(new Student{"张三", 95});
// logger->name 等价于 logger.operator->()->name
logger->name = "张三丰";
std::cout << "姓名: " << logger->name << std::endl;
logger->study();
// operator* 解引用
(*logger).score = 98;
std::cout << "分数: " << logger->score << std::endl;
return 0;
}
预期输出:
===== operator-> 演示 =====
[访问] operator-> 调用
[访问] operator-> 调用
姓名: 张三丰
[访问] operator-> 调用
张三丰 在学习...
[解引用] operator* 调用
[访问] operator-> 调用
分数: 98
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
operator[] 只提供非 const 版本 | const 对象无法访问 | 同时提供 const 和非 const 版本 |
operator() 滥用 | 语义模糊 | 保留给有明确"调用"语义的场景 |
operator-> 返回非指针类型 | 编译器不断调用 operator-> | 最终必须返回原生指针 |
| 字面量运算符重载全局作用域 | 命名冲突 | 放在自定义命名空间中 |
operator new 只重载不重载 delete | 不匹配的分配/释放 | new 和 delete 成对重载 |
常见面试问题
operator()和普通成员函数的区别?什么时候用?——operator()让对象像函数一样可调用(仿函数),常用于 STL 算法(比函数指针更灵活,可保持状态)、回调、延迟计算等。相比普通成员函数,仿函数提供了统一的调用语义。operator->的实现原理?为什么最终必须返回原生指针?——编译器对obj->member的处理是:若obj的operator->返回一个指针,直接用该指针访问成员;若返回的是对象,递归调用该对象的operator->,直到返回原生指针。这是 C++ 标准规定的。什么是代理类(Proxy)模式?在
operator[]中有什么用?——通过operator[]返回一个代理对象来支持多维下标(如matrix[i][j])。代理对象本身重载了operator[],形成级联。用户定义字面量的约束?——参数只能是
unsigned long long、long double、char、wchar_t、char16_t、char32_t或const char*+size_t。不能重载 C++ 内置的字面量后缀(如f、L)。推荐以下划线_开头。为什么需要同时重载 const 和非 const 版本的
operator[]和operator()?——const 对象只能调用 const 成员函数。如果只有非 const 版本,const 对象或 const 引用将无法访问元素。
小结
operator()创建仿函数——参数个数不限,是 STL 算法的重要搭档operator[]返回代理对象实现多维下标——需同时提供 const 和非 const 版本operator->必须最终返回原生指针——编译器会递归调用直到得到指针- 用户定义字面量(C++11)让代码更具可读性——后缀必须以下划线开头
operator new/delete定制内存分配/释放——成对重载,静态成员