第九章 运算符重载与STL入门
各位同学,今天我们来聊聊C++里两个非常实用的东西——运算符重载和STL。说实话,这两个话题放在一起讲是有原因的:它们都体现了C++“让类型用起来更自然”的设计哲学。
运算符重载让你自定义的类型能像内置类型一样使用运算符,而STL则提供了一套现成的数据结构和算法。我刚开始学的时候也觉得运算符重载有点花哨,直到在项目中用它简化了矩阵运算的代码——嗯,真香。
9.1 运算符重载的基本规则
运算符重载,说白了就是给已有的运算符赋予新的含义。比如你定义了一个复数类,想让两个复数对象能直接用“+”相加,这就是运算符重载干的事。
核心原则:不能创造新的运算符,只能重载已有的。不能改变运算符的优先级和结合性。
我个人习惯把运算符重载分为两类:成员函数形式和友元函数形式。看个例子:
class Complex {
private:
double real, imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 成员函数形式
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
// 友元函数形式(适合需要左操作数不是当前对象的情况)
friend Complex operator*(double lhs, const Complex& rhs);
};
Complex operator*(double lhs, const Complex& rhs) {
return Complex(lhs * rhs.real, lhs * rhs.imag);
}
这里有个坑,我曾经踩过:赋值运算符(=)必须重载为成员函数,而且通常要返回引用。为什么?因为C++标准规定赋值运算符必须是成员函数,否则编译器不认。
避坑指南:我曾经在项目中把operator=写成了友元函数,结果编译报错找了半天。记住:=、[]、()、->这四个运算符只能重载为成员函数。
9.2 常见运算符重载实战
实际开发中,最常用的运算符重载就那么几个。我列个表,大家一目了然:
| 运算符 | 典型用途 | 推荐形式 |
|---|---|---|
| + - * / | 数学运算(向量、矩阵、复数) | 友元函数 |
| += -= *= | 复合赋值 | 成员函数,返回引用 |
| == != | 比较两个对象是否相等 | 友元函数 |
| << >> | 输入输出流 | 友元函数 |
| [] | 下标访问(类似数组) | 成员函数 |
| () | 函数对象(仿函数) | 成员函数 |
来看一个完整的例子,重载输入输出运算符:
class Point {
int x, y;
public:
Point(int x = 0, int y = 0) : x(x), y(y) {}
friend ostream& operator<<(ostream& os, const Point& p) {
os << "(" << p.x << ", " << p.y << ")";
return os;
}
friend istream& operator>>(istream& is, Point& p) {
is >> p.x >> p.y;
return is;
}
};
你想想看,如果没有运算符重载,你要打印一个Point对象就得写个print()函数,多麻烦。有了<<重载,直接cout << point就完事了。
9.3 STL容器入门
STL(标准模板库)是C++的宝藏。我当年刚接触STL时,最大的感受就是:原来写代码可以这么省事。不用自己写链表、不用自己实现动态数组,直接用现成的容器就行。
先看一张图,理清STL容器的分类:
9.3.1 vector——最常用的动态数组
vector是我用得最多的容器,没有之一。它本质上就是一个动态数组,能自动扩容。我在项目中处理传感器数据时,经常用vector来存储不定数量的采样点。
#include <vector>
#include <iostream>
using namespace std;
int main() {
vector<int> vec; // 空vector
vec.push_back(10); // 尾部添加
vec.push_back(20);
vec.push_back(30);
cout << "大小: " << vec.size() << endl; // 输出3
cout << "容量: " << vec.capacity() << endl; // 可能大于3
// 随机访问
for (size_t i = 0; i < vec.size(); ++i) {
cout << vec[i] << " ";
}
// 范围for循环(C++11起)
for (int val : vec) {
cout << val << " ";
}
return 0;
}
小技巧:如果你预先知道要存多少元素,用reserve()提前分配内存,能避免多次扩容带来的性能开销。我在处理大数据量时,这个优化能省下不少时间。
9.3.2 list——双向链表
list适合频繁插入删除的场景。你想想看,vector在中间插入元素要移动后面的所有元素,而list只需要改几个指针就行。
#include <list>
list<string> names;
names.push_back("Alice");
names.push_front("Bob"); // list支持头部插入
names.insert(++names.begin(), "Charlie"); // 在第二个位置插入
不过list也有缺点:不支持随机访问。你想访问第5个元素?得从头遍历过去。所以,频繁随机访问用vector,频繁中间插入删除用list。
9.3.3 map——键值对存储
map是我个人非常喜欢的一个容器。它存储键值对,并且按键自动排序。我在项目中用它来做配置管理,把配置项名称作为键,配置值作为值,查找起来特别方便。
#include <map>
map<string, int> scores;
scores["Alice"] = 95;
scores["Bob"] = 87;
scores["Charlie"] = 92;
// 查找
auto it = scores.find("Bob");
if (it != scores.end()) {
cout << "Bob的分数: " << it->second << endl;
}
// 遍历
for (const auto& pair : scores) {
cout << pair.first << ": " << pair.second << endl;
}
注意:用[]访问map时,如果键不存在,会自动插入一个默认值。这有时候会带来意想不到的bug。我曾经因为这个特性,在统计词频时多统计了一个空字符串。建议用find()或at()来安全访问。
9.4 迭代器——容器的通用访问方式
迭代器是什么?说白了,它就像一个指针,让你能遍历容器中的元素。而且所有STL容器的迭代器用法都差不多,这就是泛型编程的魅力。
看个例子,用迭代器遍历vector:
vector<int> vec = {1, 2, 3, 4, 5};
// 正向迭代
for (auto it = vec.begin(); it != vec.end(); ++it) {
cout << *it << " "; // 解引用获取元素
}
// 反向迭代
for (auto it = vec.rbegin(); it != vec.rend(); ++it) {
cout << *it << " "; // 输出5 4 3 2 1
}
// 常量迭代器(只读)
for (auto it = vec.cbegin(); it != vec.cend(); ++it) {
// *it = 10; // 错误!不能修改
cout << *it << " ";
}
迭代器分为几种类型,我整理了一下:
| 迭代器类型 | 支持的操作 | 典型容器 |
|---|---|---|
| 输入迭代器 | 只读、单向移动 | istream_iterator |
| 输出迭代器 | 只写、单向移动 | ostream_iterator |
| 前向迭代器 | 读写、单向移动 | forward_list |
| 双向迭代器 | 读写、双向移动 | list, set, map |
| 随机访问迭代器 | 读写、任意位置跳转 | vector, deque |
这里有个实用技巧:用auto关键字声明迭代器,省得写长长的类型名。比如:
map<string, vector<int>>::iterator it; // 太长了
auto it = myMap.begin(); // 简洁明了
9.5 综合示例:学生成绩管理系统
最后,我把今天讲的东西串起来,写一个简单的学生成绩管理系统。这个例子我在教学中经常用,因为它涵盖了运算符重载、vector、map和迭代器的综合运用。
#include <iostream>
#include <vector>
#include <map>
#include <string>
#include <algorithm>
using namespace std;
class Student {
string name;
int score;
public:
Student(const string& n, int s) : name(n), score(s) {}
// 重载<运算符,用于排序
bool operator<(const Student& other) const {
return score > other.score; // 按分数降序
}
// 重载<<运算符,用于输出
friend ostream& operator<<(ostream& os, const Student& s) {
os << s.name << ": " << s.score;
return os;
}
string getName() const { return name; }
int getScore() const { return score; }
};
int main() {
// 用vector存储所有学生
vector<Student> students;
students.push_back(Student("Alice", 95));
students.push_back(Student("Bob", 87));
students.push_back(Student("Charlie", 92));
// 排序(使用了重载的<运算符)
sort(students.begin(), students.end());
cout << "=== 成绩排名 ===" << endl;
for (const auto& s : students) {
cout << s << endl; // 使用了重载的<<运算符
}
// 用map按等级分组
map<string, vector<Student>> gradeMap;
for (const auto& s : students) {
string grade;
if (s.getScore() >= 90) grade = "优秀";
else if (s.getScore() >= 80) grade = "良好";
else grade = "及格";
gradeMap[grade].push_back(s);
}
cout << "\n=== 按等级分组 ===" << endl;
for (const auto& pair : gradeMap) {
cout << pair.first << " (" << pair.second.size() << "人): ";
for (const auto& s : pair.second) {
cout << s.getName() << " ";
}
cout << endl;
}
return 0;
}
这个例子把今天讲的知识点都用上了。你想想看,如果没有运算符重载,sort函数怎么知道按什么规则排序?如果没有STL容器,你得自己写链表、自己管理内存,多麻烦。
好了,今天的内容就到这里。运算符重载和STL是C++进阶的重要一步,掌握好了,写代码的效率能提升一大截。多写多练,慢慢就熟练了。