编程导论(2024-2025冬季学期)
第11章:虚方法(Virtuelle Methoden)
课程信息
- 授课教师:亚历山大·克劳斯博士(Dr.-Ing. Alexander Krause)
- 所属机构:多特蒙德工业大学(Technische Universität Dortmund)应用信息学12系系统软件工作组
- 课程链接:https://sys.cs.tu-dortmund.de/de/lehre/ws24/eidp/
- 联系邮箱:eidp-problems@ls12.cs.tu-dortmund.de
第11章核心内容
- 继承中的方法绑定问题(静态绑定 vs 动态绑定)
- 虚方法的定义与动态绑定实现
- 纯虚方法与抽象类
- 虚析构函数(构造函数不可为虚)
1. 继承中的方法绑定问题
1.1 传统继承的局限性:静态绑定
此前学习的继承中,方法与对象的绑定发生在编译期(静态绑定)——即编译时已确定调用哪个类的方法,与运行时对象的实际类型无关。
示例:水果类层次结构(无虚方法)
定义四级类层次:Frucht(水果,父类)→ HFrucht(荚果)/Obst(水果子类,如苹果)→ SFrucht(热带水果,继承自Obst),均重写ausgabe()方法输出类标识和名称。
#include <string>
#include <iostream>
using namespace std;
// 父类:Frucht(水果)
class Frucht {
protected:
string name; // 水果名称
public:
Frucht(char const* n) : name(n) {}
// 非虚方法:静态绑定
void ausgabe() {
cout << "(F) " << name << endl;
}
};
// 子类1:HFrucht(荚果,继承Frucht)
class HFrucht : public Frucht {
public:
HFrucht(char const* n) : Frucht(n) {}
void ausgabe() {
cout << "(HF) " << name << endl;
}
};
// 子类2:Obst(水果子类,如苹果,继承Frucht)
class Obst : public Frucht {
public:
Obst(char const* n) : Frucht(n) {}
void ausgabe() {
cout << "(O) " << name << endl;
}
};
// 子类3:SFrucht(热带水果,继承Obst)
class SFrucht : public Obst {
public:
SFrucht(char const* n) : Obst(n) {}
void ausgabe() {
cout << "(SF) " << name << endl;
}
};
测试代码与静态绑定问题
int main() {
// 1. 直接调用对象方法:输出符合预期
Frucht ruebe{"萝卜"}; // (F) 萝卜
HFrucht erbse{"豌豆"}; // (HF) 豌豆
Obst apfel{"苹果"}; // (O) 苹果
SFrucht banane{"香蕉"}; // (SF) 香蕉
ruebe.ausgabe();
erbse.ausgabe();
apfel.ausgabe();
banane.ausgabe();
// 2. 父类指针指向子类对象:问题出现(静态绑定)
Obst* a = &apfel; // 指向Obst对象,输出(O) 苹果(符合预期)
a->ausgabe();
Frucht* f = &apfel; // 指向Obst对象,但调用Frucht::ausgabe()(不符合预期)
f->ausgabe(); // 输出:(F) 苹果
Obst* b = &banane; // 指向SFrucht对象,但调用Obst::ausgabe()(不符合预期)
b->ausgabe(); // 输出:(O) 香蕉
return 0;
}
问题核心:父类指针/引用调用方法时,仅根据指针“声明类型”(如Frucht*)绑定方法,而非运行时“实际指向的对象类型”(如Obst/SFrucht)。
1.2 解决方案:动态绑定与虚方法
动态绑定:方法与对象的绑定发生在运行期,根据对象的实际类型确定调用的方法——需通过虚方法(virtual method) 实现。
2. 虚方法的定义与使用
2.1 虚方法的语法
在父类的方法声明前添加virtual关键字,即可将该方法标记为虚方法。子类重写该方法时,无需重复virtual(但添加也可,不影响),自动继承虚属性。
修正示例:将Frucht::ausgabe()设为虚方法
class Frucht {
protected:
string name;
public:
Frucht(char const* n) : name(n) {}
// 虚方法:动态绑定(仅需在父类声明时添加virtual)
virtual void ausgabe() {
cout << "(F) " << name << endl;
}
};
// 子类重写时无需重复virtual(自动继承虚属性)
class Obst : public Frucht {
public:
Obst(char const* n) : Frucht(n) {}
void ausgabe() { // 自动为虚方法
cout << "(O) " << name << endl;
}
};
class SFrucht : public Obst {
public:
SFrucht(char const* n) : Obst(n) {}
void ausgabe() { // 自动为虚方法
cout << "(SF) " << name << endl;
}
};
2.2 虚方法的动态绑定效果
重新运行测试代码,结果符合预期——方法调用由对象实际类型决定:
int main() {
// 父类指针指向子类对象:动态绑定到实际类型的方法
Frucht* f = &apfel; // 指向Obst对象,调用Obst::ausgabe()
f->ausgabe(); // 输出:(O) 苹果
Obst* b = &banane; // 指向SFrucht对象,调用SFrucht::ausgabe()
b->ausgabe(); // 输出:(SF) 香蕉
return 0;
}
2.3 虚方法的关键规则
- 仅需父类声明
virtual:子类重写时无需重复,自动继承虚属性(添加virtual仅为代码可读性,无语法意义)。 - 动态绑定仅适用于指针/引用:直接使用对象(非指针/引用)时,仍为静态绑定(因对象类型编译期确定)。 ```cpp SFrucht kiwi{"猕猴桃"}; Obst x = kiwi; // 对象拷贝(静态绑定),调用Obst::ausgabe() x.ausgabe(); // 输出:(O) 猕猴桃
Obst& xr = kiwi; // 引用(动态绑定),调用SFrucht::ausgabe()
xr.ausgabe(); // 输出:(SF) 猕猴桃
3. **未重写则继承父类实现**:若子类未重写虚方法,将调用最近的父类中的虚方法实现。cpp
class X {
public:
virtual void druck() { cout << "X"; }
};
class Y : public X {
public:
void druck() { cout << "Y"; } // 重写虚方法
};
class Z : public Y {}; // 未重写druck(),继承Y的实现
int main() { X* arr[3] = {new X, new Y, new Z}; for (int i=0; i<3; i++) arr[i]->druck(); // 输出:XYY(动态绑定) return 0; } ```
3. 纯虚方法与抽象类
3.1 纯虚方法的定义
纯虚方法:无具体实现的虚方法,用于强制子类必须重写该方法。
语法:在虚方法声明后添加= 0,且不提供方法实现(父类中仅声明)。
示例:将Frucht::ausgabe()设为纯虚方法
class Frucht {
protected:
string name;
public:
Frucht(char const* n) : name(n) {}
// 纯虚方法:无实现,强制子类重写
virtual void ausgabe() = 0;
};
3.2 抽象类的特性
- 抽象类定义:包含至少一个纯虚方法的类称为“抽象类(abstrakte Klasse)”。
- 核心限制:抽象类无法实例化对象,仅能作为父类被继承(“蓝图类”)。
- 子类要求:子类必须重写父类的所有纯虚方法,否则子类也会成为抽象类(无法实例化)。
示例:抽象类AusgabeGeraet(输出设备)
class AusgabeGeraet {
protected:
bool kannFarben; // 是否支持彩色输出
string data; // 输出数据
public:
AusgabeGeraet(bool farbe, string d) : kannFarben(farbe), data(d) {}
// 普通方法:调用纯虚方法(无需实现纯虚方法即可使用)
void Drucke() {
if (kannFarben) {
FarbDruck(); // 调用纯虚方法
} else {
cout << data; // 黑白输出
}
}
// 纯虚方法:强制子类实现彩色输出逻辑
virtual void FarbDruck() = 0;
};
// 子类1:激光打印机(支持彩色)
class LaserDrucker : public AusgabeGeraet {
public:
LaserDrucker(bool farbe, string d) : AusgabeGeraet(farbe, d) {}
// 重写纯虚方法:实现彩色输出
void FarbDruck() override { // override关键字可选,增强可读性
cout << "[彩色激光打印] " << data << endl;
}
};
// 子类2:黑白打印机(不支持彩色,仍需重写纯虚方法)
class SchwarzWeissDrucker : public AusgabeGeraet {
public:
SchwarzWeissDrucker() : AusgabeGeraet(false, "") {}
// 重写纯虚方法(虽不支持彩色,仍需满足语法要求)
void FarbDruck() override {
cout << "错误:该打印机不支持彩色输出!" << endl;
}
};
int main() {
// AusgabeGeraet g; // 错误:抽象类无法实例化
LaserDrucker ld(true, "报告文档");
ld.Drucke(); // 输出:[彩色激光打印] 报告文档
return 0;
}
4. 虚析构函数(构造函数不可为虚)
4.1 构造函数不可为虚的原因
- 构造函数的作用是初始化对象(分配内存、初始化属性),而虚方法的动态绑定依赖对象的“虚函数表”(运行时确定类型)。
- 对象创建时,虚函数表尚未初始化,无法实现动态绑定——因此C++禁止将构造函数声明为
virtual。
4.2 析构函数必须为虚的场景
当通过父类指针删除子类对象时,若父类析构函数非虚,仅会调用父类析构函数,导致子类的动态内存未释放(内存泄漏)。
解决方案:将父类析构函数声明为虚析构函数,确保运行时调用对象实际类型的析构函数。
示例:非虚析构函数的问题
class Base {
public:
// 非虚析构函数
~Base() {
cout << "Base析构" << endl;
}
};
class Derived : public Base {
private:
int* arr; // 子类动态分配的内存
public:
Derived() {
arr = new int[10]; // 分配内存
}
// 子类析构函数:释放动态内存
~Derived() {
delete[] arr;
cout << "Derived析构" << endl;
}
};
int main() {
Base* b = new Derived; // 父类指针指向子类对象
delete b; // 仅调用Base析构,Derived的arr未释放(内存泄漏)
// 输出:Base析构
return 0;
}
修正:父类析构函数设为虚
class Base {
public:
// 虚析构函数
virtual ~Base() {
cout << "Base析构" << endl;
}
};
class Derived : public Base {
// ... 同前 ...
};
int main() {
Base* b = new Derived;
delete b; // 先调用Derived析构,再调用Base析构(正确释放内存)
// 输出:
// Derived析构
// Base析构
return 0;
}
4.3 最佳实践
- 父类析构函数优先设为虚:只要类可能被继承,就将其析构函数声明为
virtual,避免内存泄漏风险。 - 子类析构函数无需重复
virtual:父类析构函数为虚时,子类析构函数自动继承虚属性(即使不写virtual)。
5. 本章总结
- 静态绑定 vs 动态绑定:
- 静态绑定:编译期根据指针声明类型绑定方法(传统非虚方法)。
- 动态绑定:运行期根据对象实际类型绑定方法(需
virtual关键字)。 - 虚方法:父类声明
virtual,子类重写实现动态绑定,支持多态。 - 纯虚方法与抽象类:
- 纯虚方法:
virtual 方法名(参数) = 0,无实现,强制子类重写。 - 抽象类:含纯虚方法的类,无法实例化,仅作父类使用。
- 虚析构函数:父类析构函数设为
virtual,确保通过父类指针删除子类对象时正确释放资源(构造函数不可为虚)。
—— 本文来自火龙信奥(义乌睿码科技):义乌青少年信息学奥赛与编程教育平台,专注 CSP-J/S、NOIP、GESP 竞赛培训,线上线下融合教学,助力编程升学。网址:hlcoding.com