编程导论(2024-2025冬季学期)
第10章:继承(Vererbung)
课程信息
- 授课教师:亚历山大·克劳斯博士(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
第10章核心内容
- 类的核心目标回顾与继承的必要性
- 继承的基本概念与语法
- 访问权限控制(public/protected/private)
- 继承中的构造函数与方法重写
- 多类继承示例与循环依赖解决方案
- 抽象类与纯虚方法
1. 继承的背景与必要性
1.1 类的核心目标回顾
此前学习的类的核心特性包括: - 数据封装:隐藏私有属性,仅通过方法访问 - 方法封装:将类专属函数与数据绑定 - 构造函数/析构函数:对象的初始化与资源释放 - 方法与构造函数重载:同一名称多实现(参数签名不同) - 运算符重载:自定义类的运算符行为
新增核心目标:高效代码复用——通过继承(Vererbung) 实现。
1.2 传统代码复用的问题
以“银行账户”建模为例:
- 已定义Konto(账户)类,包含账户基本属性(如余额)和方法(如存款、取款)
- 需新增Girokonto(支票账户)类,其功能基于Konto,但需额外添加“透支额度”属性和“转账”方法
传统方案:复制Konto代码→重命名为Girokonto→修改/新增属性和方法
缺陷:
1. 维护成本高:若Konto需新增功能(如“利息计算”),需手动同步到Girokonto
2. 错误率高:复制过程中易遗漏或修改错误
3. 代码冗余:重复存储相同属性和方法的实现
2. 继承的基本概念与语法
2.1 继承的核心定义
- ** Oberklasse(父类/基类)**:提供基础属性和方法的类(如
Konto、KString) - ** Unterklasse(子类/派生类)**:从父类继承并扩展功能的类(如
Girokonto、KVersalien) - 继承关系:子类自动获得父类的属性和方法,可新增属性/方法,或重写(override)父类方法
- 对象兼容性:子类的每个实例同时也是父类的实例(如“支票账户”一定是“账户”)
2.2 C++继承语法
// 父类定义
class 父类名 {
// 父类属性与方法
};
// 子类定义:public继承(最常用)
class 子类名 : public 父类名 {
// 子类新增的属性与方法
// 可重写父类的方法(需保持参数签名一致)
};
示例:定义KVersalien(大写字符串类)继承自KString(基础字符串类)
// 父类:基础字符串类
class KString {
protected:
char* mString; // 字符串指针(指向char数组)
int mLength; // 字符串长度(不含'\0')
public:
KString(char const* s); // 构造函数
~KString(); // 析构函数
void setString(char const* s); // 设置字符串
char* getString(); // 获取字符串
int length(); // 获取长度
void print(); // 输出字符串
};
// 子类:大写字符串类(继承自KString)
class KVersalien : public KString {
private:
void up(); // 私有方法:将字符串转为大写
public:
KVersalien(char const* s); // 子类构造函数
void setString(char const* s); // 重写父类方法
void print(); // 重写父类方法
};
3. 继承中的访问权限控制
3.1 三种访问权限的核心区别
父类的属性/方法通过不同访问权限修饰,决定子类和外部的访问范围:
| 访问权限修饰符 | 父类内部 | 子类内部 | 类外部(其他对象) |
|---|---|---|---|
public |
✅ 可访问 | ✅ 可访问 | ✅ 可访问 |
protected |
✅ 可访问 | ✅ 可访问 | ❌ 不可访问 |
private |
✅ 可访问 | ❌ 不可访问 | ❌ 不可访问 |
3.2 访问权限的实际意义
-
private(私有):完全隐藏的“私有信息”
父类仅自身可访问,子类和外部均无法直接操作(如KString中若mString为private,KVersalien无法直接修改字符串内容)。 -
protected(受保护):仅家族可见的“家族信息”
父类和其子类(及子类的子类)可访问,外部不可访问(如KString的mString设为protected,KVersalien可直接操作字符串,而外部对象无法直接修改)。 -
public(公开):对外暴露的“接口信息”
父类、子类、外部均能访问(如KString的length()方法,所有对象均可调用)。
3.3 继承方式对访问权限的影响
C++支持三种继承方式(public/protected/private),会改变父类成员在子类中的访问权限:
| 父类成员权限 | public继承(常用) |
protected继承(少见) |
private继承(极少见) |
|---|---|---|---|
public |
子类中仍为public |
子类中变为protected |
子类中变为private |
protected |
子类中仍为protected |
子类中仍为protected |
子类中变为private |
private |
子类中不可访问 | 子类中不可访问 | 子类中不可访问 |
说明:
- public继承:保持父类接口开放性,是绝大多数场景的选择(如“支票账户”继承“账户”,外部仍可调用存款方法)。
- protected/private继承:限制子类对外暴露父类接口,仅用于特殊封装场景(如隐藏父类的部分公开方法)。
4. 继承中的构造函数与方法重写
4.1 子类构造函数的执行顺序
子类对象创建时,先执行父类构造函数,再执行子类构造函数,确保父类属性先初始化。
示例:KVersalien(子类)构造函数
// KVersalien.h(子类声明)
class KVersalien : public KString {
private:
void up(); // 转为大写的私有方法
public:
KVersalien(char const* s); // 子类构造函数
// 重写父类方法
void setString(char const* s);
void print();
};
// KVersalien.cpp(子类实现)
#include <cctype> // 包含toupper()等字符处理函数
#include "KVersalien.h"
// 子类构造函数:初始化列表调用父类构造函数
KVersalien::KVersalien(char const* s) : KString(s) {
up(); // 父类构造后,执行子类逻辑(转为大写)
}
// 私有方法:将字符串转为大写
void KVersalien::up() {
for (int i = 0; i < mLength; ++i) {
if (islower(mString[i])) { // 判断是否为小写字母
mString[i] = toupper(mString[i]); // 转为大写
}
}
}
执行流程:
1. 调用KString(s)(父类构造函数):分配内存,复制字符串到mString,设置mLength。
2. 执行KVersalien构造函数体:调用up(),将mString中的小写字母转为大写。
4.2 方法重写(Override)
子类可重写父类的方法(需保持参数签名完全一致,区别于“重载”),实现子类专属逻辑。
示例1:重写setString方法
// 父类KString的setString方法(基础实现)
void KString::setString(char const* s) {
int newLength = strlen(s); // 计算新字符串长度
if (newLength > mLength) { // 若新长度超过当前容量,重新分配内存
delete[] mString;
mString = new char[newLength + 1]; // +1存储'\0'
}
strcpy(mString, s); // 复制新字符串
mLength = newLength; // 更新长度
}
// 子类KVersalien的setString方法(重写,新增大写转换)
void KVersalien::setString(char const* s) {
KString::setString(s); // 先调用父类方法,完成基础字符串设置
up(); // 子类扩展逻辑:转为大写
}
示例2:重写print方法
// 父类KString的print方法
void KString::print() {
cout << mString << endl;
}
// 子类KVersalien的print方法(重写,添加类标识)
void KVersalien::print() {
cout << "KVersalien::print: " << mString << endl;
}
4.3 继承的方法复用
子类自动继承父类未重写的方法,无需重复实现。
例如KVersalien未重写length()和getString(),直接使用KString的实现:
int main() {
KVersalien v1{"test"};
v1.print(); // 调用子类重写的print:输出"KVersalien::print: TEST"
cout << "长度: " << v1.length() << endl; // 调用父类length:输出4
return 0;
}
5. 多类继承示例与循环依赖解决
5.1 多类继承场景:人员建模
构建三级类层次结构:
- 父类Person:存储性别无关的属性(姓名、父母)和方法(设置父母、输出信息)。
- 子类Mann(男性):继承Person,新增“妻子”属性和“结婚”方法。
- 子类Frau(女性):继承Person,新增“丈夫”属性和“结婚”方法。
类声明(含循环依赖问题)
#include <string>
using namespace std;
// 问题:Person引用Mann和Frau,Mann/Frau又继承Person,形成循环依赖
class Person {
private:
string* mName; // 姓名(动态分配内存)
Frau* mMutter; // 母亲(指向Frau对象)
Mann* mVater; // 父亲(指向Mann对象)
public:
Person(string name);
string Name();
void setzeVater(Mann* m); // 设置父亲
void setzeMutter(Frau* f); // 设置母亲
void ausgabe(char const* s); // 输出信息
~Person();
};
class Frau : public Person {
private:
Mann* mEhemann; // 丈夫
public:
Frau(string name);
Frau(Person* p); // 从Person对象构造
void heirateMann(Mann* m); // 与男性结婚
Mann* EhefrauVon(); // 获取丈夫
};
class Mann : public Person {
private:
Frau* mEhefrau; // 妻子
public:
Mann(string name);
Mann(Person* p); // 从Person对象构造
void heirateFrau(Frau* f); // 与女性结婚
Frau* EhemannVon(); // 获取妻子
};
5.2 循环依赖的解决方案:前向声明
问题原因:Person类中使用Frau和Mann作为指针类型,但这两个类尚未定义,编译器无法识别。
解决方案:使用前向声明(Vorwärtsdeklaration),提前告知编译器“该类存在”。
修正后的类声明
#include <string>
using namespace std;
// 前向声明:告知编译器Frau和Mann是类(无需知道具体实现)
class Frau;
class Mann;
// 父类Person:此时Frau和Mann已声明,可作为指针类型使用
class Person {
private:
string* mName;
Frau* mMutter;
Mann* mVater;
public:
Person(string name);
string Name();
void setzeVater(Mann* m);
void setzeMutter(Frau* f);
void ausgabe(char const* s);
~Person();
};
// 子类Frau和Mann:此时Person已完整定义,可正常继承
class Frau : public Person {
private:
Mann* mEhemann;
public:
Frau(string name);
Frau(Person* p);
void heirateMann(Mann* m);
Mann* EhefrauVon();
};
class Mann : public Person {
private:
Frau* mEhefrau;
public:
Mann(string name);
Mann(Person* p);
void heirateFrau(Frau* f);
Frau* EhemannVon();
};
5.3 核心方法实现
1. 父类Person的实现
// 构造函数:初始化姓名和父母指针
Person::Person(string name) : mMutter(nullptr), mVater(nullptr) {
mName = new string{name}; // 动态分配字符串内存
}
// 析构函数:释放姓名内存
Person::~Person() {
delete mName;
}
// 获取姓名
string Person::Name() {
return *mName;
}
// 设置父亲
void Person::setzeVater(Mann* m) {
mVater = m;
}
// 设置母亲
void Person::setzeMutter(Frau* f) {
mMutter = f;
}
// 输出信息(递归输出父母信息)
void Person::ausgabe(char const* s) {
cout << s << " 姓名: " << *mName << endl;
if (mMutter != nullptr) {
cout << s << " 母亲: ";
mMutter->ausgabe(""); // 递归调用母亲的ausgabe
}
if (mVater != nullptr) {
cout << s << " 父亲: ";
mVater->ausgabe(""); // 递归调用父亲的ausgabe
}
}
2. 子类Frau和Mann的实现
// Frau构造函数1:直接初始化
Frau::Frau(string name) : Person(name), mEhemann(nullptr) {}
// Frau构造函数2:从Person对象构造
Frau::Frau(Person* p) : Person(p->Name()), mEhemann(nullptr) {}
// Frau:与男性结婚
void Frau::heirateMann(Mann* m) {
mEhemann = m;
}
// Frau:获取丈夫
Mann* Frau::EhefrauVon() {
return mEhemann;
}
// Mann构造函数1:直接初始化
Mann::Mann(string name) : Person(name), mEhefrau(nullptr) {}
// Mann构造函数2:从Person对象构造
Mann::Mann(Person* p) : Person(p->Name()), mEhefrau(nullptr) {}
// Mann:与女性结婚
void Mann::heirateFrau(Frau* f) {
mEhefrau = f;
}
// Mann:获取妻子
Frau* Mann::EhemannVon() {
return mEhefrau;
}
完整举例
// 使用常用标准库(推荐替代 #include <bits/stdc++.h>)
#include <iostream>
#include <string>
using namespace std;
// 前向声明(Forward Declarations)
// 因为 Frau 和 Mann 在 Person 中作为指针使用,只需声明,无需包含完整定义
class Frau;
class Mann;
// =========================
// 基类:Person(人)
// 包含姓名、父母指针,是 Mann 和 Frau 的父类
// =========================
class Person {
private:
string* mName; // 姓名(动态分配)
Frau* mMutter; // 指向母亲的指针
Mann* mVater; // 指向父亲的指针
public:
// 构造函数:初始化姓名,父母初始为空
Person(string name);
// 获取姓名(注意:函数名应为 Name,与声明一致)
string Name();
// 设置父母
void setzeVater(Mann* vater); // 设置父亲
void setzeMutter(Frau* mutter); // 设置母亲
// 输出信息
void ausgabe(char const* prefix);
// 析构函数:释放动态分配的 mName
~Person();
};
// -------------------------
// Person 成员函数实现
// -------------------------
// 构造函数:使用初始化列表设置 mName, mMutter, mVater
Person::Person(string name)
: mMutter(0) // 母亲指针初始为空
, mVater(0) // 父亲指针初始为空
{
mName = new string(name); // 动态分配字符串
}
// 析构函数:释放 mName
Person::~Person() {
delete mName;
}
// 获取姓名(注意:函数名是 Name,不是 name)
string Person::Name() {
return *mName;
}
// 设置父亲
void Person::setzeVater(Mann* vater) {
mVater = vater;
}
// 设置母亲
void Person::setzeMutter(Frau* mutter) {
mMutter = mutter;
}
// =========================
// 派生类:Frau(女性)
// 继承自 Person,并添加配偶(丈夫)指针
// =========================
class Frau : public Person {
private:
Mann* mEhemann; // 丈夫指针
public:
// 构造函数:通过姓名创建女性
Frau(string name);
// 构造函数:通过 Person 对象创建女性(复制姓名)
Frau(Person* p);
// 结婚:嫁给某个男性
void heirateMann(Mann* mann);
// 获取丈夫(谁是我丈夫?)
Mann* EhemannVon(); // 更正:应为“EhemannVon”表示“丈夫是谁”
};
// -------------------------
// Frau 成员函数实现
// -------------------------
Frau::Frau(string name)
: Person(name) // 调用基类构造函数
, mEhemann(0) // 初始无配偶
{}
Frau::Frau(Person* p)
: Person(p->Name()) // 复制传入 Person 的姓名
, mEhemann(0)
{}
// 女性嫁给某个男性
void Frau::heirateMann(Mann* mann) {
mEhemann = mann;
}
// 返回丈夫指针
Mann* Frau::EhemannVon() {
return mEhemann;
}
// =========================
// 派生类:Mann(男性)
// 继承自 Person,并添加配偶(妻子)指针
// =========================
class Mann : public Person {
private:
Frau* mEhefrau; // 妻子指针
public:
// 构造函数:通过姓名创建男性
Mann(string name);
// 构造函数:通过 Person 对象创建男性
Mann(Person* p);
// 结婚:娶某个女性
void heirateFrau(Frau* frau);
// 获取妻子(谁是我妻子?)
Frau* EhefrauVon(); // 更正:应为“EhefrauVon”表示“妻子是谁”
};
// -------------------------
// Mann 成员函数实现
// -------------------------
Mann::Mann(string name)
: Person(name) // 调用基类构造函数
, mEhefrau(nullptr) // 初始无配偶
{}
Mann::Mann(Person* p)
: Person(p->Name()) // 复制姓名
, mEhefrau(nullptr)
{}
// 男性娶某个女性
void Mann::heirateFrau(Frau* frau) {
mEhefrau = frau;
}
// 返回妻子指针
Frau* Mann::EhefrauVon() {
return mEhefrau;
}
// =========================
// 全局函数:结婚与离婚
// =========================
// 让一位女性和一位男性结婚
// 返回 true 表示成功,false 表示失败(如已婚)
bool verheirate(Frau* f, Mann* m) {
// 检查指针是否为空
if (f == 0 || m == 0)
return false;
// 检查是否已婚:女性已有丈夫 或 男性已有妻子
if (f->EhemannVon() != nullptr || m->EhefrauVon() != nullptr)
return false;
// 双向绑定:女性嫁给男性,男性娶女性
f->heirateMann(m);
m->heirateFrau(f);
return true;
}
// 输出信息,带前缀(如 "A: ")
void Person::ausgabe(char const* prefix) {
cout << prefix << "Name: " << *mName << endl;
// 如果母亲存在,递归输出母亲信息
if (mMutter != nullptr) {
cout << prefix << "Mutter: ";
mMutter->ausgabe(""); // 递归调用,输出母亲姓名
}
// 如果父亲存在,递归输出父亲信息
if (mVater != nullptr) {
cout << prefix << "Vater: ";
mVater->ausgabe(""); // 递归调用,输出父亲姓名
}
}
// 让一对夫妻离婚
// 返回 true 表示成功
bool trenne(Frau* f, Mann* m) {
// 检查指针有效性
if (f == nullptr || m == nullptr)
return false;
// 检查是否确实是夫妻
if (f->EhemannVon() != m || m->EhefrauVon() != f)
return false;
// 解除婚姻关系:设为 nullptr
f->heirateMann(nullptr);
m->heirateFrau(nullptr);
return true;
}
// =========================
// 主函数:测试
// =========================
int main() {
// 创建两个人:Anton(男)和 Bertha(女)
Mann* Anton = new Mann("Anton");
Frau* Bertha = new Frau("Bertha");
// 创建 Carl,他是 Anton 和 Bertha 的儿子
Mann* Carl = new Mann("Carl");
Carl->setzeVater(Anton); // 设置父亲
Carl->setzeMutter(Bertha); // 设置母亲
// 输出 Anton 和 Bertha 的信息
Anton->ausgabe("A: ");
Bertha->ausgabe("B: ");
cout << endl;
// 输出 Carl 的信息(会递归输出父母)
Carl->ausgabe("\tC: ");
cout << endl;
// 两人结婚
verheirate(Bertha, Anton);
cout << "Hochzeit: "
<< Bertha->Name() << " + " << Anton->Name()
<< endl << endl;
// 验证婚姻关系
if (Bertha->EhemannVon() != nullptr) {
cout << Bertha->Name() << "s Ehemann ist "
<< Bertha->EhemannVon()->Name() << endl;
}
if (Anton->EhefrauVon() != nullptr) {
cout << Anton->Name() << "s Ehefrau ist "
<< Anton->EhefrauVon()->Name() << endl;
}
// 最后释放所有动态分配的内存
delete Anton;
delete Bertha;
delete Carl;
return 0;
}
—— 本文来自火龙信奥(义乌睿码科技):义乌青少年信息学奥赛与编程教育平台,专注 CSP-J/S、NOIP、GESP 竞赛培训,线上线下融合教学,助力编程升学。网址:hlcoding.com