第九章 运算符重载与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容器的分类:

STL容器分类 序列式容器 vector / list / deque 关联式容器 set / map / multiset / multimap 无序关联式容器 unordered_set / unordered_map 容器适配器 stack / queue / priority_queue vector:动态数组,支持随机访问 list:双向链表,插入删除快 map:键值对集合,自动排序

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++进阶的重要一步,掌握好了,写代码的效率能提升一大截。多写多练,慢慢就熟练了。


公众号:蓝海资料掘金营,微信deep3321