第 2.4 章:结构体与类
📝 前置条件: 第 2.1–2.3 章(变量、控制流、函数、数组)
竞赛编程中,你经常需要把相关数据组合在一起——例如,一个点有 x 和 y,一条边有两个端点和权重,一名学生有姓名和成绩。C++ 提供了 struct 和 class 来把数据(和行为)绑定成单一类型。本章涵盖两者,重点关注竞赛编程中最重要的内容。
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()创建一个元组用于字典序比较。调换元素顺序可反转该字段的排序方向。这是竞赛编程中非常常见的技巧。
在集合和映射中使用结构体
如果想把结构体作为 set 或 map 的键,必须定义 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++ 中几乎完全相同。唯一区别是默认访问级别:
| 特性 | struct | class |
|---|---|---|
| 默认访问 | public | private |
| 可以有方法? | ✅ 可以 | ✅ 可以 |
| 可以有构造函数? | ✅ 可以 | ✅ 可以 |
| 可以继承? | ✅ 可以 | ✅ 可以 |
// 这两个功能上完全相同:
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。
成员函数(方法)
struct 和 class 都可以有成员函数:
📄 `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。
含 array 或 vector 成员的结构体
📄 查看代码:含 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()、set、map、priority_queue 支持你的类型 |
| tie() | 简洁的多关键字比较技巧 |
| pair | 内置的 2 字段结构体,支持字典序比较 |
| const 方法 | 标记不修改对象的方法 |
🎯 竞赛编程核心要点
- 竞赛中始终用
struct—— 更简单、更简短 - 掌握
operator<重载 —— 几乎每道 USACO 题都会用到 - 多关键字排序用
tie()—— 简洁且不易出错 - 记得
const—— STL 兼容性的要求 - 初始化你的成员 —— 避免未定义行为
- 2 个字段用
pair,3 个及以上用自定义 struct —— 好的经验法则
你现在知道如何创建自定义数据类型了——这是竞赛编程中组织数据的关键技能。接下来:强大的 STL 容器!