Day 2

OOP 进阶

Qt 方向实训 · 第 2 / 4 天 · 讲:练 ≈ 1:2.7

  • 模块 A(3 课时):对象模型 / 友元 / 运算符重载
  • 模块 B(3 课时):继承与多态

回顾:Day1 已建立类/构造析构/深浅拷贝的直觉——今天在这个基础上深挖机制

模块 A · 讲授 35min

对象模型 / 友元 / 运算符重载

  • this 指针到底解决什么问题
  • 友元:突破封装的代价与取舍
  • 运算符重载:=、==、[]、()、++/-- 全景

this 指针:区分同名 + 隐式参数


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);
    }
};
    

出处:p01/ch04/s04/this_pointer.cpp

友元:全局函数 / 成员函数 / 整个类


class Building{
    friend void CleanBedRoom(Building& building);   // 友元全局函数
    friend class MyFriend;                          // 友元类——MyFriend 的所有成员都能访问私有区
public:
    Building();
private:
    string mBedroom;
};
    

友元是「单向」「不可传递」「不可继承」的特权授予——用得越多,封装边界越模糊, 权衡取舍要讲清楚。出处:p01/ch04/s05/friend_syntax.cpp

赋值运算符重载:与拷贝构造的分工


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 连续赋值
}
    

“初始化用拷贝构造,已存在对象再赋值用 operator=”——这条分界线是本节强化重点。 出处:p01/ch04/s06/overload_assignment.cpp

前置/后置 ++ 的重载差异


Complex& operator++(){        // 前置++:改自身,返回引用
    mA++; mB++;
    return *this;
}
Complex operator++(int){      // 后置++:占位 int 参数只是为了和前置区分重载签名
    Complex temp = *this;      // 必须先拷贝旧值
    mA++; mB++;
    return temp;               // 返回值(而非引用)——所以后置++开销更大
}
    

出处:p01/ch04/s06/overload_increment_decrement.cpp

模块 A · 练习 95min

动手:运算符重载

任务卡 1 · operator= + operator==

给 Day1 写的类(或新写一个含指针成员的类)实现 operator=(注意先 释放旧资源)与 operator==,参照 overload_assignment.cppoverload_equality.cpp

任务卡 2 · operator[] 边界检查

实现一个重载了 operator[] 的简单容器类(固定大小数组包装), 越界访问要有明确行为(断言或抛异常),参照 overload_pointer_operator.cpp 的思路。

任务卡 3 · 选做:友元实现 operator<<

参照 overload_with_friend.cpp, 为你的类实现 friend ostream& operator<<,讨论「为什么流输出运算符 通常不能写成成员函数」(提示:左操作数是 ostream,不是本类对象)。

模块 B · 讲授 35min

继承与多态

  • 继承中构造/析构的顺序
  • 同名成员遮蔽 vs 重写
  • 虚继承解决菱形问题
  • upcast 裁切问题 → virtual 解决
  • 纯虚函数/抽象类、虚析构的意义

构造先基类后派生类,析构相反


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析构
    

出处:p01/ch04/s07/inheritance_ctor_dtor_order.cpp

同名成员:默认遮蔽,需类名限定


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;
};
    

出处:p01/ch04/s07/same_name_members.cpp

菱形继承 → 虚继承


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 子对象
    

出处:diamond_virtual_inheritance.cpp_2.cpp(虚继承版本对比)

upcast 裁切问题:不加 virtual 就没有多态


// 问题版: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 → 动态绑定,输出「小狗在唱歌」
    

问题版出处:upcast_problem.cpp;解决版: virtual_function_solution.cpp

纯虚函数、抽象类、虚析构的意义


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 泄漏
    

强化点:ERRATA.md [58954469:5] abstract-instantiation ——wiki 原文一度把带纯虚析构的类直接实例化,这是编译错误。出处: pure_virtual_dtor.cppvirtual_dtor_purpose.cpp

模块 B · 练习 95min

动手:继承与多态

任务卡 1 · 复现并修复 upcast 裁切

先跑 upcast_problem.cpp 观察输出, 再手动加 virtual 关键字重新编译,对比两次输出差异,写清楚“静态绑定 vs 动态绑定” 各自发生在什么时候。

任务卡 2 · 基类指针管理派生类对象

设计一个含虚析构的基类 + 2 个派生类(各自持有一个堆内存成员),用基类指针数组 管理多个对象,逐个 delete,通过析构函数里的打印验证:(1) 调用的是派生类 析构 (2) 没有内存泄漏(可用打印计数或 valgrind 验证)。

任务卡 3 · 选做:普通继承 vs 虚继承对比

对照 diamond_virtual_inheritance.cpp_2.cpp, 打印 sizeof(Derived) 并解释两种继承方式下对象体积差异的原因。

当日产出验证

验收标准

cmake --build build --target all_p01_ch04_s06
cmake --build build --target all_p01_ch04_s07
cmake --build build --target all_p01_ch04_s08

全绿;产出「基类指针管理派生类对象、正确释放」的可运行小程序。

返回目录