📖 第 2.4 章 ⏱️ 约 50 分钟 🎯 入门

第 2.4 章:结构体与类

📝 前置条件: 第 2.1–2.3 章(变量、控制流、函数、数组)

竞赛编程中,你经常需要把相关数据组合在一起——例如,一个点有 xy,一条边有两个端点和权重,一名学生有姓名和成绩。C++ 提供了 structclass 来把数据(和行为)绑定成单一类型。本章涵盖两者,重点关注竞赛编程中最重要的内容。


2.4.1 为什么要把数据组合在一起?

🎒 背包类比

📄 查看代码:🎒 背包类比
想象你要去旅行。你可以把每件东西分别拿着:
  - 左手:护照
  - 右手:手机
  - 口袋:钱包
  - 嘴里:机票 😬

或者,把所有东西放进一个背包:
  - backpack.passport ✅
  - backpack.phone ✅
  - backpack.wallet ✅
  - backpack.ticket ✅

struct/class 就是那个背包——把相关的东西组合在一个名字下。

没有结构体,如果你想存储 1000 名学生的名字和成绩,需要两个独立的数组:

string names[1000];
int scores[1000];
// 你必须手动保持下标同步——容易出错!

有了结构体,干净而安全:

struct Student {
    string name;
    int score;
};
Student students[1000];  // 每个学生自带名字和成绩

2.4.2 结构体基础

定义结构体

📄 查看代码:定义结构体
#include <bits/stdc++.h>
using namespace std;

struct Point {
    int x;
    int y;
};  // <-- 别忘了分号!

int main() {
    Point p;       // 声明一个 Point 变量
    p.x = 3;       // 用点号(.)访问成员
    p.y = 7;
    cout << "(" << p.x << ", " << p.y << ")" << endl;  // (3, 7)
    return 0;
}

初始化方式

// 方式一:聚合初始化(C++11,竞赛中最常用)
Point p1 = {3, 7};

// 方式二:指定初始化(C++20)
Point p2 = {.x = 3, .y = 7};

// 方式三:逐字段赋值
Point p3;
p3.x = 3;
p3.y = 7;

💡 竞赛技巧: 竞赛编程中,聚合初始化 {val1, val2, ...} 是最常用的风格——输入快,读起来简洁。

带构造函数的结构体

可以定义构造函数让创建实例更简洁:

📄 可以定义**构造函数**让创建实例更简洁:
struct Point {
    int x, y;

    // 构造函数
    Point(int _x, int _y) : x(_x), y(_y) {}
};

int main() {
    Point p(3, 7);  // 调用构造函数
    cout << p.x << " " << p.y << endl;  // 3 7
}

⚠️ 警告: 一旦定义了自定义构造函数,就不能再用 Point p;(无参数)了,除非同时提供默认构造函数或给参数设默认值。

struct Point {
    int x, y;

    Point() : x(0), y(0) {}            // 默认构造函数
    Point(int _x, int _y) : x(_x), y(_y) {}  // 有参构造函数
};

Point p1;       // OK——使用默认构造函数,x=0, y=0
Point p2(3, 7); // OK——使用有参构造函数

2.4.3 结构体在竞赛编程中的应用

存储图论问题中的边

📄 查看代码:存储图论问题中的边
struct Edge {
    int from, to, weight;
};

int main() {
    int n, m;
    cin >> n >> m;

    vector<Edge> edges(m);
    for (int i = 0; i < m; i++) {
        cin >> edges[i].from >> edges[i].to >> edges[i].weight;
    }
}

自定义比较来排序结构体

这在 USACO 题目中极为常见。你经常需要按某个特定字段排序。

方法一:在结构体内重载 operator<

📄 C++ 完整代码
struct Event {
    int start, end;

    // 按结束时间排序(贪心调度)
    bool operator<(const Event& other) const {
        return end < other.end;
    }
};

int main() {
    vector<Event> events = {{1, 4}, {3, 5}, {0, 6}, {5, 7}, {3, 8}, {5, 9}};
    sort(events.begin(), events.end());  // 自动使用 operator<

    for (auto& e : events) {
        cout << "[" << e.start << ", " << e.end << "] ";
    }
}

方法二:lambda 比较器(更灵活)

📄 C++ 完整代码
struct Event {
    int start, end;
};

int main() {
    vector<Event> events = {{1, 4}, {3, 5}, {0, 6}, {5, 7}};

    // 按开始时间排序
    sort(events.begin(), events.end(), [](const Event& a, const Event& b) {
        return a.start < b.start;
    });
}

方法三:编写比较函数

bool compareByEnd(const Event& a, const Event& b) {
    return a.end < b.end;
}

sort(events.begin(), events.end(), compareByEnd);

💡 竞赛技巧: 对大多数 USACO 题,方法一(运算符重载)在只有一种自然排序顺序时最简洁。当同一程序需要多种不同排序顺序时,用方法二(lambda)。

多关键字排序

有时需要先按一个字段排序,再用另一个字段打破平局:

struct Student {
    string name;
    int score;

    bool operator<(const Student& other) const {
        if (score != other.score) return score > other.score;  // 高分优先
        return name < other.name;  // 分数相同时按姓名字典序
    }
};

或用 tie() 写法更简洁:

struct Student {
    string name;
    int score;

    bool operator<(const Student& other) const {
        // 按分数降序,再按姓名升序
        return tie(other.score, name) < tie(score, other.name);
    }
};

💡 tie() 技巧: tie() 创建一个元组用于字典序比较。调换元素顺序可反转该字段的排序方向。这是竞赛编程中非常常见的技巧。

在集合和映射中使用结构体

如果想把结构体作为 setmap 的键,必须定义 operator<

📄 如果想把结构体作为 `set` 或 `map` 的键,**必须**定义 `operator<`:
struct Point {
    int x, y;
    bool operator<(const Point& other) const {
        return tie(x, y) < tie(other.x, other.y);
    }
};

set<Point> visited;
visited.insert({1, 2});
visited.insert({3, 4});

if (visited.count({1, 2})) {
    cout << "已访问过!" << endl;
}

在优先队列中使用结构体

📄 查看代码:在优先队列中使用结构体
struct State {
    int dist, node;

    // 最小堆:希望 dist 最小的在顶部
    // priority_queue 默认是最大堆,所以反转比较
    bool operator>(const State& other) const {
        return dist > other.dist;
    }
};

// 用 operator> 实现最小堆
priority_queue<State, vector<State>, greater<State>> pq;
pq.push({0, 1});   // 距离 0,节点 1
pq.push({5, 2});   // 距离 5,节点 2

auto top = pq.top();  // {0, 1}——距离最小

2.4.4 struct vs class——有什么区别?

实际情况是:struct 和 class 在 C++ 中几乎完全相同。唯一区别是默认访问级别

特性structclass
默认访问publicprivate
可以有方法?✅ 可以✅ 可以
可以有构造函数?✅ 可以✅ 可以
可以继承?✅ 可以✅ 可以
// 这两个功能上完全相同:

struct PointS {
    int x, y;  // 默认 public
};

class PointC {
public:          // 必须显式声明为 public
    int x, y;
};

什么时候用哪个?

struct 当……class 当……
简单数据容器有不变量的复杂对象
竞赛编程(几乎始终)面向对象设计项目
所有成员都是 public需要封装(私有数据)
想要最少的样板代码构建库或大型系统

💡 竞赛惯例: 竞赛编程中,始终用 struct。它更简单、更简短,而且几乎从不需要私有成员。几乎每个竞赛选手的代码里都是 struct


2.4.5 类——完整视角

尽管 struct 足以应付竞赛编程,理解 class 对更广泛的 C++ 知识很有价值。

访问修饰符

📄 查看代码:访问修饰符
class BankAccount {
private:    // 只能在类内部访问
    double balance;

public:     // 可以从任何地方访问
    BankAccount(double initial) : balance(initial) {}

    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }

    void withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
        }
    }

    double getBalance() const {
        return balance;
    }
};

int main() {
    BankAccount acc(100.0);
    acc.deposit(50.0);
    acc.withdraw(30.0);
    cout << acc.getBalance() << endl;  // 120.0

    // acc.balance = 999999;  // 错误!balance 是私有的
}

为什么需要封装?

想象一台自动贩卖机:

- 公共接口:投币、按按钮、取饮料
- 私有内部:硬币计数器、库存、温控系统

你通过公共按钮与机器交互。
你不能直接伸手取饮料。

封装保护数据不被滥用。

竞赛编程中不需要这种级别的保护——代码速度更重要。但在软件工程中,它能防止大型代码库中的 bug。

成员函数(方法)

structclass 都可以有成员函数:

📄 `struct` 和 `class` 都可以有成员函数:
struct Rect {
    int width, height;

    int area() const {
        return width * height;
    }

    int perimeter() const {
        return 2 * (width + height);
    }

    bool contains(int x, int y) const {
        return x >= 0 && x < width && y >= 0 && y < height;
    }
};

int main() {
    Rect r = {10, 5};
    cout << "面积:" << r.area() << endl;      // 50
    cout << "周长:" << r.perimeter() << endl;  // 30
    cout << r.contains(3, 4) << endl;            // 1(true)
}

💡 方法名后的 const 方法名后的 const 关键字表示「这个方法不修改对象」。如果方法只读取数据,始终标记为 const——这是良好实践,在使用 const 引用时也是必须的。


2.4.6 竞赛编程中的进阶结构体模式

pair——内置的「两字段结构体」

C++ 提供了 pair 作为只需要两个字段时的轻量替代:

📄 C++ 提供了 `pair` 作为只需要两个字段时的轻量替代:
#include <bits/stdc++.h>
using namespace std;

int main() {
    pair<int, int> p = {3, 7};
    cout << p.first << " " << p.second << endl;  // 3 7

    // pair 有内置比较(字典序)
    vector<pair<int, int>> v = {{3, 1}, {1, 5}, {3, 0}, {1, 2}};
    sort(v.begin(), v.end());
    // 结果:{1, 2}, {1, 5}, {3, 0}, {3, 1}

    // 可以用 make_pair 或直接用花括号
    auto q = make_pair(10, 20);
}

什么时候用 pair vs 自定义 struct

pair用自定义 struct
只有 2 个字段3 个及以上字段
字段不需要有意义的名称需要描述性字段名
临时分组代码清晰度很重要

tuple——内置的「N 字段结构体」

tuple<int, string, double> t = {42, "Alice", 3.14};
cout << get<0>(t) << endl;  // 42
cout << get<1>(t) << endl;  // Alice

// 结构化绑定(C++17)——更简洁
auto [id, name, gpa] = t;
cout << name << " 的 GPA 是 " << gpa << endl;

💡 竞赛技巧: 超过 2 个字段时,带名称的 struct 几乎总是比 tuple 更易读。pair 可以随意用,但能用 struct 时就别用 tuple

arrayvector 成员的结构体

📄 查看代码:含 array 或 vector 成员的结构体
struct Graph {
    int n;
    vector<vector<int>> adj;

    Graph(int _n) : n(_n), adj(_n) {}

    void addEdge(int u, int v) {
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
};

int main() {
    Graph g(5);
    g.addEdge(0, 1);
    g.addEdge(1, 2);

    for (int v : g.adj[1]) {
        cout << v << " ";  // 0 2
    }
}

2.4.7 常见错误

❌ 错误一:忘记 } 后的分号

struct Point {
    int x, y;
}   // ← 缺少分号!

int main() { ... }
// 编译器给出令人困惑的错误,指向 main()

修复: 结构体/类定义的右花括号后始终加 ;

❌ 错误二:运算符重载中忘记 const

struct Point {
    int x, y;
    // 错误——缺少 const
    bool operator<(const Point& other) {  // ← 在某些 STL 容器中无法工作
        return tie(x, y) < tie(other.x, other.y);
    }
};

修复: 比较运算符始终标记为 const

bool operator<(const Point& other) const {  // ✅
    return tie(x, y) < tie(other.x, other.y);
}

❌ 错误三:使用未初始化的结构体成员

struct State {
    int dist, node;
};

State s;
cout << s.dist;  // 未定义行为!可能是任意值

修复: 始终初始化,或提供默认值:

struct State {
    int dist = 0;
    int node = 0;
};

❌ 错误四:优先队列中 operator< 方向搞反

struct State {
    int dist;
    // 想实现最小堆,你可能会这样写:
    bool operator<(const State& other) const {
        return dist < other.dist;  // 这会得到最大堆!(与你想要的相反)
    }
};

修复:priority_queue 实现最小堆,要么反转比较,要么用 greater<>

📄 C++ 完整代码
// 方案 A:反转 operator<
bool operator<(const State& other) const {
    return dist > other.dist;  // dist 更大的优先级更低 → 最小堆
}
priority_queue<State> pq;

// 方案 B:定义 operator> 并使用 greater<>
bool operator>(const State& other) const {
    return dist > other.dist;
}
priority_queue<State, vector<State>, greater<State>> pq;

2.4.8 练习题

🟢 题目一:学生排名

读取 n 名学生(姓名和成绩),按成绩降序排序,打印排名。

输入:
3
Alice 85
Bob 92
Charlie 85

输出:
1. Bob 92
2. Alice 85
3. Charlie 85
💡 提示 定义一个带 operator< 的 struct,成绩不同时高分优先,相同时按姓名字典序。
✅ 题解
#include <bits/stdc++.h>
using namespace std;

struct Student {
    string name;
    int score;

    bool operator<(const Student& other) const {
        if (score != other.score) return score > other.score;
        return name < other.name;
    }
};

int main() {
    int n;
    cin >> n;
    vector<Student> students(n);
    for (int i = 0; i < n; i++) {
        cin >> students[i].name >> students[i].score;
    }
    sort(students.begin(), students.end());
    for (int i = 0; i < n; i++) {
        cout << i + 1 << ". " << students[i].name << " " << students[i].score << "\n";
    }
}

🟢 题目二:最近点对(一维)

给定数轴上 n 个点,找距离最小的点对。

输入:
5
7 1 4 9 2

输出:
1
(点 1 和点 2 之间)
💡 提示 排序后,答案是相邻元素的最小差值。
✅ 题解
#include <bits/stdc++.h>
using namespace std;

struct PointVal {
    int val, originalIndex;

    bool operator<(const PointVal& other) const {
        return val < other.val;
    }
};

int main() {
    int n;
    cin >> n;
    vector<PointVal> points(n);
    for (int i = 0; i < n; i++) {
        cin >> points[i].val;
        points[i].originalIndex = i;
    }
    sort(points.begin(), points.end());

    int minDist = INT_MAX;
    int bestI = 0, bestJ = 1;
    for (int i = 0; i + 1 < n; i++) {
        int d = points[i + 1].val - points[i].val;
        if (d < minDist) {
            minDist = d;
            bestI = i;
            bestJ = i + 1;
        }
    }
    cout << minDist << "\n";
    cout << "(点 " << points[bestI].val << " 和点 " << points[bestJ].val << " 之间)\n";
}

🟡 题目三:区间调度(贪心)

给定 n 个区间 [start, end],找不重叠区间的最大数量。

📄 给定 `n` 个区间 `[start, end]`,找不重叠区间的最大数量。
输入:
6
1 4
3 5
0 6
5 7
3 8
5 9

输出:
3
💡 提示 按结束时间排序区间。贪心地选择结束时间最早且与上一个已选区间不冲突的区间。
✅ 题解
#include <bits/stdc++.h>
using namespace std;

struct Interval {
    int start, end;

    bool operator<(const Interval& other) const {
        return end < other.end;
    }
};

int main() {
    int n;
    cin >> n;
    vector<Interval> intervals(n);
    for (int i = 0; i < n; i++) {
        cin >> intervals[i].start >> intervals[i].end;
    }
    sort(intervals.begin(), intervals.end());

    int count = 0, lastEnd = -1;
    for (auto& it : intervals) {
        if (it.start >= lastEnd) {
            count++;
            lastEnd = it.end;
        }
    }
    cout << count << "\n";
}

📋 本章总结

概念核心要点
struct组合相关数据;成员默认为 public
class与 struct 相同,但成员默认为 private
构造函数创建实例时调用的特殊函数
operator<sort()setmappriority_queue 支持你的类型
tie()简洁的多关键字比较技巧
pair内置的 2 字段结构体,支持字典序比较
const 方法标记不修改对象的方法

🎯 竞赛编程核心要点

  1. 竞赛中始终用 struct —— 更简单、更简短
  2. 掌握 operator< 重载 —— 几乎每道 USACO 题都会用到
  3. 多关键字排序用 tie() —— 简洁且不易出错
  4. 记得 const —— STL 兼容性的要求
  5. 初始化你的成员 —— 避免未定义行为
  6. 2 个字段用 pair,3 个及以上用自定义 struct —— 好的经验法则

✅ 第 2.4 章完成!
你现在知道如何创建自定义数据类型了——这是竞赛编程中组织数据的关键技能。接下来:强大的 STL 容器!