Qt 方向实训 · 第 2 / 4 天 · 讲:练 ≈ 1:2.7
回顾:Day1 已建立类/构造析构/深浅拷贝的直觉——今天在这个基础上深挖机制
模块 A · 讲授 35min
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);
}
};
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; // 返回值(而非引用)——所以后置++开销更大
}
模块 A · 练习 95min
给 Day1 写的类(或新写一个含指针成员的类)实现 operator=(注意先
释放旧资源)与 operator==,参照
overload_assignment.cpp、
overload_equality.cpp。
实现一个重载了 operator[] 的简单容器类(固定大小数组包装),
越界访问要有明确行为(断言或抛异常),参照
overload_pointer_operator.cpp 的思路。
参照 overload_with_friend.cpp,
为你的类实现 friend ostream& operator<<,讨论「为什么流输出运算符
通常不能写成成员函数」(提示:左操作数是 ostream,不是本类对象)。
模块 B · 讲授 35min
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析构
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;
};
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(虚继承版本对比)
// 问题版: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.cpp、
virtual_dtor_purpose.cpp
模块 B · 练习 95min
先跑 upcast_problem.cpp 观察输出,
再手动加 virtual 关键字重新编译,对比两次输出差异,写清楚“静态绑定 vs 动态绑定”
各自发生在什么时候。
设计一个含虚析构的基类 + 2 个派生类(各自持有一个堆内存成员),用基类指针数组
管理多个对象,逐个 delete,通过析构函数里的打印验证:(1) 调用的是派生类
析构 (2) 没有内存泄漏(可用打印计数或 valgrind 验证)。
对照 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
全绿;产出「基类指针管理派生类对象、正确释放」的可运行小程序。
明日预告:Day 3 —— 泛型与标准库(模板 / 类型转换 / 异常 / IO 流 / STL)