IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> [C++] 类的继承 -> 正文阅读

[C++知识库][C++] 类的继承

类的继承

C++中类与类之间的继承有三种方式

  • 一是静态继承
  • 二是动态继承
  • 三是虚继承

其中静态基础和动态继承互斥, 虚继承可与静态继承或动态继承之一组合.

静态继承+单继承

无虚函数或虚表的类的继承为静态继承.
设有一个不含虚函数和虚表的类

struct A { ... };

另一个同样不含虚函数和虚表的类继承该类时就会发生静态继承

struct B : A { ... };

静态继承实质上是复制基类的内容到子类, 从而构建出新的类型.
除了共有的成员变量的偏移是一样的之外, 基类和子类之间无其他更多关联.

在其上调用类函数时, 用哪个类的指针, 就直接调用哪个类的函数, 无任何多态性质.

void* p = new B;
((B*) p)->f(); // 调用 B::f()
((A*) p)->f(); // 调用 A::f()
void* p = new A;
((B*) p)->f(); // 调用 B::f() 不过如果函数使用了B类的成员变量那么会造成越界访问
((A*) p)->f(); // 调用 A::f()

静态继承+多继承

C++允许多继承, 其实现方式为子类复制各个基类的内容到子类.
此时基类成员变量的偏移与子类中继承的成员变量的偏移将不一致, 若像单继承那样直接用基类指针指向子类对象并调用基类函数将由于成员变量偏移不一致, 导致基类函数访问到错误的子类对象中继承得到的基类成员变量.

struct A { int a; };
struct B { int b; int getA() { return b; } };
struct C : A, B {};
void* c = new C{ 1, 2 };
((C*) c)->getB(); // 得到 2
((B*) c)->getB(); // 错误地得到 1
offsetof(C, b) // 得到 4
offsetof(B, b) // 得到 0

若要用基类指针指向多继承的子类对象, 则需要先让指针转为子类指针, 然后再用static_cast转为基类指针. 该变换可以在编译期完成.

((B*)(C*) c)->getB(); // 得到 2

获取成员变量在类中的偏移, 有offsetof()宏, 但标准库并未提供获取基类在子类中的偏移的工具, 可以写一个宏自行实现:

#define typeoffsetof(t, s) (((size_t)(s*)(t*)1) - 1)
typeoffsetof(C, A) // 得到 0
typeoffsetof(C, B) // 得到 4

注: 0地址是特殊值, 0地址转任何类型都只会得到0地址

另外静态继承发生菱形继承时, 菱形继承链底端的子类将复制多份基类的内容, 分别来自菱形继承链腰部的各个类. 子类可通过腰部基类::顶部基类成员访问各个基类的内容

struct Z { int z; };
struct A : Z { int a; };
struct B : Z { int b; };
struct C : A, B {};
void* p = new C{ 1, 2, 3, 4 };
((C*) p)->A::z // 得到 1
((C*) p)->a // 得到 2
((C*) p)->B::z // 得到 3
((C*) p)->b // 得到 4

子类可以先转为腰部基类后再转为顶部基类来获取各个基类的内容.

((size_t)(Z*)(A*)(C*) 1) - 1 // 得到 0
((size_t)(Z*)(B*)(C*) 1) - 1 // 得到 8

动态继承

当类定义了虚函数后, 该类的对象即有了虚表, 从该类开始, 所有的子类对其父类的继承都为动态继承, 或者说动态继承有扩散的特性.

struct A { virtual void f() {}; }

如果子类不覆写(override)基类虚函数, 那么子类的虚表可直接复制自基类, 与基类完全一致(有虚继承亦可如此).
如果有覆写, 那么可以将基类的虚表复制一份到子类, 以此为模板, 用子类覆写的函数的地址覆盖基类的函数的地址.

类对象有了虚表, 即给对象打上了构造他的类的标记. 无论是用基类(仅限有虚表的基类), 还是用子类指针去调用对象的虚函数, 最终都会调用构造他的类的实现, 此即称为运行时多态.

虚继承

类继承时使用virtual的继承称为虚继承. 基类可有虚表亦可无虚表.

虚继承除了复制基类内容到子类外, 还添加了基类偏移指针(虚继承表)到子类, 子类访问基类内容时不直接取其偏移地址然后访问(这是类访问成员变量的一般方式), 而是通过基类偏移指针访问基类内容. 单论该类的对象来说, 由于增加了一层指针访问, 类中部分成员变量的访问变慢了(大约200ps).

struct Z { int z; };
struct A : Z {};
struct B : virtual Z {};
double base = timeIt([] {});
timeIt([b = new B]{ volatile int z = b->z; }) - base; // 846ps
timeIt([a = new A]{ volatile int z = a->z; }) - base; // 1062ps

但发生菱形继承时, 菱形继承链底端的多继承子类对象可以只含有一个顶端基类的内容, 其内腰部基类对象可以共享一个顶端基类的内容.

struct Z { int z; };
struct A : virtual Z {};
struct B : virtual Z {};
struct C : A, B {};
void* p = new C;
p->z; // 对 Z::z 的访问没有歧义了

基类被虚继承后其所有子类共享一个基类对象, 但非虚继承的还是会单独复制一份基类的内容

struct Z { int z; };
struct A : virtual Z {};
struct B : virtual Z {};
struct C : Z {};
struct D : A, B, C {};
void* p = new D;
((D*) p)->A::z = 1;
((D*) p)->B::z = 2;
((D*) p)->C::z = 3;
((D*) p)->A::z == 2;
((D*) p)->B::z == 2;
((D*) p)->C::z == 3;

子类中如果有虚继承, 那么所有从C中来的对各成员用大括号初始化的语法(默认构造函数)将不可用, 需要显式写构造函数来初始化.

虚继承而来的成员变量不可取成员地址, 只可以取普通的地址

&D::A::z; // 不可以 得到空指针
&D::B::z; // 不可以 得到空指针
&D::C::z; // 不可以 得到空指针
void* p = new D;
auto p1 = &((D*) p)->A::z; // 可以
auto p2 = &((D*) p)->B::z; // 可以 有 p1 == p2
auto p3 = &((D*) p)->C::z; // 可以 有 p3 != p1

各基类内容在内存布局上, 从地址低位开始, 是非虚继承而来的成员排在前列, 然后才是虚继承而来的成员

虚表 虚继承表

如果类中含虚函数, 那么该类及其所有子类都有虚表(虚函数表)
如果类中虚继承了一个基类, 那么该类及其所有子类都有虚继承表

接口

一般称呼一个没有成员变量只有虚函数的类为接口或接口类. 此类由于没有成员变量而没有多继承的问题, 也没有虚继承的必要.
一些面向对象语言(java等)中, 将C++的类进一步细化为一般类和接口两种, 并要求一般类不允许除接口之外的多继承. 没有静态继承, 一切函数都为虚函数.

而C++中为复用基类数据而使用的虚继承, 在这些语言中可改写为将基类做为成员变量来模拟多继承乃至菱形继承, 改继承链为引用链.

附录

计时函数实现

template<typename T>
double timeIt(T const& func) {
    LARGE_INTEGER t1, t2, tc;
    QueryPerformanceFrequency(&tc);
    size_t sum = 0;
    size_t cnt = 0;
    while (sum < 1000000000 / 50) {
        cnt++;
        QueryPerformanceCounter(&t1);
        func(); func(); func(); func(); func();
        func(); func(); func(); func(); func();

        func(); func(); func(); func(); func();
        func(); func(); func(); func(); func();

        func(); func(); func(); func(); func();
        func(); func(); func(); func(); func();

        func(); func(); func(); func(); func();
        func(); func(); func(); func(); func();

        func(); func(); func(); func(); func();
        func(); func(); func(); func(); func();
        QueryPerformanceCounter(&t2);
        sum += (t2.QuadPart - t1.QuadPart) * 1000000000 / 50 / tc.QuadPart;
    }
    return (double) sum / cnt;
}
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-12-25 10:47:31  更:2022-12-25 10:51:02 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年4日历 -2024/4/20 18:34:03-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码