简介:这份资源是清华大学郑莉教授「C++语言程序设计」课程的配套课件包,面向高校计算机专业学生、C++初学者以及希望系统梳理语言基础的开发者,帮助读者从语法入门到面向对象与泛型编程建立完整知识框架。压缩包共274个文件,约802KB,以181个cpp源文件与48个h头文件为主体,配合13个ppt讲义、若干工程配置与资源文件,覆盖变量与流程控制、函数与递归、类与对象、数组与指针、动态内存管理、STL容器与算法、输入输出流、异常处理、预处理指令、模板及C++11新特性等核心知识点。课件中的例程代码与讲义相互印证,便于边看边练,将理论转化为实际编程能力。目前已有84人学习下载,适合作为课堂同步资料或自学参考,帮助读者打牢C++基础,为后续系统级编程与算法分析课程做好准备。
1. 从一份课件说起:C++ 系统学习的路径选择
很多人第一次认真学 C++,不是从某本厚书开始,而是从一份高校课件起步。标题里提到的这份课件,核心价值在于它把 C++ 的语法、面向对象、泛型与标准库串成了一条相对完整的线,而不是零散地讲几个知识点。我带过几个刚入门的同学,他们最大的问题不是不努力,而是不知道先学什么、后学什么,学到指针就卡住,学到模板就放弃。这份课件的好处是节奏稳,从基本类型、函数、类,再到继承、多态、模板和 STL,每一块都有配套例题和作业。如果你正在找一条能坚持走下去的 C++ 入门路线,或者你手里已经有这份课件但不知道怎么用,下面我会按实际带人的经验,把怎么学、怎么练、怎么避坑讲清楚。适合人群:零基础但愿意动手写代码的在校生、转行做后端或嵌入式的初学者,以及想重新梳理 C++ 知识体系的在职开发者。
2. 把课件拆成可执行的学习单元:环境、顺序与练习方法
2.1 先搭一个不折腾的编译环境
课件里的代码大多基于标准 C++,不依赖特定平台。我一般建议初学者在 Windows 上用 MinGW-w64 加 VS Code,或者在 Linux 上直接用 g++。不要一上来就装 Visual Studio 完整版,那东西太大,配置项太多,容易在环境上耗掉耐心。下面是在 Ubuntu 或 WSL 里最简的编译命令,先确保你能跑通一个 Hello World。
# 安装 g++ 和 gdb(如果还没装) sudo apt update sudo apt install -y g++ gdb # 写一个测试文件 cat > hello.cpp << 'EOF' #include <iostream> using namespace std; int main() { cout << "Hello, C++" << endl; return 0; } EOF # 编译并运行 g++ -std=c++17 -Wall -g hello.cpp -o hello ./hello这里-std=c++17指定标准版本,课件里如果用到auto、lambda或结构化绑定,至少需要 C++11,我建议直接上 C++17,兼容性更好。-Wall打开常用警告,初学阶段很多错误其实是警告被忽略了。-g保留调试信息,方便后面用 gdb 看变量。如果你在 Windows 上,把g++换成 MinGW 的完整路径即可,命令参数一样。
2.2 按课件的章节顺序走,但不要平均用力
这份课件的组织逻辑是:基础语法 → 函数与引用 → 类与对象 → 运算符重载 → 继承与多态 → 模板 → STL → 异常与文件。我一般会告诉学生,前四章必须慢,后面可以快。因为类与对象、拷贝控制、运算符重载这三块是 C++ 的分水岭,这里没吃透,后面看 STL 源码就是天书。具体时间分配上,如果每天能投入两小时,基础语法两周,函数和引用一周,类与对象三周,继承与多态两周,模板和 STL 三周,总共三个月左右能过一遍。不要跳过作业,课件里的编程题比看书有效得多。
2.3 用“最小可运行示例”验证每个知识点
课件里有些例子是片段,不能直接编译。我的习惯是每学一个知识点,自己补全成可运行的完整程序。比如学虚函数时,不要只看课件上的类图,自己写一个基类指针指向派生类对象,调用虚函数,观察输出。下面这个例子覆盖了虚函数和虚析构的基本用法。
#include <iostream> #include <memory> using namespace std; class Base { public: Base() { cout << "Base ctor" << endl; } virtual ~Base() { cout << "Base dtor" << endl; } // 虚析构,防止派生类资源泄漏 virtual void show() const { cout << "Base show" << endl; } }; class Derived : public Base { public: Derived() { cout << "Derived ctor" << endl; } ~Derived() override { cout << "Derived dtor" << endl; } void show() const override { cout << "Derived show" << endl; } }; int main() { unique_ptr<Base> p = make_unique<Derived>(); p->show(); // 输出 Derived show return 0; }这段代码的关键点:virtual ~Base()必须写,否则unique_ptr<Base>析构时不会调用Derived的析构函数,造成资源泄漏。override关键字是 C++11 引入的,让编译器帮你检查是否真的覆盖了基类虚函数。很多课件还在用老式写法,不加override,我建议你加上,能提前发现拼写错误或签名不匹配。
2.4 用表格管理每章的验收标准
不要凭感觉判断自己学没学会。我一般让学生每章结束后填一张自检表,下面是我常用的模板,你可以直接抄。
| 章节 | 能独立写出什么 | 常见错误 | 验收方式 |
|---|---|---|---|
| 基础语法 | 输入输出、循环、数组、字符串 | 数组越界、cin与getline混用 | 手写一个统计单词频率的程序 |
| 函数与引用 | 值传递、引用传递、默认参数、函数重载 | 返回局部变量引用 | 实现一个交换两个字符串的函数 |
| 类与对象 | 构造函数、拷贝构造、赋值运算符、析构 | 浅拷贝导致 double free | 写一个带动态数组的类,能正确拷贝 |
| 继承与多态 | 虚函数、纯虚函数、虚析构 | 对象切片、忘记虚析构 | 用基类指针管理不同派生类 |
| 模板与 STL | 函数模板、类模板、vector、map、算法 | 迭代器失效、模板实例化错误 | 用 map 统计词频并按值排序 |
这张表的作用是让你知道“会了”的标准是什么。比如类与对象那一行,如果你写不出一个能正确拷贝的动态数组类,说明拷贝控制还没过,不要急着往下学。
3. 课件里最容易翻车的四个地方:避坑与排查
3.1 指针和引用混用导致段错误
现象:程序编译通过,运行时报 Segmentation fault,或者输出乱码。原因:课件里讲引用时经常和指针对比,初学者容易写出int& r = *p;但p是空指针,或者返回局部变量的引用。解决:任何引用绑定前,先确认被绑定的对象生命周期足够长。函数返回引用时,只返回静态变量、全局变量或调用者传入的对象。用 gdb 跑一遍,bt看调用栈,p看变量值。
3.2 拷贝构造函数写成浅拷贝
现象:两个对象析构时程序崩溃,报 double free。原因:类里有指针成员,拷贝时只复制了指针地址,两个对象指向同一块内存,析构两次。解决:只要类管理了资源,就按“三法则”或“五法则”写拷贝构造、拷贝赋值、析构。C++11 之后可以用= default和= delete明确控制。下面是一个正确写法。
class MyArray { int* data; size_t size; public: MyArray(size_t n) : data(new int[n]), size(n) {} ~MyArray() { delete[] data; } MyArray(const MyArray& other) : data(new int[other.size]), size(other.size) { std::copy(other.data, other.data + size, data); } MyArray& operator=(const MyArray& other) { if (this != &other) { int* newData = new int[other.size]; std::copy(other.data, other.data + other.size, newData); delete[] data; data = newData; size = other.size; } return *this; } };注意赋值运算符里先分配新内存再释放旧内存,防止自赋值和异常安全。
3.3 迭代器失效后继续使用
现象:用 vector 存储数据,一边遍历一边删除元素,程序崩溃或结果不对。原因:vector 删除元素后,被删位置之后的迭代器全部失效。解决:用erase的返回值更新迭代器,或者先记录要删除的下标,最后统一删除。常见写法:
for (auto it = vec.begin(); it != vec.end(); ) { if (*it % 2 == 0) { it = vec.erase(it); // erase 返回下一个有效迭代器 } else { ++it; } }3.4 模板编译错误看不懂
现象:写了一个模板函数,调用时出现几十行错误信息,找不到重点。原因:模板实例化时类型不匹配,编译器把整个调用链都打印出来。解决:从错误信息的第一行开始看,找到“no matching function”或“invalid conversion”,然后检查传入的类型是否满足模板里的操作。我一般会先用具体类型写一遍,确认逻辑正确,再改成模板。另外,模板定义通常要放在头文件里,否则链接时会报未定义符号。
4. 从课件到项目:用 STL 写一个可复现的词频统计器
4.1 为什么选词频统计作为第一个完整项目
课件里 STL 部分讲了 vector、map、string 和算法,但很多人学完不知道能干什么。词频统计是一个最小闭环:读文件、分词、统计、排序、输出。它覆盖了容器、迭代器、算法、字符串处理和文件流,而且代码量不大,适合作为第一个拿得出手的练习。我一般要求学生在两小时内独立完成,然后对照下面的实现找差距。
4.2 完整实现与逐段说明
#include <iostream> #include <fstream> #include <sstream> #include <string> #include <map> #include <vector> #include <algorithm> #include <cctype> // 将单词转为小写,并去掉首尾非字母字符 std::string normalize(const std::string& word) { std::string result; for (char ch : word) { if (std::isalpha(static_cast<unsigned char>(ch))) { result += std::tolower(static_cast<unsigned char>(ch)); } } return result; } int main(int argc, char* argv[]) { if (argc != 2) { std::cerr << "Usage: " << argv[0] << " <filename>" << std::endl; return 1; } std::ifstream file(argv[1]); if (!file) { std::cerr << "Cannot open file: " << argv[1] << std::endl; return 1; } std::map<std::string, int> freq; std::string line; while (std::getline(file, line)) { std::istringstream iss(line); std::string word; while (iss >> word) { std::string key = normalize(word); if (!key.empty()) { ++freq[key]; } } } // 将 map 转为 vector 以便按频率排序 std::vector<std::pair<std::string, int>> items(freq.begin(), freq.end()); std::sort(items.begin(), items.end(), [](const auto& a, const auto& b) { if (a.second != b.second) return a.second > b.second; return a.first < b.first; }); for (const auto& [word, count] : items) { std::cout << word << ": " << count << std::endl; } return 0; }逻辑说明:normalize函数只保留字母并转小写,这样 “Hello” 和 “hello” 会统计为同一个词。std::map自动按 key 排序,但我们需要按频率排序,所以转存到vector再用std::sort配合 lambda。lambda 里先比频率降序,频率相同再按字典序升序。最后用 C++17 的结构化绑定[word, count]输出。参数方面,命令行只接受一个文件名,如果文件打不开会报错并返回非零值,方便脚本调用。
4.3 编译、运行与验证
g++ -std=c++17 -Wall -O2 wordfreq.cpp -o wordfreq ./wordfreq input.txt-O2打开优化,对大数据文件速度提升明显。你可以用课件里的任意英文文本测试,比如把一段讲义保存为input.txt。验证方法:手动数一下前三个高频词,和程序输出对比。如果结果不对,先检查normalize是否把数字和标点去掉了,再检查文件是否真的读到了内容。
4.4 可以继续扩展的方向
这个版本只统计英文单词。你可以改成支持中文分词,或者增加排除停用词的功能。停用词表可以用std::set<std::string>存储,在++freq[key]之前判断一下。另一个方向是输出到文件而不是终端,用std::ofstream替换std::cout即可。这些改动都不大,但能让你把课件里的文件流、容器和算法串起来。
5. 进阶技巧:用课件里的模板知识写一个类型安全的缓存
5.1 从模板类到实际用途
课件里模板那一章通常只讲语法,比如函数模板、类模板、特化。很多人学完就忘了。我一般会带学生写一个简单的 LRU 缓存,用std::unordered_map加std::list实现,但这里为了贴合课件,我们用模板类封装一个固定容量的缓存,支持任意可拷贝的类型。这个练习能让你真正理解模板的实例化、成员函数定义和类型推导。
5.2 实现一个最小可用的模板缓存
#include <iostream> #include <map> #include <list> #include <stdexcept> template <typename Key, typename Value> class LRUCache { public: explicit LRUCache(size_t capacity) : cap(capacity) {} void put(const Key& key, const Value& value) { auto it = cache.find(key); if (it != cache.end()) { it->second->second = value; items.splice(items.begin(), items, it->second); return; } if (cache.size() >= cap) { auto last = items.back().first; items.pop_back(); cache.erase(last); } items.emplace_front(key, value); cache[key] = items.begin(); } Value get(const Key& key) { auto it = cache.find(key); if (it == cache.end()) { throw std::out_of_range("key not found"); } items.splice(items.begin(), items, it->second); return it->second->second; } private: size_t cap; std::list<std::pair<Key, Value>> items; std::map<Key, typename std::list<std::pair<Key, Value>>::iterator> cache; }; int main() { LRUCache<int, std::string> cache(2); cache.put(1, "one"); cache.put(2, "two"); std::cout << cache.get(1) << std::endl; // one cache.put(3, "three"); // 淘汰 key 2 try { std::cout << cache.get(2) << std::endl; } catch (const std::out_of_range& e) { std::cout << "2 evicted" << std::endl; } return 0; }关键点:items是std::list,存储键值对,最近使用的放在前面。cache是std::map,键是 Key,值是 list 的迭代器。put时如果 key 已存在,更新值并把节点移到最前;如果容量满了,删掉 list 最后一个节点,并从 map 中擦除对应键。get时把节点移到最前并返回值。模板参数Key和Value可以是任意可拷贝类型,但注意std::map要求 Key 支持operator<,如果你用自定义类型,需要提供比较函数或重载<。
5.3 验证方法与边界测试
写完之后不要只跑一个例子。至少测这几种情况:容量为 1 时连续插入两个不同键,看第一个是否被淘汰;重复插入同一个键,看是否更新值且不增加容量;get一个不存在的键,看是否抛出异常。我一般会写一个小测试函数,把预期结果打印出来对比。另外,模板代码编译较慢,错误信息也长,建议每写一个成员函数就编译一次,不要全写完再编。
5.4 我踩过的一个坑
早期我写类似缓存时,用std::vector存节点,结果每次移动都要搬移大量元素,性能很差。后来换成std::list,移动节点是 O(1),但要注意迭代器失效问题:splice不会使迭代器失效,但erase会使被删节点的迭代器失效。所以cache里存的迭代器在erase之后必须同步删除,否则下次访问就是悬空迭代器。这个坑我调了一个下午,血泪经验就是:容器混用时,一定要画图理清每个迭代器指向哪里,什么时候会失效。希望帮到你。
本文还有配套的精品资源,点击获取