新增 4 天教案大纲 + reveal.js 交互幻灯片(docs/teaching/)

大纲按「3天C++强化(含OOP)+1天Qt桌面」组织,讲:练≈1:2.7;每个知识点直接
链接仓库真实代码文件与 ERRATA/VERSION_NOTES 里记录的具体坑。幻灯片
vendor 了 reveal.js 5.2.1(核心+highlight插件),零 CDN 依赖,配 serve.py
本地起服务(仅绑定 127.0.0.1)供离线播放,代码链接可点开查看源码。
This commit is contained in:
张宗平
2026-07-01 16:51:12 +08:00
parent 8e44203929
commit b89819d5db
13 changed files with 2093 additions and 0 deletions
+311
View File
@@ -0,0 +1,311 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>Day 2 · OOP 进阶</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="reveal/reveal.css">
<link rel="stylesheet" href="reveal/theme.css">
<link rel="stylesheet" href="reveal/plugin/highlight/monokai.css">
<link rel="stylesheet" href="custom.css">
</head>
<body>
<div class="reveal">
<div class="slides">
<section>
<h1>Day 2</h1>
<h3>OOP 进阶</h3>
<p class="module-badge">Qt 方向实训 · 第 2 / 4 天 · 讲:练 ≈ 1:2.7</p>
<ul>
<li>模块 A(3 课时):对象模型 / 友元 / 运算符重载</li>
<li>模块 B3 课时):继承与多态</li>
</ul>
<p><small>回顾:Day1 已建立类/构造析构/深浅拷贝的直觉——今天在这个基础上深挖机制</small></p>
</section>
<!-- ===================== 模块 A:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 讲授 35min</p>
<h2>对象模型 / 友元 / 运算符重载 <span class="tag tag-lecture"></span></h2>
<ul>
<li>this 指针到底解决什么问题</li>
<li>友元:突破封装的代价与取舍</li>
<li>运算符重载:=、==、[]、()、++/-- 全景</li>
</ul>
</section>
<section>
<h3>this 指针:区分同名 + 隐式参数 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class Person{
public:
Person(string name, int age){
this->name = name; // 形参名与成员名相同,靠 this-> 区分
this->age = age;
}
// 成员函数隐含的第一个参数就是 this —— operator= 也是这样实现的
Person PersonPlusPerson(Person& person){
string newname = this->name + person.name;
int newage = this->age + person.age;
return Person(newname, newage);
}
};
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p01/ch04/s04/this_pointer.cpp">p01/ch04/s04/this_pointer.cpp</a></small></p>
</section>
<section>
<h3>友元:全局函数 / 成员函数 / 整个类 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class Building{
friend void CleanBedRoom(Building& building); // 友元全局函数
friend class MyFriend; // 友元类——MyFriend 的所有成员都能访问私有区
public:
Building();
private:
string mBedroom;
};
</code></pre>
<p><small>友元是「单向」「不可传递」「不可继承」的特权授予——用得越多,封装边界越模糊,
权衡取舍要讲清楚。出处:<a class="filelink" target="_blank"
href="../../../p01/ch04/s05/friend_syntax.cpp">p01/ch04/s05/friend_syntax.cpp</a></small></p>
</section>
<section>
<h3>赋值运算符重载:与拷贝构造的分工 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
Person2& operator=(const Person2& person){
// 关键:当前对象已存在,pName 可能已指向堆内存——必须先释放,否则内存泄漏
if (this->pName != NULL){ delete[] this->pName; }
this->pName = new char[strlen(person.pName) + 1];
strcpy(this->pName, person.pName);
return *this; // 返回引用才能支持 a = b = c 连续赋值
}
</code></pre>
<p><small>“初始化用拷贝构造,已存在对象再赋值用 operator=”——这条分界线是本节强化重点。
出处:<a class="filelink" target="_blank"
href="../../../p01/ch04/s06/overload_assignment.cpp">p01/ch04/s06/overload_assignment.cpp</a></small></p>
</section>
<section>
<h3>前置/后置 ++ 的重载差异 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
Complex& operator++(){ // 前置++:改自身,返回引用
mA++; mB++;
return *this;
}
Complex operator++(int){ // 后置++:占位 int 参数只是为了和前置区分重载签名
Complex temp = *this; // 必须先拷贝旧值
mA++; mB++;
return temp; // 返回值(而非引用)——所以后置++开销更大
}
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p01/ch04/s06/overload_increment_decrement.cpp">p01/ch04/s06/overload_increment_decrement.cpp</a></small></p>
</section>
</section>
<!-- ===================== 模块 A:练 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 练习 95min</p>
<h2>动手:运算符重载 <span class="tag tag-practice"></span></h2>
</section>
<section>
<div class="task-card">
<h4>任务卡 1 · operator= + operator== <span class="tag tag-practice"></span></h4>
<p>给 Day1 写的类(或新写一个含指针成员的类)实现 <code>operator=</code>(注意先
释放旧资源)与 <code>operator==</code>,参照
<a class="filelink" target="_blank" href="../../../p01/ch04/s06/overload_assignment.cpp">overload_assignment.cpp</a>
<a class="filelink" target="_blank" href="../../../p01/ch04/s06/overload_equality.cpp">overload_equality.cpp</a></p>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 2 · operator[] 边界检查 <span class="tag tag-practice"></span></h4>
<p>实现一个重载了 <code>operator[]</code> 的简单容器类(固定大小数组包装),
越界访问要有明确行为(断言或抛异常),参照
<a class="filelink" target="_blank" href="../../../p01/ch04/s06/overload_pointer_operator.cpp">overload_pointer_operator.cpp</a> 的思路。</p>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 3 · 选做:友元实现 operator&lt;&lt; <span class="tag tag-practice"></span></h4>
<p>参照 <a class="filelink" target="_blank"
href="../../../p01/ch04/s06/overload_with_friend.cpp">overload_with_friend.cpp</a>
为你的类实现 <code>friend ostream&amp; operator&lt;&lt;</code>,讨论「为什么流输出运算符
通常不能写成成员函数」(提示:左操作数是 ostream,不是本类对象)。</p>
</div>
</section>
</section>
<!-- ===================== 模块 B:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 讲授 35min</p>
<h2>继承与多态 <span class="tag tag-lecture"></span></h2>
<ul>
<li>继承中构造/析构的顺序</li>
<li>同名成员遮蔽 vs 重写</li>
<li>虚继承解决菱形问题</li>
<li>upcast 裁切问题 → virtual 解决</li>
<li>纯虚函数/抽象类、虚析构的意义</li>
</ul>
</section>
<section>
<h3>构造先基类后派生类,析构相反 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class A{ public: A(){cout&lt;&lt;"A构造\n";} ~A(){cout&lt;&lt;"A析构\n";} };
class B : public A{ public: B(){cout&lt;&lt;"B构造\n";} ~B(){cout&lt;&lt;"B析构\n";} };
class C : public B{ public: C(){cout&lt;&lt;"C构造\n";} ~C(){cout&lt;&lt;"C析构\n";} };
int main(){ C c; }
// 输出顺序:A构造 → B构造 → C构造 → C析构 → B析构 → A析构
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p01/ch04/s07/inheritance_ctor_dtor_order.cpp">p01/ch04/s07/inheritance_ctor_dtor_order.cpp</a></small></p>
</section>
<section>
<h3>同名成员:默认遮蔽,需类名限定 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class Base{ public: Base():mParam(0){} int mParam; };
class Derived : public Base{
public:
Derived():mParam(10){}
void Print(){
cout &lt;&lt; Base::mParam &lt;&lt; endl; // 必须类名限定才能访问被遮蔽的基类成员
cout &lt;&lt; mParam &lt;&lt; endl; // 默认访问派生类自己的
}
int mParam;
};
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p01/ch04/s07/same_name_members.cpp">p01/ch04/s07/same_name_members.cpp</a></small></p>
</section>
<section>
<h3>菱形继承 → 虚继承 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class BigBase{ public: int mParam; };
class Base1 : public BigBase{};
class Base2 : public BigBase{};
class Derived : public Base1, public Base2{};
// derived.mParam 二义——两份 BigBase 子对象;须写 derived.Base1::mParam 消歧义
// 虚继承(public virtual BigBase)能让两条路径共享同一份 BigBase 子对象
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p01/ch04/s07/diamond_virtual_inheritance.cpp">diamond_virtual_inheritance.cpp</a>
<a class="filelink" target="_blank"
href="../../../p01/ch04/s07/diamond_virtual_inheritance_2.cpp">_2.cpp</a>(虚继承版本对比)</small></p>
</section>
<section>
<h3>upcast 裁切问题:不加 virtual 就没有多态 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
// 问题版:Animal::speak_1 不是 virtual
class Animal{ public: void speak_1(){ cout&lt;&lt;"动物在唱歌...\n"; } };
class Dog : public Animal{ public: void speak_1(){ cout&lt;&lt;"小狗在唱歌...\n"; } };
void DoBussiness(Animal& animal){ animal.speak_1(); }
void test(){ Dog dog; DoBussiness(dog); } // 输出「动物在唱歌」——静态绑定,不是想要的结果
// 解决版:都加 virtual → 动态绑定,输出「小狗在唱歌」
</code></pre>
<p><small>问题版出处:<a class="filelink" target="_blank"
href="../../../p01/ch04/s08/upcast_problem.cpp">upcast_problem.cpp</a>;解决版:
<a class="filelink" target="_blank"
href="../../../p01/ch04/s08/virtual_function_solution.cpp">virtual_function_solution.cpp</a></small></p>
</section>
<section>
<h3>纯虚函数、抽象类、虚析构的意义 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class People{
public:
virtual void showName() = 0; // 纯虚函数 → People 是抽象类,不能实例化
virtual ~People(){ cout&lt;&lt;"析构 People\n"; } // 虚析构:基类指针 delete 派生类对象时能正确调用派生类析构
};
class Worker : public People{
public:
virtual void showName(){ /* ... */ }
~Worker(){ cout&lt;&lt;"析构 Worker\n"; delete[] pName; }
private:
char* pName;
};
People* p = new Worker;
delete p; // 若 ~People() 不是 virtual,只会调用 ~PeopleWorker::pName 泄漏
</code></pre>
<p><small>强化点:<code class="filelink">ERRATA.md [58954469:5] abstract-instantiation</code>
——wiki 原文一度把带纯虚析构的类直接实例化,这是编译错误。出处:
<a class="filelink" target="_blank" href="../../../p01/ch04/s08/pure_virtual_dtor.cpp">pure_virtual_dtor.cpp</a>
<a class="filelink" target="_blank" href="../../../p01/ch04/s08/virtual_dtor_purpose.cpp">virtual_dtor_purpose.cpp</a></small></p>
</section>
</section>
<!-- ===================== 模块 B:练 ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 练习 95min</p>
<h2>动手:继承与多态 <span class="tag tag-practice"></span></h2>
</section>
<section>
<div class="task-card">
<h4>任务卡 1 · 复现并修复 upcast 裁切 <span class="tag tag-practice"></span></h4>
<p>先跑 <a class="filelink" target="_blank"
href="../../../p01/ch04/s08/upcast_problem.cpp">upcast_problem.cpp</a> 观察输出,
再手动加 <code>virtual</code> 关键字重新编译,对比两次输出差异,写清楚“静态绑定 vs 动态绑定”
各自发生在什么时候。</p>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 2 · 基类指针管理派生类对象 <span class="tag tag-practice"></span></h4>
<p>设计一个含虚析构的基类 + 2 个派生类(各自持有一个堆内存成员),用基类指针数组
管理多个对象,逐个 <code>delete</code>,通过析构函数里的打印验证:(1) 调用的是派生类
析构 (2) 没有内存泄漏(可用打印计数或 valgrind 验证)。</p>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 3 · 选做:普通继承 vs 虚继承对比 <span class="tag tag-practice"></span></h4>
<p>对照 <a class="filelink" target="_blank"
href="../../../p01/ch04/s07/diamond_virtual_inheritance.cpp">diamond_virtual_inheritance.cpp</a>
<a class="filelink" target="_blank"
href="../../../p01/ch04/s07/diamond_virtual_inheritance_2.cpp">_2.cpp</a>
打印 <code>sizeof(Derived)</code> 并解释两种继承方式下对象体积差异的原因。</p>
</div>
</section>
</section>
<!-- ===================== 收尾 ===================== -->
<section>
<h2>当日产出验证</h2>
<div class="verify-box">
<h4>验收标准</h4>
<pre><code class="bash" data-trim>cmake --build build --target all_p01_ch04_s06
cmake --build build --target all_p01_ch04_s07
cmake --build build --target all_p01_ch04_s08</code></pre>
<p>全绿;产出「基类指针管理派生类对象、正确释放」的可运行小程序。</p>
</div>
<p class="footer-note"><small>明日预告:Day 3 —— 泛型与标准库(模板 / 类型转换 /
异常 / IO 流 / STL</small></p>
<p><a class="filelink" href="index.html">返回目录</a></p>
</section>
</div>
</div>
<script src="reveal/reveal.js"></script>
<script src="reveal/plugin/highlight/highlight.js"></script>
<script>
Reveal.initialize({
hash: true, controls: true, progress: true, center: true, slideNumber: true,
plugins: [ RevealHighlight ]
});
</script>
</body>
</html>