b89819d5db
大纲按「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)供离线播放,代码链接可点开查看源码。
312 lines
14 KiB
HTML
312 lines
14 KiB
HTML
<!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>模块 B(3 课时):继承与多态</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<< <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& operator<<</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<<"A构造\n";} ~A(){cout<<"A析构\n";} };
|
||
class B : public A{ public: B(){cout<<"B构造\n";} ~B(){cout<<"B析构\n";} };
|
||
class C : public B{ public: C(){cout<<"C构造\n";} ~C(){cout<<"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 << Base::mParam << endl; // 必须类名限定才能访问被遮蔽的基类成员
|
||
cout << mParam << 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<<"动物在唱歌...\n"; } };
|
||
class Dog : public Animal{ public: void speak_1(){ cout<<"小狗在唱歌...\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<<"析构 People\n"; } // 虚析构:基类指针 delete 派生类对象时能正确调用派生类析构
|
||
};
|
||
class Worker : public People{
|
||
public:
|
||
virtual void showName(){ /* ... */ }
|
||
~Worker(){ cout<<"析构 Worker\n"; delete[] pName; }
|
||
private:
|
||
char* pName;
|
||
};
|
||
People* p = new Worker;
|
||
delete p; // 若 ~People() 不是 virtual,只会调用 ~People,Worker::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>
|