简介:这份资源是清华大学郑莉教授「C++语言程序设计」课程的配套课件包,面向高校计算机专业学生、C++初学者以及希望系统梳理语言基础的开发者,帮助读者从语法入门到面向对象与泛型编程建立完整知识框架。压缩包共274个文件,约802KB,以181个cpp源文件与48个h头文件为主体,配合13个ppt讲义、若干工程配置与资源文件,覆盖类与对象、数组与指针、动态内存管理、STL容器与算法、输入输出流、异常处理、模板及C++11新特性等核心知识点,并配有例程代码与实验素材,便于边学边练。目前已有84人学习下载。课件结构清晰,既可作为课堂同步复习资料,也适合作为上机实践的参考代码库,帮助读者将理论转化为实际编程能力。
1. 从一份课件标题说起:C++ 系统学习的路径选择
很多人第一次看到“清华大学郑莉C++语言程序设计课件”这个标题,第一反应是去找下载链接,第二反应是存进网盘吃灰。我见过太多这样的案例:资源囤了几十个G,编译器都没装好。这份课件真正有价值的地方,不是它来自哪里,而是它背后那套从语法到面向对象再到泛型编程的递进结构——这套结构恰好对应了C++学习者最容易踩坑的三个阶段。如果你正在自学C++,或者带新人入门,又或者想从C语言转过来但被指针和类搞得一头雾水,这份课件的组织方式值得你花时间拆解。它解决的核心问题是:在碎片化教程泛滥的环境下,如何用一条主线把C++的知识点串成可运行的代码能力。适合谁?适合已经会写简单C程序、但一遇到类和模板就翻车的人,也适合想系统梳理C++知识体系但不想啃枯燥标准的从业者。
2. 课件结构拆解:从语法基础到面向对象的递进逻辑
2.1 为什么先讲函数重载而不是类
很多C++教程一上来就讲class,结果新手连引用和指针的区别都没搞清,写出来的类全是内存泄漏。这份课件的顺序是:先讲函数重载、默认参数、引用,再引入类。这个顺序有讲究——函数重载让你理解同名不同参的编译期决议机制,引用让你理解别名的语义,这两点搞懂了,后面理解拷贝构造函数和运算符重载才不会懵。
我一般会建议跟着这个顺序走的人,每学一个语法点就写一个最小可运行程序。比如学引用的时候,不要只看课件里的图示,直接写代码验证:
// 引用与指针的核心区别验证 #include <iostream> using namespace std; void swapByRef(int& a, int& b) { int temp = a; a = b; b = temp; } void swapByPtr(int* a, int* b) { int temp = *a; *a = *b; *b = temp; } int main() { int x = 10, y = 20; swapByRef(x, y); // 引用传递,语法更干净 cout << "x=" << x << ", y=" << y << endl; int m = 30, n = 40; swapByPtr(&m, &n); // 指针传递,需要取地址 cout << "m=" << m << ", n=" << n << endl; // 引用的关键特性:必须初始化,不能重新绑定 int& ref = x; ref = 100; // 修改ref就是修改x cout << "x after ref change: " << x << endl; return 0; }这段代码的逻辑说明:swapByRef展示引用作为函数参数时,调用方不需要加取地址符,函数体内直接当普通变量用;swapByPtr展示指针版本需要显式取地址和解引用。参数说明:int& a是左值引用,绑定到调用时的实参,函数内对a的修改直接作用于原变量。关键点在于引用必须在定义时初始化,且一旦绑定不能改绑——这是它和指针最大的语义差异。课件里通常会强调“引用是别名”,但只有自己写一遍才能体会到为什么拷贝构造函数必须用引用传参:如果用值传参,会无限递归调用拷贝构造。
2.2 类与对象:封装不是把变量藏起来那么简单
课件在讲类的时候,重点落在接口与实现分离上。很多人以为封装就是加个private,然后写一堆getter/setter,这其实是把类写成了结构体。真正的封装是:类的公开接口只暴露必要的操作,内部数据结构的改变不影响调用方。
我一般会用一个简化的“模拟项目”来练手——比如写一个整数栈:
// 封装的核心:接口稳定,实现可替换 #include <iostream> #include <vector> #include <stdexcept> using namespace std; class IntStack { public: void push(int value) { data.push_back(value); } int pop() { if (data.empty()) { throw runtime_error("stack underflow"); } int top = data.back(); data.pop_back(); return top; } bool empty() const { return data.empty(); } size_t size() const { return data.size(); } private: vector<int> data; // 内部实现细节,外部不可见 }; int main() { IntStack s; s.push(1); s.push(2); s.push(3); while (!s.empty()) { cout << s.pop() << " "; } cout << endl; return 0; }逻辑说明:IntStack的公开接口只有push、pop、empty、size,内部用vector<int>存储。如果以后想换成链表实现,只要接口不变,调用方代码一行都不用改。参数说明:push接收一个int值,pop返回栈顶元素并在空栈时抛出异常。注意empty()和size()标记为const,因为它们不修改对象状态。课件里会强调 const 成员函数的重要性,但新手经常忘记加,导致 const 对象无法调用这些方法。
2.3 继承与多态:什么时候该用虚函数
课件在继承部分花了大量篇幅讲虚函数表的工作机制,这是理解多态的关键。很多人知道在基类指针指向派生类对象时,调用虚函数会执行派生类的版本,但不知道为什么。简单说:每个有虚函数的类都有一个虚函数表,对象里存一个指向该表的指针,调用时通过表找到实际函数地址。
// 多态的最小验证:不加virtual会怎样 #include <iostream> using namespace std; class Base { public: virtual void show() { cout << "Base::show" << endl; } virtual ~Base() {} // 虚析构,防止派生类资源泄漏 }; class Derived : public Base { public: void show() override { cout << "Derived::show" << endl; } }; int main() { Base* p = new Derived(); p->show(); // 输出 Derived::show delete p; // 虚析构保证正确释放 return 0; }逻辑说明:Base的show声明为virtual,Derived重写它。通过基类指针调用时,实际执行的是Derived::show。如果把virtual去掉,输出就是Base::show。参数说明:override是 C++11 引入的关键字,让编译器检查是否真的重写了基类虚函数,避免签名不匹配导致的“假重写”。虚析构函数是必须的——如果基类指针指向派生类对象,删除时没有虚析构,派生类的析构函数不会被调用,造成资源泄漏。这个坑我在实际项目里见过不止一次,血泪经验就是:只要类可能被继承,析构函数就加 virtual。
3. 模板与泛型编程:从函数模板到STL的实战衔接
3.1 函数模板的推导规则与常见翻车点
课件在模板部分从函数模板讲起,这是理解STL的基础。函数模板的核心是类型推导——编译器根据实参推导模板参数类型。但推导规则有几个容易翻车的地方:数组退化为指针、const 修饰符的保留、引用折叠。
// 模板推导的典型陷阱 #include <iostream> #include <typeinfo> using namespace std; template <typename T> void printType(T value) { cout << "T is: " << typeid(T).name() << endl; } template <typename T> void printRefType(T& value) { cout << "T& is: " << typeid(T).name() << endl; } int main() { int arr[5] = {1, 2, 3, 4, 5}; printType(arr); // T推导为int*,数组退化为指针 printRefType(arr); // T推导为int[5],引用保留数组类型 const int ci = 10; printType(ci); // T推导为int,const被丢弃(值传递) printRefType(ci); // T推导为const int,const被保留 return 0; }逻辑说明:printType是值传递,数组退化为指针,const 被丢弃;printRefType是引用传递,数组类型保留,const 保留。参数说明:typeid(T).name()返回编译器内部的类型名称,不同编译器输出不同,这里只用于观察推导结果。这个知识点在实际写模板函数时非常关键——如果你希望保留数组大小信息,就必须用引用传参。课件里通常会强调“值传递会拷贝,引用传递不拷贝”,但推导规则的细节才是真正决定行为的地方。
3.2 STL容器适配:vector、list、map的选型依据
课件在讲STL时,重点不是罗列每个容器的API,而是讲什么时候用哪个。这个思路很对——API查文档就行,选型错了后面全是坑。
| 容器 | 底层结构 | 随机访问 | 插入删除 | 适用场景 |
|---|---|---|---|---|
| vector | 动态数组 | O(1) | 尾部O(1),中间O(n) | 频繁随机访问,尾部增删 |
| list | 双向链表 | O(n) | O(1) | 频繁中间插入删除 |
| map | 红黑树 | O(log n) | O(log n) | 需要有序键值对 |
| unordered_map | 哈希表 | 平均O(1) | 平均O(1) | 不需要有序,追求查找速度 |
选型依据:如果你需要频繁按下标访问,选 vector;如果需要在中间频繁插入删除,选 list;如果需要按键查找且键有序,选 map;如果只追求查找速度且键无序,选 unordered_map。我一般会建议新手先用 vector 解决80%的问题,遇到性能瓶颈再考虑换容器。课件里会提到迭代器失效的问题——vector 扩容后所有迭代器失效,list 删除元素后只有被删元素的迭代器失效。这个坑在写循环删除时特别容易踩。
3.3 用模板写一个通用数组类:把课件知识串起来
把类模板、运算符重载、异常处理串起来,写一个通用的动态数组:
// 类模板综合练习:通用动态数组 #include <iostream> #include <stdexcept> using namespace std; template <typename T> class DynArray { public: explicit DynArray(size_t cap = 10) : data(new T[cap]), capacity(cap), length(0) {} ~DynArray() { delete[] data; } // 禁用拷贝,防止浅拷贝double free DynArray(const DynArray&) = delete; DynArray& operator=(const DynArray&) = delete; void push_back(const T& value) { if (length == capacity) { resize(capacity * 2); } data[length++] = value; } T& operator[](size_t index) { if (index >= length) { throw out_of_range("index out of range"); } return data[index]; } size_t size() const { return length; } private: void resize(size_t newCap) { T* newData = new T[newCap]; for (size_t i = 0; i < length; ++i) { newData[i] = data[i]; } delete[] data; data = newData; capacity = newCap; } T* data; size_t capacity; size_t length; }; int main() { DynArray<int> arr; for (int i = 0; i < 20; ++i) { arr.push_back(i * i); } for (size_t i = 0; i < arr.size(); ++i) { cout << arr[i] << " "; } cout << endl; return 0; }逻辑说明:DynArray用模板参数T表示元素类型,内部用new T[]分配堆内存。push_back在容量满时翻倍扩容,operator[]提供越界检查。拷贝构造和拷贝赋值被delete禁用,防止浅拷贝导致的 double free。参数说明:explicit防止隐式转换,size_t表示无符号大小类型。这个练习把类模板、运算符重载、异常处理、内存管理都串起来了,是检验课件学习效果的好方式。
4. 避坑与排查:学C++课件时最容易翻车的五个地方
4.1 现象:程序编译通过但运行崩溃,提示 double free
原因:类里用了裸指针管理资源,但没有写拷贝构造函数和拷贝赋值运算符,导致默认的浅拷贝让两个对象指向同一块内存,析构时释放两次。
解决:遵循三法则——如果需要自定义析构函数,通常也需要自定义拷贝构造和拷贝赋值。或者直接用智能指针unique_ptr/shared_ptr管理资源,从根源上避免手动释放。
4.2 现象:虚函数调用了但没执行派生类版本
原因:基类函数没有加virtual,或者派生类函数签名不匹配(比如基类是void show() const,派生类写成了void show())。
解决:基类函数加virtual,派生类函数加override让编译器检查。如果编译报错说没有可重写的虚函数,就说明签名不匹配。
4.3 现象:vector 循环中删除元素后迭代器失效,程序行为异常
原因:erase会使被删元素及其后的迭代器失效,继续用旧迭代器是未定义行为。
解决:用erase的返回值更新迭代器:
for (auto it = vec.begin(); it != vec.end(); ) { if (shouldRemove(*it)) { it = vec.erase(it); // erase返回下一个有效迭代器 } else { ++it; } }4.4 现象:模板代码编译报错,错误信息长达几百行
原因:模板实例化时类型不满足要求,编译器把整个推导链路的错误都打印出来。
解决:从错误信息的最上面开始看,找到“required from here”的部分,定位到实际调用点。我一般会先把模板参数替换成具体类型手动验证一遍,确认逻辑没问题再改回模板。
4.5 现象:const 对象调用成员函数编译失败
原因:成员函数没有标记const,编译器认为它可能修改对象状态。
解决:不修改对象状态的成员函数都加const,包括size()、empty()、getXXX()这类。养成习惯后,const 正确性会成为代码质量的一部分。
5. 把课件变成能力:三个验证学习效果的具体方法
5.1 用“闭卷重写”检验语法掌握程度
看完一章课件后,关掉所有参考,从空白文件开始重写课件里的示例代码。能写出来说明语法记住了,写不出来说明只是看懂了。我一般会挑三个典型例子:一个类定义、一个模板函数、一个继承体系。重写时不要追求和课件一模一样,只要行为正确就行。
5.2 用“改需求”检验设计理解深度
拿课件里的示例代码,给自己提一个修改需求。比如课件里写了一个整数栈,你改成模板栈;课件里写了一个基类,你加一个派生类并重写虚函数。改需求的过程中会遇到各种边界问题,这些问题才是真正长经验的地方。
5.3 用“最小可运行项目”检验综合能力
给自己定一个一周能完成的小项目,比如命令行通讯录、简易计算器、文本词频统计。要求用到类、模板、STL容器、异常处理。写完后对照课件查漏补缺——哪个知识点用得不顺手,就回去重看那一章。
| 验证方法 | 检验目标 | 耗时 | 通过标准 |
|---|---|---|---|
| 闭卷重写 | 语法记忆 | 30分钟/章 | 能独立写出可编译运行的等价代码 |
| 改需求 | 设计理解 | 1小时/例 | 修改后行为正确,无内存泄漏 |
| 最小项目 | 综合能力 | 3-7天 | 功能完整,异常处理到位 |
这三个方法我反复用过,最有效的是闭卷重写——它逼你面对“以为自己会了”的幻觉。很多次我以为自己懂了某个语法,一关参考就写不出来,这种翻车经历比看十遍课件都管用。希望帮到你。
本文还有配套的精品资源,点击获取