Files
scuc-qt-course/docs/teaching/slides/day2.html
T
张宗平 b89819d5db 新增 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)供离线播放,代码链接可点开查看源码。
2026-07-01 16:51:12 +08:00

312 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>