【C++继承】深入浅出C++继承机制
💻作 者 简 介:曾 与 你 一 样 迷 茫,现 以 经 验 助 你 入 门 C++。
💡个 人 主 页:@笑口常开xpr 的 个 人 主 页
📚系 列 专 栏:C++ 炼 魂 场:从 青 铜 到 王 者 的 进 阶 之 路
✨代 码 趣 语:友 元 不 继 承 像 “爸 爸 的 朋 友”:爸 爸 的 好 朋 友(基 类 友 元),不 一 定 是 儿 子 的 好 朋 友;儿 子 想 让 他 进 门,得 自 己 再 递 张 邀 请 卡(子 类 重 新 声 明 友 元)。
💪代 码 千 行,始 于 坚 持,每 日 敲 码,进 阶 编 程 之 路。
📦gitee 链 接:gitee
文 章 目 录
- 一、继 承
- (1)定 义
- (2)格 式
- (3)继 承 关 系 和 访 问 限 定 符
- (4)继 承 基 类 成 员 访 问 方 式 的 变 化
- (5)基 类 和 派 生 类 对 象 赋 值 转 换
- (6)继 承 中 的 作 用 域
- (7)派 生 类 的 默 认 成 员 函 数
- (8)友 元
- (9)继 承 与 静 态 成 员
- 二、多 继 承 及 菱 形 继 承
- (1)单 继 承
- (2)多 继 承
- (3)菱 形 继 承
- 1、菱 形 继 承 的 问 题
- 2、虚 拟 继 承
- 3、virtual 的 原 理
- 三、总 结
继 承 是 C++ 面 向 对 象 编 程 实 现 代 码 复 用 的 核 心,也 是 构 建 类 层 次 结 构 的 基 础。初 学 者 常 被 访 问 权 限、对 象 转 换、作 用 域 隐 藏、多 继 承 问 题 等 困 扰,本 文 从 单 继 承 基 础 入 手,逐 步 拆 解 核 心 知 识 点,结 合 代 码 与 内 存 分 析,帮 你 理 清 逻 辑、避 开 陷 阱。
一、继 承
(1)定 义
继 承 机 制 是 面 向 对 象 程 序 设 计 使 代 码 可 以 复 用 的 最 重 要 的 手 段,它 允 许 程 序 员 在 保 持 原 有 类 特 性 的 基 础 上 进 行 扩 展,增 加 功 能,这 样 产 生 新 的 类,称 派 生 类。继 承 呈 现 了 面 向 对 象 程 序 设 计 的 层 次 结 构,体 现 了 由 简 单 到 复 杂 的 认 知 过 程。以 前 我 们 接 触 的 复 用 都 是 函 数 复 用,继 承 是 类 设 计 层 次 的 复 用。
(2)格 式
Person 是 父 类,也 称 作 基 类。Student 是 子 类,也 称 作 派 生 类。
(3)继 承 关 系 和 访 问 限 定 符
(4)继 承 基 类 成 员 访 问 方 式 的 变 化
- 基 类 private 成 员 在 派 生 类 中 无 论 以 什 么 方 式 继 承 都 是 不 可 见 的。这 里 的 不 可 见 是 指 基 类 的 私 有 成 员 还 是 被 继 承 到 了 派 生 类 对 象 中,但 是 语 法 上 限 制 派 生 类 对 象 不 管 在 类 里 面 还 是 类 外 面 都 不 能 去 访 问 它。
- 基 类 private 成 员 在 派 生 类 中 是 不 能 被 访 问,如 果 基 类 成 员 不 想 在 类 外 直 接 被 访 问,但 需 要 在 派 生 类 中 能 访 问,就 定 义 为 protected。可 以 看 出 保 护 成 员 限 定 符 是 因 继 承 才 出 现 的。
- 基 类 的 私 有 成 员 在 子 类 都 是 不 可 见。基 类 的 其 他 成 员 在 子 类 的 访 问 方 式 == Min(成 员 在 基 类 的 访 问 限 定 符,继 承 方 式),public > protected > private。
- 使 用 关 键 字 class 时 默 认 的 继 承 方 式 是 private,使 用 struct 时 默 认 的 继 承 方 式 是 public,不 过 最 好 显 示 的 写 出 继 承 方 式。
- 在 实 际 运 用 中 一 般 使 用 都 是 public 继 承,几 乎 很 少 使 用 protetced/private 继 承,也 不 提 倡 使 用 protetced/private 继 承,因 为 protetced/private 继 承 下 来 的 成 员 都 只 能 在 派 生 类 的 类 里 面 使 用,实 际 中 扩 展 维 护 性 不 强。
#include<iostream>
using namespace std;
class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "peter"; //姓名int _age = 18; //年龄
};
class Student : public Person
{
protected:int _stuid; //学号
};
class Teacher : public Person
{
protected:int _jobid; //工号
};
int main()
{Student s;Teacher t;s.Print();t.Print();return 0;
}
(5)基 类 和 派 生 类 对 象 赋 值 转 换
- 派 生 类 对 象 可 以 赋 值 给 基 类 的 对 象 / 基 类 的 指 针 / 基 类 的 引 用。这 里 有 个 操 作 叫 切 片 或 者 切 割。把 派 生 类 中 父 类 那 部 分 切 来 赋 值 过 去。
- 基 类 对 象 不 能 赋 值 给 派 生 类 对 象。
- 基 类 的 指 针 或 者 引 用 可 以 通 过 强 制 类 型 转 换 赋 值 给 派 生 类 的 指 针 或 者 引 用。但 是 必 须 是 基 类 的 指 针 是 指 向 派 生 类 对 象 时 才 是 安 全 的。这 里 基 类 如 果 是 多 态 类 型,可 以 使 用 RTTI 的 dynamic_cast 来 进 行 识 别 后 进 行 安 全 转 换。
#include<iostream>
using namespace std;
class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "peter"; //姓名int _age = 18; //年龄
};
class Student : public Person
{
protected:int _stuid; //学号
};
class Teacher : public Person
{
protected:int _jobid; //工号
};
int main()
{Person p;Student s;Teacher t;p = t;//子类可以给父类//t = p;//父类不可以给子类t = (Student)p;return 0;
}
子 类 可 以 给 父 类,即 向 上 转 换。子 类 对 象 是 特 殊 的 父 类 对 象,父 类 不 可 以 给 子 类。
子 类 对 象 可 以 赋 值 给 父 类 对 象 /指 针/引 用
Student sobj;
Person pobj = sobj;
Person* pp = &sobj;
Person& rp = sobj;
这 里 是 切 割,没 有 发 生 类 型 转 换,因 为 如 果 发 生 类 型 转 换,会 产 生 临 时 变 量,临 时 变 量 前 面 需 要 添 加 const,否 则 代 码 会 报 错。
基 类 的 指 针 可 以 通 过 强 制 类 型 转 换 赋 值 给 派 生 类 的 指 针
Student sobj;
pp = &sobj;
Student* ps1 = (Student*)pp;
ps1->_No = 10;
这 种 情 况 转 换 时 虽 然 可 以,但 是 会 存 在 越 界 访 问 的 问 题
Student sobj;
Person pobj = sobj;
pp = &pobj;
Student* ps2 = (Student*)pp;
ps2->_No = 10;
这 里 将 基 类 对 象 指 针 强 制 转 换 为 派 生 类 指 针,然 后 访 问 派 生 类 特 有 成 员 _No,这 会 导 致 未 定 义 行 为,可 能 造 成 内 存 越 界 和 程 序 崩 溃。
(6)继 承 中 的 作 用 域
- 在 继 承 体 系 中 基 类 和 派 生 类 都 有 独 立 的 作 用 域。
- 子 类 和 父 类 中 有 同 名 成 员,子 类 成 员 将 屏 蔽 父 类 对 同 名 成 员 的 直 接 访 问,这 种 情 况 叫 隐 藏,也 叫 重 定 义。(在 子 类 成 员 函 数 中,可 以 使 用 基 类::基 类 成 员 显 示 访 问)
- 需 要 注 意 的 是 如 果 是 成 员 函 数 的 隐 藏,只 需 要 函 数 名 相 同 就 构 成 隐 藏。
- 注 意 在 实 际 中 在 继 承 体 系 里 面 最 好 不 要 定 义 同 名 的 成 员。父 类 可 以 和 子 类 定 义 同 名 的 成 员,因 为 不 在 同 一 个 作 用 域 中。查 找 变 量 时,先 在 局 部 域 中 查 找,然 后 去 全 局 域 查 找。可 以 指 定 作 用 域 查 找,如 果 找 不 到,就 会 报 错。
#include<iostream>
using namespace std;
class Person
{
protected:string _name = "小李子"; // 姓名int _num = 111; // 身份证号
};
class Student : public Person
{
public:void Print(){cout << " 姓名:" << _name << endl;cout << " 身份证号:" << Person::_num << endl;cout << " 学号:" << _num << endl;//就近原则,先查找子类的作用域}
protected:int _num = 999; // 学号
};
int main()
{Student s1;s1.Print();return 0;
}
#include<iostream>
using namespace std;
class Person
{
public:void func(){cout << "Person" << endl;}
protected:string _name = "小李子"; // 姓名int _num = 111; // 身份证号
};
class Student : public Person
{
public:void func(int i){cout << "Student : public Person" << endl;}void Print(){cout << " 姓名:" << _name << endl;cout << " 身份证号:" << Person::_num << endl;cout << " 学号:" << _num << endl;//就近原则,先查找子类的作用域}
protected:int _num = 999; // 学号
};
int main()
{Student s1;s1.func(1);return 0;
}
子 类 和 父 类 的 两 个 func 构 成 隐 藏 / 重 定 义,父 子 类 域 中,成 员 函 数 名 相 同 就 构 成 函 数 重 载,函 数 重 载 的 前 提 是 在 同 一 个 作 用 域 中。
(7)派 生 类 的 默 认 成 员 函 数
6 个 默 认 成 员 函 数,“默 认” 的 意 思 就 是 指 我 们 不 写,编 译 器 会 帮 我 们 自 动 生 成 一 个,那 么 在 派 生 类 中,这 几 个 成 员 函 数 是 如 何 生 成 的 呢?
- 派 生 类 的 构 造 函 数 必 须 调 用 基 类 的 构 造 函 数 初 始 化 基 类 的 那 一 部 分 成 员。如 果 基 类 没 有 默 认 的 构 造 函 数,则 必 须 在 派 生 类 构 造 函 数 的 初 始 化 列 表 阶 段 显 示 调 用。
- 派 生 类 的 拷 贝 构 造 函 数 必 须 调 用 基 类 的 拷 贝 构 造 完 成 基 类 的 拷 贝 初 始 化。
- 派 生 类 的 operator= 必 须 要 调 用 基 类 的 operator= 完 成 基 类 的 复 制。
- 派 生 类 的 析 构 函 数 会 在 被 调 用 完 成 后 自 动 调 用 基 类 的 析 构 函 数 清 理 基 类 成 员。因 为 这 样 才 能 保 证 派 生 类 对 象 先 清 理 派 生 类 成 员 再 清 理 基 类 成 员 的 顺 序。
- 派 生 类 对 象 初 始 化 先 调 用 基 类 构 造 再 调 派 生 类 构 造。
- 派 生 类 对 象 析 构 清 理 先 调 用 派 生 类 析 构 再 调 基 类 的 析 构。
- 因 为 后 续 一 些 场 景 析 构 函 数 需 要 构 成 重 写,重 写 的 条 件 之 一 是 函 数 名 相 同(这 个 我 们 后 面 会 讲 解)。那 么 编 译 器 会 对 析 构 函 数 名 进 行 特 殊 处 理,处 理 成 destrutor(), 所 以 父 类 析 构 函 数 不 加 virtual 的 情 况 下,子 类 析 构 函 数 和 父 类 析 构 函 数 构 成 隐 藏 关 系。
#include<iostream>
using namespace std;
class Person
{
public:Person(){cout << "Person" << endl;}void func(){cout << "Person" << endl;}~Person(){cout << "~Person" << endl;}
protected:string _name;
};
class student : public Person
{
public:student(const char* name = "张三",int id = 0):_id(0),Person(name)//,_name(name){ }
protected:int _id;
};
int main()
{student s;return 0;
}
- C++ 规 定,派 生 类 不 能 初 始 化 父 类 的 成 员 变 量,父 类 的 成 员 变 量 可 以 通 过 父 类 的 构 造 函 数 或 者 通 过 派 生 类 的 匿 名 对 象 来 初 始 化。
- 派 生 类 会 调 用 父 类 的 构 造 函 数 初 始 化 成 员,初 始 化 时 先 初 始 化 父 类 的 成 员,然 后 初 始 化 子 类 的 成 员。
先 子 后 父 的 原 因 是 子 类 当 中 可 能 有 父 类 成 员。子 可 以 用 父,父 不 能 用 子。
#include<iostream>
using namespace std;
class Person
{
public:Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p){cout << "Person operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}~Person(){cout << "~Person()" << endl;delete _pstr;}
protected:string _name; // 姓名string* _pstr = new string("111111");
};
class student : public Person
{
public:student(const char* name = "张三",int id = 0):Person(name),_id(0)//,_name(name){ }student(const student& s):_id(s._id), Person(s){}student& operator=(const student& s){if (this != &s){Person::operator=(s);_id = s._id;}return *this;}~student(){//由于后面多态的原因,析构函数的函数名被特殊处理//统一处理成destructorcout << *_pstr << endl;//Person::~Person();//显示调用父类析构,无法保证先子后父//所以子类析构函数完成后,自动调用父类析构,这样就保证了先子后父delete _ptr;}
protected:int _id;int* _ptr = new int;
};
int main()
{student s1;student s2(s1);student s3("李四", 1);s1 = s3;return 0;
}
显 示 调 用 父 类 析 构,无 法 保 证 先 子 后 父。所 以 子 类 析 构 函 数 完 成 后,自 动 调 用 父 类 析 构,这 样 就 保 证 了 先 子 后 父。可 以 不 显 示 调 用 父 类 析 构,编 译 器 会 自 动 调 用。
(8)友 元
#include<iostream>
using namespace std;
class Student;
class Person
{
public:friend void Display(const Person& p, const Student& s);
protected:string _name; // 姓名
};
class Student : public Person
{
protected:int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{cout << p._name << endl;cout << s._stuNum << endl;
}
int main()
{Person p;Student s;Display(p, s);return 0;
}
友 元 函 数 不 能 继 承。
解 决 方 法
在 student 类 中 添 加 友 元 函 数 即 可。
class Student : public Person
{friend void Display(const Person& p, const Student& s);
protected:int _stuNum; // 学号
};
(9)继 承 与 静 态 成 员
基 类 定 义 了 static 静 态 成 员,则 整 个 继 承 体 系 里 面 只 有 一 个 这 样 的 成 员。无 论 派 生 出 多 少 个 子 类,都 只 有 一 个 static 成 员 实 例。
#include<iostream>
using namespace std;
class Person
{
public:Person() { ++_count; }
//protected:string _name; // 姓名
public:static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:int _stuNum; // 学号
};
class Graduate : public Student
{
protected:string _seminarCourse; // 研究科目
};
int main()
{Person p;Student s;cout << &p._name << endl;cout << &s._name << endl;cout << &p._count << endl;cout << &s._count << endl;cout << &Person::_count << endl;cout << &Student::_count << endl;return 0;
}
静 态 成 员 继 承 的 是 使 用 权,静 态 成 员 属 于 父 类 和 派 生 类,派 生 类 中 不 会 单 独 拷 贝 一 份。
int main()
{Person p;Student s1;Student s2;cout << p._count << endl;return 0;
}
子 类 的 构 造 必 须 调 用 父 类 的 构 造 函 数 来 完 成,所 以 这 里 的 结 果 为 3。
二、多 继 承 及 菱 形 继 承
一 个 类 同 时 具 有 多 个 类 的 特 征。
(1)单 继 承
一 个 子 类 只 有 一 个 直 接 父 类 时 称 这 个 继 承 关 系 为 单 继 承。
(2)多 继 承
一 个 子 类 有 两 个 或 以 上 直 接 父 类 时 称 这 个 继 承 关 系 为 多 继 承。
(3)菱 形 继 承
菱 形 继 承 是 多 继 承 的 一 种 特 殊 情 况。
1、菱 形 继 承 的 问 题
数 据 的 冗 余 性,从 下 面 的 对 象 成 员 模 型 构 造,可 以 看 出 菱 形 继 承 有 数 据 冗 余 和 二 义 性 的 问 题。在 Assistant 的 对 象 中 Person 成 员 会 有 两 份。
#include<iostream>
using namespace std;
class Person
{
public:string _name; // 姓名int _age;
};
class Student : public Person
{
protected:int _num; //学号
};
class Teacher : public Person
{
protected:int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};
int main()
{Assistant as;as.Teacher::_age = 18;as.Student::_age = 30;return 0;
}
需 要 显 示 指 定 访 问 哪 个 父 类 的 成 员 可 以 解 决 二 义 性 问 题,但 是 数 据 冗 余 问 题 无 法 解 决。这 样 会 有 二 义 性 无 法 明 确 知 道 访 问 的 是 哪 一 个。
2、虚 拟 继 承
修 改 菱 形 继 承 的 问 题。虚 拟 继 承 可 以 解 决 菱 形 继 承 的 二 义 性 和 数 据 冗 余 的 问 题。如 上 面 的 继 承 关 系,在Student 和 Teacher 的 继 承 Person 时 使 用 虚 拟 继 承,即 添 加 关 键 字 virtual 即 可 解 决 问 题。需 要 注 意 的 是,虚 拟 继 承 不 要 在 其 他 地 方 去 使 用。
class Student : virtual public Person
{
protected:int _num; //学号
};
class Teacher : virtual public Person
{
protected:int _id; // 职工编号
};
3、virtual 的 原 理
菱 形 继 承
#include<iostream>
using namespace std;
class A
{
public:int _a;
};
// class B : public A
class B : public A
{
public:int _b;
};
// class C : public A
class C : public A
{
public:int _c;
};
class D : public B, public C
{
public:int _d;
};
int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
菱 形 虚 拟 继 承
#include<iostream>
using namespace std;
class A
{
public:int _a;
};
// class B : public A
class B : virtual public A
{
public:int _b;
};
// class C : public A
class C : virtual public A
{
public:int _c;
};
class D : public B, public C
{
public:int _d;
};
int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;d._a = 0;return 0;
}
三、总 结
本 文 覆 盖 C++ 继 承 关 键 内 容:单 继 承 的 访 问 权 限、对 象 转 换、作 用 域 隐 藏、默 认 成 员 函 数;多 继 承 场 景;菱 形 继 承 的 二 义 性 / 数 据 冗 余 及 虚 拟 继 承 解 决 方 案。学 习 核 心 是 理 解 “类 层 次 与 内 存 布 局”,建 议 结 合 实 战 调 试,夯 实 基 础 以 应 对 后 续 多 态 学 习。