C++ 实战项目
学了 16 篇基础,是时候把它们串起来做点真东西了。这一章给三个完整、可编译运行的 C++ 项目——学生成绩管理系统、STL 单词频率统计、模板矩阵计算器,每个都给出完整代码、运行结果和要点讲解。三个项目分别对应三个核心方向:面向对象 + 文件 I/O 的工程化、STL 容器算法的组合威力、模板与运算符重载的泛型抽象。建议你把代码复制到本地用 g++ 亲手编译运行一遍——编程是手艺活,光看不练记不住。
实战 1:学生成绩管理系统(class + vector + fstream)
需求:用 struct 描述学生,用 vector 存储全班,用 fstream 把记录持久化到文本文件并能读回,用 algorithm::sort 按总分降序,再算出班级平均/最高/最低。
涉及知识:结构体与成员函数、vector 容器、ifstream / ofstream 文件读写、sort 算法与比较函数、iomanip 格式化输出。编译命令:g++ -std=c++11 students.cpp -o students。
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <algorithm>
#include <iomanip>
using namespace std;
// 学生结构体:仍然是 aggregate,可用花括号初始化
struct Student {
int id;
string name;
double chinese;
double math;
double english;
double total() const { return chinese + math + english; }
double average() const { return total() / 3.0; }
};
// 比较函数:按总分降序(algorithm::sort 用)
bool cmpByTotal(const Student& a, const Student& b) {
return a.total() > b.total();
}
// 保存到文本文件:第一行写记录数,后面每行一条
void saveToFile(const vector<Student>& v, const string& path) {
ofstream ofs(path);
if (!ofs) {
cerr << "[错误] 无法写入文件: " << path << endl;
return;
}
ofs << v.size() << "\n";
for (const auto& s : v) {
ofs << s.id << " " << s.name << " "
<< s.chinese << " " << s.math << " " << s.english << "\n";
}
ofs.close();
cout << "[保存] 已写入 " << v.size() << " 条记录到 " << path << endl;
}
// 从文本文件读回:用 >> 自动按空格分割
vector<Student> loadFromFile(const string& path) {
vector<Student> v;
ifstream ifs(path);
if (!ifs) {
cerr << "[提示] 文件不存在或无法打开: " << path << endl;
return v;
}
size_t n;
ifs >> n;
for (size_t i = 0; i < n; ++i) {
Student s;
ifs >> s.id >> s.name >> s.chinese >> s.math >> s.english;
v.push_back(s);
}
ifs.close();
return v;
}
// 打印所有学生:fixed + setprecision 设置一次,影响整个循环
void printAll(const vector<Student>& v) {
cout << "ID\t姓名\t语文\t数学\t英语\t总分\t平均" << endl;
cout << fixed << setprecision(2);
for (const auto& s : v) {
cout << s.id << "\t" << s.name << "\t"
<< s.chinese << "\t" << s.math << "\t" << s.english << "\t"
<< s.total() << "\t" << s.average() << endl;
}
}
// 全班统计:平均总分、最高、最低
void statistics(const vector<Student>& v) {
if (v.empty()) { cout << "(无数据)" << endl; return; }
double sum = 0;
double best = v[0].total();
double worst = v[0].total();
string topName = v[0].name, lowName = v[0].name;
for (const auto& s : v) {
double t = s.total();
sum += t;
if (t > best) { best = t; topName = s.name; }
if (t < worst) { worst = t; lowName = s.name; }
}
cout << fixed << setprecision(2);
cout << "全班平均总分: " << sum / v.size() << endl;
cout << "最高: " << topName << " (" << best << ")" << endl;
cout << "最低: " << lowName << " (" << worst << ")" << endl;
}
int main() {
// 花括号聚合初始化:Student 是 aggregate(成员函数不影响)
vector<Student> students = {
{1, "张三", 85, 92, 78},
{2, "李四", 76, 88, 90},
{3, "王五", 92, 95, 89},
{4, "赵六", 65, 70, 72},
};
cout << "=== 原始成绩 ===" << endl;
printAll(students);
// 按总分降序排序
sort(students.begin(), students.end(), cmpByTotal);
cout << "\n=== 按总分降序 ===" << endl;
printAll(students);
cout << "\n=== 班级统计 ===" << endl;
statistics(students);
// 持久化到文件,再读回验证
saveToFile(students, "students.txt");
vector<Student> loaded = loadFromFile("students.txt");
cout << "\n=== 从文件重新读出的记录 ===" << endl;
printAll(loaded);
return 0;
}
// 输出:
// === 原始成绩 ===
// ID 姓名 语文 数学 英语 总分 平均
// 1 张三 85.00 92.00 78.00 255.00 85.00
// 2 李四 76.00 88.00 90.00 254.00 84.67
// 3 王五 92.00 95.00 89.00 276.00 92.00
// 4 赵六 65.00 70.00 72.00 207.00 69.00
//
// === 按总分降序 ===
// ID 姓名 语文 数学 英语 总分 平均
// 3 王五 92.00 95.00 89.00 276.00 92.00
// 1 张三 85.00 92.00 78.00 255.00 85.00
// 2 李四 76.00 88.00 90.00 254.00 84.67
// 4 赵六 65.00 70.00 72.00 207.00 69.00
//
// === 班级统计 ===
// 全班平均总分: 248.00
// 最高: 王五 (276.00)
// 最低: 赵六 (207.00)
// [保存] 已写入 4 条记录到 students.txt
//
// === 从文件重新读出的记录 ===
// (与上面排序后一致)要点讲解:
- struct 也是 class:C++ 里
struct和class唯一区别是默认访问权限(public vs private)。这里用 struct 是为了聚合初始化——花括号{1, "张三", 85, 92, 78}直接赋值,因为它没有任何用户自定义构造函数。 vector<Student>:C++ 动态数组,相比 C 的malloc不用手动管容量;成员push_back尾部追加,迭代器遍历安全。sort(begin, end, cmp):STL 算法,对区间排序;第三个参数是比较函数,返回true表示 a 应排在 b 前面。这里是a.total > b.total,即降序。ofstream写文件:构造时给路径即可创建输出流;ofs << x用法和cout完全一样。文件析构时自动关闭,但显式close()是好习惯。ifstream读文件:配合>>运算符按空白切分——这就是为什么名字里不能有空格(否则要用getline)。- 格式化是"粘性"的:
fixed和setprecision(2)一旦设置,会一直影响后续浮点输出;而setw只影响下一个输出。这是iomanip常见坑。
实战 2:STL 单词频率统计(map + lambda 排序)
需求:读入一段文本(或文件),分词、规范化(转小写去标点),用 map 统计每个单词出现次数,先按字母序输出,再按频率降序输出 Top 5。
涉及知识:ifstream 整文件读入、istringstream 按空白切词、map 关联数组、vector<pair> 按值排序、lambda 表达式、cctype 字符分类函数。
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <map>
#include <algorithm>
#include <cctype>
using namespace std;
// 把一个原始 token 规范化:转小写 + 只保留字母
string normalize(const string& s) {
string out;
for (char c : s) {
// isalpha / tolower 要先把 char 转成 unsigned char,否则负数会 UB
if (isalpha(static_cast<unsigned char>(c))) {
out += static_cast<char>(tolower(static_cast<unsigned char>(c)));
}
}
return out;
}
int main() {
// 演示用:内联一段文本
string text = "the quick brown fox jumps over the lazy dog. "
"The fox is quick, the dog is lazy.";
// 实际项目里从文件读取,代码如下(取消注释即可):
// ifstream ifs("input.txt");
// if (!ifs) { cerr << "无法打开文件" << endl; return 1; }
// stringstream ss;
// ss << ifs.rdbuf();
// string text = ss.str();
// 1. 分词 + 频率统计:istringstream 用空白切词,map 自动按 key 排序
map<string, int> freq;
istringstream iss(text);
string raw;
while (iss >> raw) {
string w = normalize(raw);
if (!w.empty()) freq[w]++;
}
// 2. map 自带按 key(字母)升序,直接遍历即可
cout << "=== 按字母顺序 ===" << endl;
for (const auto& p : freq) {
cout << p.first << " : " << p.second << endl;
}
// 3. 按频率降序:map 不能按 value 排,拷贝到 vector<pair> 再 sort
vector<pair<string, int>> vec(freq.begin(), freq.end());
sort(vec.begin(), vec.end(),
[](const pair<string, int>& a, const pair<string, int>& b) {
if (a.second != b.second) return a.second > b.second; // 频率降序
return a.first < b.first; // 同频再按字母升序
});
// 4. 输出 Top 5
cout << "\n=== 按频率降序 Top 5 ===" << endl;
int n = 0;
for (const auto& p : vec) {
cout << p.first << " : " << p.second << endl;
if (++n >= 5) break;
}
return 0;
}
// 输出:
// === 按字母顺序 ===
// brown : 1
// dog : 2
// fox : 2
// is : 2
// jumps : 1
// lazy : 2
// over : 1
// quick : 2
// the : 4
//
// === 按频率降序 Top 5 ===
// the : 4
// dog : 2
// fox : 2
// is : 2
// lazy : 2要点讲解:
map<string, int>:红黑树实现的有序映射,插入时自动按 key 排序;freq[w]++在 key 不存在时会自动插入 0 再自增,这是 map 最舒服的用法。- map 不能按 value 排序:这是 map 的固有局限(它只按 key 排序)。解决套路是拷贝到
vector<pair>再用sort——这道题是 STL 组合的经典范式。 lambda:[](...){...}是 C++11 的匿名函数。方括号是捕获列表,这里空着表示不捕获外部变量;参数声明为const pair<string, int>&避免拷贝。- 多条件排序:if-else 控制次级排序——主条件(
a.second != b.second)决定主序,否则按副序。比纯布尔表达式更直观。 isalpha/tolower的坑:参数要求是int,负数(扩展 ASCII 的中文/特殊字符)会触发未定义行为,必须先转unsigned char。这是 C/C++ 老坑。- 可扩展方向:换成
unordered_map(哈希表)插入更快但无序、用priority_queue直接出 Top K、加stop words过滤(the/a/an 等虚词)。
实战 3:简易模板矩阵计算器(template + operator 重载)
需求:用 template <typename T> 写一个通用矩阵类,支持 M(i, j) 下标访问、+ / - / * 运算符,既能算 int 矩阵也能算 double 矩阵——一份代码,多种类型。
涉及知识:类模板、运算符重载(operator() / operator+ / operator- / operator*)、异常 throw、vector<vector<T>> 二维容器、矩阵加减乘的数学维度规则。
#include <iostream>
#include <vector>
#include <stdexcept>
#include <iomanip>
using namespace std;
// 类模板:元素类型 T 可以是 int / double / float ...
template <typename T>
class Matrix {
private:
vector<vector<T>> data;
size_t rows, cols;
public:
Matrix(size_t r, size_t c, T init = T())
: rows(r), cols(c), data(r, vector<T>(c, init)) {}
size_t Rows() const { return rows; }
size_t Cols() const { return cols; }
// operator() 重载:支持下标读写 M(i, j),自带边界检查
T& operator()(size_t i, size_t j) {
if (i >= rows || j >= cols) throw out_of_range("矩阵下标越界");
return data[i][j];
}
const T& operator()(size_t i, size_t j) const {
if (i >= rows || j >= cols) throw out_of_range("矩阵下标越界");
return data[i][j];
}
// 矩阵加法:维度必须完全一致
Matrix<T> operator+(const Matrix<T>& other) const {
if (rows != other.rows || cols != other.cols)
throw invalid_argument("维度不匹配,无法相加");
Matrix<T> r(rows, cols);
for (size_t i = 0; i < rows; ++i)
for (size_t j = 0; j < cols; ++j)
r(i, j) = data[i][j] + other(i, j);
return r;
}
// 矩阵减法
Matrix<T> operator-(const Matrix<T>& other) const {
if (rows != other.rows || cols != other.cols)
throw invalid_argument("维度不匹配,无法相减");
Matrix<T> r(rows, cols);
for (size_t i = 0; i < rows; ++i)
for (size_t j = 0; j < cols; ++j)
r(i, j) = data[i][j] - other(i, j);
return r;
}
// 矩阵乘法:A 是 rows x cols,B 是 other.rows x other.cols,要求 cols == other.rows
Matrix<T> operator*(const Matrix<T>& other) const {
if (cols != other.rows)
throw invalid_argument("维度不匹配,无法相乘");
Matrix<T> r(rows, other.cols, T(0));
for (size_t i = 0; i < rows; ++i)
for (size_t j = 0; j < other.cols; ++j)
for (size_t k = 0; k < cols; ++k)
r(i, j) += data[i][k] * other(k, j);
return r;
}
// 打印:cout 的 operator<< 对 setw 是粘性的,这里用最简单的格式
void print(const string& name = "") const {
if (!name.empty()) cout << name << " =" << endl;
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < cols; ++j)
cout << data[i][j] << "\t";
cout << endl;
}
}
};
int main() {
// 用 int 实例化
Matrix<int> A(2, 2);
A(0, 0) = 1; A(0, 1) = 2;
A(1, 0) = 3; A(1, 1) = 4;
A.print("A");
Matrix<int> B(2, 2);
B(0, 0) = 5; B(0, 1) = 6;
B(1, 0) = 7; B(1, 1) = 8;
B.print("B");
// 用重载的运算符写起来就像数学公式
Matrix<int> C = A + B;
C.print("A + B");
Matrix<int> D = A - B;
D.print("A - B");
Matrix<int> E = A * B;
E.print("A * B");
// 模板的威力:同一份代码换成 double 也照常工作(2x3 * 3x2 = 2x2)
Matrix<double> F(2, 3, 0.0);
F(0, 0) = 1.5; F(0, 1) = 2.0; F(0, 2) = 3.0;
F(1, 0) = 0.5; F(1, 1) = 1.0; F(1, 2) = 1.5;
Matrix<double> G(3, 2, 0.0);
G(0, 0) = 1.0; G(0, 1) = 0.0;
G(1, 0) = 0.5; G(1, 1) = 1.0;
G(2, 0) = 2.0; G(2, 1) = 0.5;
Matrix<double> H = F * G;
H.print("F * G (double)");
return 0;
}
// 输出:
// A =
// 1 2
// 3 4
// B =
// 5 6
// 7 8
// A + B =
// 6 8
// 10 12
// A - B =
// -4 -4
// -4 -4
// A * B =
// 19 22
// 43 50
// F * G (double) =
// 8.5 3.5
// 4 1.75要点讲解:
template <typename T>:T 是占位类型,实例化时由编译器替换。Matrix<int>、Matrix<double>是两个完全不同的类型——这就是泛型编程的核心,STL 全靠这套机制。operator()重载:让对象像函数一样调用,M(i, j)看起来像二维数组访问,实际是函数调用。这种"仿函数"语法在数学库、OpenCV 的cv::Mat里随处可见。- 双版本 operator():提供
const和非const两个版本——前者用于只读场景(const Matrix&也能调用),后者返回可写引用允许赋值。 operator+:返回新对象而非引用(Matrix<T> operator+(...) const),符合a + b = c的数学语义。这也是为什么运算符重载在栈上返回值——RVO(返回值优化)会消除拷贝开销。- 异常作为契约:维度不匹配就
throw invalid_argument,让调用方用try / catch优雅处理,而不是默默返回错误结果。这是异常安全思维。 - 矩阵乘法的三重循环:经典的
O(n^3)算法,i, j, k三个下标。注意内存访问顺序——k在最内层时对other(k, j)是按列访问,缓存不友好,实际工程会做分块矩阵乘法优化。 - 可扩展方向:加
operator<<友元输出、加det()行列式、transpose()转置、用std::array替代vector让编译期维度更安全。
小结
三个项目分别展示了 C++ 三大杀手锏:工程化(类 + 文件 IO 把数据和逻辑组织成系统)、组合力(STL 容器算法像积木一样拼出强大功能)、抽象力(模板让一份代码服务任意类型)。它们都用到了前面 16 篇学的基础——只是组合起来解决真实问题。建议你至少把其中一个改造成自己的版本:成绩管理加 GUI、单词统计改成读 URL、矩阵类加求逆——做过一遍比读十遍更扎实。
恭喜!到这里 C++ 系列就真正完结了。接下来挑一个方向深入——游戏开发(Unreal Engine / SDL)、高性能计算(多线程 / OpenMP / CUDA)、嵌入式(STM32 / 树莓派)、图形渲染(OpenGL / Vulkan)——边做边学,进步最快。
← 上一篇 C++ 文件 IO
返回 C++ 教程目录