C++ 继承的底层设计与原理

C++ 作为面向对象语言,其次面向对象语言的重要特性封装、继承、多态,所以理解继承底层设计对于我们学习C++是非常重要的,其次他是C++的灵魂所在,本人也是走了些弯路所以打算深度学习一下!

环境

  1. GCC 8.3.0(本人的运行环境)
cpp 复制代码
# 查看class的结构信息(Dump class hierarchy information)
g++ -std=c++17 -O0 -fdump-lang-class main.cpp

# 编译  
g++ -std=c++17 -O0 main.cpp -o main
  1. Clang 13.0.0
cpp 复制代码
# 查看class的结构信息
clang++ -std=c++17 -O0 -c main.cpp -Xclang -fdump-vtable-layouts

虚表

C++的虚函数底层实现上采用的都是虚表(virtual table),虚表中会把虚函数真实的函数地址记录下来,方便函数调用直接使用,因此虚函数的性能会略差一下因为多了一次寻址! 注意: 这个只是大部分编译器的实现,例如GCC!

例如下面例子TestA定义了虚函数foo1,那么TestA会生成一个虚函数表,其中定义了 foo1 指向 TestA::foo1 , 其中TestB继承自A它会继承A的虚函数表,如果重写会覆盖重写的函数,下面这个例子B的虚函数表中定义了一个 foo1 指向 TestA::foo1 ,TestC继承了TestB,重写了foo1方法,因此TestC的虚表中变成了TestC::foo1

cpp 复制代码
#include <iostream>

struct TestA {
    virtual void foo1() {
        std::cout << "TestA.foo1\n";
    }
};

struct TestB : virtual TestA {};

struct TestC : virtual TestB {
    void foo1() {
        std::cout << "TestC.foo1\n";
    }
};

int main() {
    TestA *ab = new TestB();
    ab->foo1(); // 会查找TestB虚表中foo1函数地址(代码段地址),发现foo1为TestA::foo1,最终执行输出TestA.foo1

    TestA *ac = new TestC();
    ac->foo1();  // 会查找TestC虚表中foo1函数地址(代码段地址),发现foo1为TestC::foo1,最终执行输出TestC.foo1
}

虚表是C++元信息的体现,它记录了虚函数的函数地址,但是C++本质上并不会为runtime阶段提供类型的元信息,例如类型的字段、函数信息等,所以C++不支持反射。但是c++是一个直接面向内存的语言,只要拿到了内存什么语法限制(private?const?)都不存在了。

虚函数

虚函数是继承的核心,派生类允许重写父类的虚函数,进而实现多态!

代码示例: godbolt.org/z/TEEsYra7f 或者 coliru.stacked-crooked.com/a/9895523dd...

单继承

cpp 复制代码
#include <iostream>

struct TestA {
    virtual void foo1() {
        std::cout << "TestA.foo1\n";
    }
    virtual void foo2() {
        std::cout << "TestA.foo2\n";
    }
    void foo3() {
        std::cout << "TestA.foo3\n";
    }
    long arr[3];
};

struct TestB : TestA {
    virtual void foo1() {
        std::cout << "TestB.foo1\n";
    }
    virtual void foo3() {
        std::cout << "TestB.foo3\n";
    }
    long arr[3];
};

struct TestC : TestB {
    virtual void foo4() {
        std::cout << "TestC.foo4\n";
    }
};

// 理解这个需要理解 虚函数继承的实现方式和类对象的内存结构
struct TestAMemory {
    struct TestATable {
        void (*foo1)(); // 指向 TestA.foo1 函数 (offset=16)
        void (*foo2)(); // 指向 TestA.foo2 函数
    };
    TestATable *vptr;
    long arr[3];
};

// 普通继承的内存结构
// 优先定义基类,再定义派生类
struct TestBMemory {
    struct TestBTable {
        void (*foo1)(); // 指向 TestB.foo1 函数 (offset=16)
        void (*foo2)(); // 指向 TestA.foo2 函数 (这里可以看到虚表会拷贝基类全部的虚函数,不管是否重写)
        void (*foo3)(); // 指向 TestB.foo3 函数 (添加自己的虚函数)
    };
    TestBTable *vptr;
    // TestA.arr
    long arra[3];
    // TestB.arr
    long arrb[3];
};

int main() {
    TestA *a = new TestB();
    a->foo1(); // TestB.foo1
    a->foo2(); // TestA.foo2
    a->foo3(); // TestA.foo3

    ((TestB *)a)->foo1(); // TestB.foo1
    ((TestB *)a)->foo2(); // TestA.foo2
    ((TestB *)a)->foo3(); // TestB.foo3

    // TestB的内存结构,这个参考就行了 ... (实际上TestB成员函数地址拿不到的,因为他是编译信息.)
    TestBMemory *bb = (TestBMemory *)(a);
    bb->vptr->foo1(); // TestB.foo1
    bb->vptr->foo2(); // TestA.foo2
    bb->vptr->foo3(); // TestB.foo3

    // 修改成员变量
    bb->arra[0] = 111;
    bb->arrb[0] = 222;

    // 当基类和派生类定义类相同的字段/函数,那么主要是看当前指针的类型是什么,类型是基类那么就是基类的函数,否则派生类的函数
    std::cout << "(TestA)arr[0]: " << a->arr[0] << "\n";            // (TestA)arr[0]: 111
    std::cout << "(TestB)arr[0]: " << ((TestB *)a)->arr[0] << "\n"; // (TestB)arr[0]: 222

    // 同上
    TestAMemory *aa = (TestAMemory *)(new TestA());
    aa->vptr->foo1(); // TestA.foo1
    aa->vptr->foo2(); // TestA.foo2
}

那么具体是如何实现的呢? 可以通过 g++ -O0 -fdump-lang-class main.cpp dump 类信息

cpp 复制代码
Vtable for TestA
TestA::_ZTV5TestA: 4 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI5TestA)
16    (int (*)(...))TestA::foo1
24    (int (*)(...))TestA::foo2

Class TestA
   size=32 align=8 // TestA size = 32 = 8(vptr:TestA) + 24 (arr)
   base size=32 base align=8
TestA (0x0x7fdde31d2960) 0
    vptr=((& TestA::_ZTV5TestA) + 16) // 表示vtpr的指向,指向 (TestA::_ZTV5TestA addr +16), 即TestA::foo1函数地址.
  
Vtable for TestB
TestB::_ZTV5TestB: 5 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI5TestB)
16    (int (*)(...))TestB::foo1 // 重写基类虚函数
24    (int (*)(...))TestA::foo2 // 这里注意下. 会把未重写基类的copy过来
32    (int (*)(...))TestB::foo3 // 添加自己定义的虚函数

Class TestB
   size=56 align=8
   base size=56 base align=8 // size = 56 = 8(vptr:TestB) + 24(a.arr) + 24(b.arr) 
TestB (0x0x7fdde322d068) 0
    vptr=((& TestB::_ZTV5TestB) + 16) // 同上
  TestA (0x0x7fdde31d2de0) 0
      primary-for TestB (0x0x7fdde322d068) //  primary-for TestB 表示继承关系,多继承需要看这个字段


Vtable for TestC
TestC::_ZTV5TestC: 6 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI5TestC)
16    (int (*)(...))TestB::foo1
24    (int (*)(...))TestA::foo2
32    (int (*)(...))TestB::foo3
40    (int (*)(...))TestC::foo4 // 可以看到直接copy的基类的全部虚函数+自己定义的虚函数

Class TestC // c -> b -> a
   size=56 align=8 // size = 8(vptr:TestC) + 24(a.arr) + 24(b.arr)
   base size=56 base align=8
TestC (0x0x7f639bfee680) 0
    vptr=((& TestC::_ZTV5TestC) + 16)
  TestB (0x0x7f639bfee6e8) 0
      primary-for TestC (0x0x7f639bfee680)
    TestA (0x0x7f639bfd7780) 0
        primary-for TestB (0x0x7f639bfee6e8)

总结:

  1. 可以发现当定义了虚函数那么此时会生成一个虚函数表,虚函表记录了虚函数的函数地址,例如 TestA 内部会定义一个 vptr 指向 Vtable for TestA + 16 ,即 (int (*)(...))TestA::foo1 函数开始

  2. TestB 继承了 TestA,TestB内部也定义了一个 vptr 指向 Vtable for TestB , 定义了其申明的虚函数

  3. TestC 继承 TestB ,此时也只会有一份vptr指向 vtable testc

  4. 单继承仅会有一个 vptr ,指向其自己的 vtable

多继承

C++是支持多继承的,这个也是与Java等语言的区别(C++不支持接口Interface),其次多继承会比较复杂,比如涉及到交叉/菱形继承的问题,我们可以看下C++是符合实现多继承的!

继续上面那个例子,我们新增一个结构体, D 继承自A/B/C

cpp 复制代码
struct TestD : TestA, TestB, TestC {
    void foo1() {
        std::cout << "TestD.foo1\n";
    }
};

// 继承关系/多继承/菱形继承
// D -> A
//   -> B -> A
//   -> C -> B -> A

此时虚表为如下:

cpp 复制代码
Vtable for TestD
TestD::_ZTV5TestD: 15 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI5TestD)
16    (int (*)(...))TestD::foo1 // vptr(TestD/TestA)
24    (int (*)(...))TestA::foo2
32    (int (*)(...))-32
40    (int (*)(...))(& _ZTI5TestD)
48    (int (*)(...))TestD::_ZThn32_N5TestD4foo1Ev // vptr(TestB) TestD::foo1
56    (int (*)(...))TestA::foo2
64    (int (*)(...))TestB::foo3
72    (int (*)(...))-88
80    (int (*)(...))(& _ZTI5TestD)
88    (int (*)(...))TestD::_ZThn88_N5TestD4foo1Ev //vptr(TestC) TestD::foo1
96    (int (*)(...))TestA::foo2
104   (int (*)(...))TestB::foo3
112   (int (*)(...))TestC::foo4

Class TestD
   size=144 align=8 // size = 144 = vptr(TestD) + a.arr + vptr(TestB) + (a.arr,b.arr) + vptr(TestC) + (a.arr, b.arr) 
   base size=144 base align=8
TestD (0x0x7efe8ac38d98) 0 //  vptr(TestD)偏移量为0
    vptr=((& TestD::_ZTV5TestD) + 16)
  TestA (0x0x7efe8acbe840) 0  // d::a内存
      primary-for TestD (0x0x7efe8ac38d98)
  TestB (0x0x7efe8acd5750) 32 // vptr(TestB)偏移量32
      vptr=((& TestD::_ZTV5TestD) + 48)
    TestA (0x0x7efe8acbe8a0) 32 // b::a 内存
        primary-for TestB (0x0x7efe8acd5750)
  TestC (0x0x7efe8acd57b8) 88 // vptr(TestC)偏移量88
      vptr=((& TestD::_ZTV5TestD) + 88)
    TestB (0x0x7efe8acd5820) 88 // c::b 内存
        primary-for TestC (0x0x7efe8acd57b8)
      TestA (0x0x7efe8acbe900) 88 // c::a 内存
          primary-for TestB (0x0x7efe8acd5820)
  1. TestD 交叉继承造成结构的大小升级到了 144 ,导致 A冗余了2份,B 冗余了1份 ,是不是发现问题了,这么继承的话遇到重复继承基类导致内存会成倍的增加,怎么解决呢,下文会介绍到!
  2. 多继承会为每个基类分配一个 vptr 指针!
    1. vptr(TestD) 偏移量 0
    2. vptr(TestB) 偏移量 32
    3. vptr(TestC) 偏移量 88
  3. 多继承当涉及到类型转换的时候(向上/向下)类型转换的时候会涉及到指针的移动(下文会降到),具体的移动偏移量可以参考上面的class dump,向下转型需要使用 dynamic_cast ! 但是上面的例子多类型转换的时候会存在二义性,例如D向上换成A,会发现A内存中有3份到低是哪个,所以编译器不会让你转换,但是我们可以通过内存进行非安全转换!!
  4. 可以结合下面这个代码看下
cpp 复制代码
typedef void (*VoidFunc)();

VoidFunc GetVoidFunc(void *ptr, int offset) {
    long *pptr = (long *)ptr;
    long *table = (long *)(*pptr);
    long *func = table + offset;
    return (VoidFunc)(*func);
}

// struct TestD : TestA, TestB, TestC {}
// TestD = vptr(TestA) + a.arr + vptr(TestB) + (a.arr,b.arr) + vptr(TestC) + (a.arr, b.arr) = 144
int main() {
    // 多继承vptr会有偏移量
    TestD d;

    // vptr偏移量0 (TestD)
    GetVoidFunc(&d, 0)(); // TestD::foo1
    GetVoidFunc(&d, 1)(); // TestA::foo2

    // vptr偏移量32(基类为TestB)
    GetVoidFunc(((long *)&d) + 4, 0)(); // TestD.foo1
    GetVoidFunc(((long *)&d) + 4, 1)(); // TestA.foo2
    GetVoidFunc(((long *)&d) + 4, 2)(); // TestB.foo3

    // vptr偏移量88(基类为TestC)
    GetVoidFunc(((long *)&d) + 11, 0)(); // TestD.foo1
    GetVoidFunc(((long *)&d) + 11, 1)(); // TestA.foo2
    GetVoidFunc(((long *)&d) + 11, 2)(); // TestB.foo3
    GetVoidFunc(((long *)&d) + 11, 3)(); // TestC.foo4
}

虚继承

代码示例: godbolt.org/z/rxeza5EEa 或者 coliru.stacked-crooked.com/a/44776e393...

cpp 复制代码
#include <iostream>

struct TestA {
    virtual void foo1() {
        std::cout << "TestA.foo1\n";
    }
    virtual void foo2() {
        std::cout << "TestA.foo2\n";
    }
    void foo3() {
        std::cout << "TestA.foo3\n";
    }
    long arr[3];
};

struct TestB : virtual TestA {
    virtual void foo1() {
        std::cout << "TestB.foo1\n";
    }
    void foo2() {
        std::cout << "TestB.foo2\n";
    }
    virtual void foo3() {
        std::cout << "TestB.foo3\n";
    }
    long arr[3];
};

struct TestC : virtual TestB {
    virtual void foo4() {
        std::cout << "TestC.foo4\n";
    }
};

struct TestD : virtual TestB {
    virtual void foo4() {
        std::cout << "TestC.foo4\n";
    }
};

// TestE 继承关系
//       -> C
//   ->        -> B
// E                -> A
//    ->       -> B
//       -> D
struct TestE : virtual TestC, virtual TestD {
    void foo1() {
        std::cout << "TestD.foo1\n";
    }
};

typedef void (*VoidFunc)();

VoidFunc GetVoidFunc(void *ptr, int offset) {
    long *pptr = (long *)ptr;
    long *table = (long *)(*pptr);
    long *func = table + offset;
    return (VoidFunc)(*func);
}

VoidFunc GetVoidFunc(void *ptr, int size, int offset) {
    long *pptr = ((long *)ptr) + size; // 指针偏移
    long *table = (long *)(*pptr);
    long *func = table + offset; // vtable 偏移
    return (VoidFunc)(*func);
}
// https://stackoverflow.com/questions/6258559/what-is-the-vtt-for-a-class
// 72
int main() {
    // (TestC)vptr + (TestB)vptr + 24 + (TestA)vptr + 24
    TestC *c = new TestC();

    GetVoidFunc(c, 0, 0)(); // TestC.foo4

    GetVoidFunc(c, 1, 0)(); // TestB::foo1
    GetVoidFunc(c, 1, 1)(); // TestB::foo2
    GetVoidFunc(c, 1, 2)(); // TestB::foo3

    GetVoidFunc(c, 5, 0)(); // TestB.foo1
    GetVoidFunc(c, 5, 1)(); // TestB.foo2

    // 堆是从低地址到高地址
    // 强制类型转换会移动函数指针
    TestB *b = c;
    // 偏移量为8: 1个字节 = vptr(TestC)
    std::cout << ((long)b - (long)c) << std::endl;

    TestA *a = c;
    // 偏移量为40: 5个字节 = vptr(TestC) + vptr(TestB) + 24
    std::cout << ((long)a - (long)c) << std::endl;

    TestE *d = new TestE(); // vptr(TestC) + vptr(TestB) + 24 + vptr(TestA) + 24 + vptr(TestD)
    GetVoidFunc(d, 9, 0)(); // TestD::foo4

    std::cout << "size: " << sizeof(TestA) << "\n"; // 32 = vptr(TestA) + 24
    std::cout << "size: " << sizeof(TestB) << "\n"; // 64 = vptr(TestB) + 24 + vptr(TestA) + 24
    std::cout << "size: " << sizeof(TestC) << "\n"; // 72 = vptr(TestC) + vptr(TestB) + 24 + vptr(TestA) + 24
    std::cout << "size: " << sizeof(TestD) << "\n"; // 72 = vptr(TestD) + vptr(TestB) + 24 + vptr(TestA) + 24
    std::cout << "size: " << sizeof(TestE) << "\n"; // 80 = vptr(TestE) + vptr(TestB) + 24 + vptr(TestA) + 24 + vptr(TestD)
}
  1. 虚继承后内存仅需 72,只需要维护基类的vptr 和 基类分配的内存即可,所以虚继承可以极大的降低内存开销
  2. 虚继承后内存中有且仅有一份基类的内存(包含多层引用),具体的内存逻辑图可以通过 dump class查看
  3. 多继承当进行强制类型转换时会通过移动指针实现,具体可以看下面例子,但是其实还有一些case,比如TestETestE会和TestC的地址一样,原因很简单就是两者在virtual table中函数申明都一样,所以没必要再分配一份内存了(这个属于GCC的优化吧)!
cpp 复制代码
int main(){
  TestE *ee = new TestE(); // vptr(TestE/TestC) + vptr(TestB) + 24 + vptr(TestA) + 24 + vptr(TestD)
  TestC *cc = ee;
  TestD *dd = ee;
  TestB *bb = ee;
  TestA *aa = ee;
  std::cout << std::hex << ee << "\n"; // 0x600002ef80f0 (offset=0)
  std::cout << std::hex << cc << "\n"; // 0x600002ef80f0 (offset=0)
  std::cout << std::hex << bb << "\n"; // 0x600002ef80f8 (offset=8)
  std::cout << std::hex << aa << "\n"; // 0x600002ef8118 (offset=40)
  std::cout << std::hex << dd << "\n"; // 0x600002ef8138 (offset=72)
}
  1. 虚继承表如下图所示, 这里以 TestC 为例子
shell 复制代码
# TestB    
Vtable for TestB
TestB::_ZTV5TestB: 12 entries
0     32
8     (int (*)(...))0
16    (int (*)(...))(& _ZTI5TestB)
24    (int (*)(...))TestB::foo1
32    (int (*)(...))TestB::foo2
40    (int (*)(...))TestB::foo3
48    18446744073709551584
56    18446744073709551584
64    (int (*)(...))-32
72    (int (*)(...))(& _ZTI5TestB)
80    (int (*)(...))TestB::_ZTv0_n24_N5TestB4foo1Ev
88    (int (*)(...))TestB::_ZTv0_n32_N5TestB4foo2Ev

VTT for TestB
TestB::_ZTT5TestB: 2 entries
0     ((& TestB::_ZTV5TestB) + 24)
8     ((& TestB::_ZTV5TestB) + 80)

Class TestB
   size=64 align=8
   base size=32 base align=8
TestB (0x0x7f779a7cc618) 0
    vptridx=0 vptr=((& TestB::_ZTV5TestB) + 24)
  TestA (0x0x7f779a7b5660) 32 virtual
      vptridx=8 vbaseoffset=-24 vptr=((& TestB::_ZTV5TestB) + 80)

##  TestC

Vtable for TestC
TestC::_ZTV5TestC: 20 entries
0     40
8     8
16    (int (*)(...))0
24    (int (*)(...))(& _ZTI5TestC)
32    (int (*)(...))TestC::foo4
40    0
48    0
56    0
64    32
72    (int (*)(...))-8
80    (int (*)(...))(& _ZTI5TestC)
88    (int (*)(...))TestB::foo1
96    (int (*)(...))TestB::foo2
104   (int (*)(...))TestB::foo3
112   18446744073709551584
120   18446744073709551584
128   (int (*)(...))-40
136   (int (*)(...))(& _ZTI5TestC)
144   (int (*)(...))TestB::_ZTv0_n24_N5TestB4foo1Ev
152   (int (*)(...))TestB::_ZTv0_n32_N5TestB4foo2Ev

Construction vtable for TestB in TestC
TestC::_ZTC5TestC8_5TestB: 12 entries
0     32
8     (int (*)(...))0
16    (int (*)(...))(& _ZTI5TestB)
24    (int (*)(...))TestB::foo1
32    (int (*)(...))TestB::foo2
40    (int (*)(...))TestB::foo3
48    18446744073709551584
56    18446744073709551584
64    (int (*)(...))-32
72    (int (*)(...))(& _ZTI5TestB)
80    (int (*)(...))TestB::_ZTv0_n24_N5TestB4foo1Ev
88    (int (*)(...))TestB::_ZTv0_n32_N5TestB4foo2Ev

VTT for TestC
TestC::_ZTT5TestC: 5 entries
0     ((& TestC::_ZTV5TestC) + 32)
8     ((& TestC::_ZTV5TestC) + 88)
16    ((& TestC::_ZTV5TestC) + 144)
24    ((& TestC::_ZTC5TestC8_5TestB) + 24)
32    ((& TestC::_ZTC5TestC8_5TestB) + 80)

Class TestC
   size=72 align=8 // vptr(TestC) + vptr(TestB) + b.arr + vptr(TestA) + a.arr
   base size=8 base align=8
TestC (0x0x7f779a7cc750) 0 nearly-empty  ## offset=0 (TestC vptr) -> 32
    vptridx=0 vptr=((& TestC::_ZTV5TestC) + 32)
  TestB (0x0x7f779a7cc7b8) 8 virtual ## offset=8 (TestB vptr) -> 88
      subvttidx=24 vptridx=8 vbaseoffset=-24 vptr=((& TestC::_ZTV5TestC) + 88)
    TestA (0x0x7f779a7b57e0) 40 virtual ## offset=40 (TestA vptr)  -> 144
        vptridx=16 vbaseoffset=-32 vptr=((& TestC::_ZTV5TestC) + 144)

# TestE
Class TestE
   size=80 align=8 # 80 = vptr(TestE/TestC) + vptr(TestB) + 24 + vptr(TestA) + 24 + vptr(TestD)
   base size=8 base align=8
TestE (0x0x7f9b2d91d1c0) 0 nearly-empty
    vptridx=0 vptr=((& TestE::_ZTV5TestE) + 56)
  TestC (0x0x7f9b2d91c9c0) 0 nearly-empty virtual
      primary-for TestE (0x0x7f9b2d91d1c0)
      subvttidx=40 vptridx=8 vbaseoffset=-48
    TestB (0x0x7f9b2d91ca28) 8 virtual
        subvttidx=64 vptridx=16 vbaseoffset=-24 vptr=((& TestE::_ZTV5TestE) + 120)
      TestA (0x0x7f9b2d905960) 40 virtual
          vptridx=24 vbaseoffset=-32 vptr=((& TestE::_ZTV5TestE) + 176)
  TestD (0x0x7f9b2d91ca90) 72 nearly-empty virtual
      subvttidx=80 vptridx=32 vbaseoffset=-56 vptr=((& TestE::_ZTV5TestE) + 232)
    TestB (0x0x7f9b2d91ca28) alternative-path

析构函数

上面聊到了继承,但是没有聊到内存回收,我们知道不论是虚继承、普通继承他的内存分配机制大家上面应该是有所了解了,但是对于内存回收没谈到,C++作为一个非GC语言需要手动回收。析构函数使用虚函数完美的解决了内存回收,那么具体怎么使用呢?

注意:抽象类一定要为把析构函数定义为虚函数,否则系统不会回收!

cpp 复制代码
#include <iostream>

struct TestA {
    virtual ~TestA() {
        std::cout << "~TestA()\n";
    }
};

struct TestB : virtual TestA {
    ~TestB() override {
        std::cout << "~TestB()\n";
    }
};

struct TestC : virtual TestA {
    ~TestC() override {
        std::cout << "~TestC()\n";
    }
};

struct TestD : virtual TestB, virtual TestC {
    ~TestD() override {
        std::cout << "~TestD()\n";
    }
};

int main() {
    TestA *a = new TestD();
    delete a;
}
// ~TestD()
// ~TestC()
// ~TestB()
// ~TestA()

还有一个case大家有兴趣可以看下,就是多继承类型转换会涉及到指针移动,因此如果没有虚继承很可能会出现 pointer being freed was not allocated: zhuanlan.zhihu.com/p/26392392 。 我相信通过本文的学习对于这个问题应该大家也能知道为啥会报错!

或者还有一种就是把构造(包含析构)函数设置为protected防止向上转型,这么由派生类去析构就不会出现问题,代码例子: godbolt.org/z/d15KdT3PM

总结

  1. 只要基类定义了虚函数,就会添加到基类的虚函数表中,子类重写后是否标记为虚函数(virtual修饰)都会添加到子类的虚函数表中,子类的子类也是
  2. 优先使用虚继承,可以降低内存开销
  3. 子类继承的时候最好用 override 修饰一下函数重写,方便代码阅读
  4. 如果你这个类不涉及到继承(不是抽象类),那么你没必要设置一个虚函数,因为会额外分配内存
  5. 抽象类一定要把析构函数设置为虚函数,否则会存在内存泄漏或者内存回收出现空引用问题
  6. 如果遇到特别不理解的,看一下 dump class 看看
  7. 虚表的设计或多或少有些冗余,因为在派生类中记录下来全部的虚函数的函数地址(是否重写都会记录),这个过程在编译期就决定了,可以通过查看汇编代码发现数据段中定义有virtual table.
  8. 多阅读源码、优秀的开源项目可以掌握不少技巧

其他小点

  1. 我们可以使用 traits 的 std::is_convertible 来判断是否可以进行类型转换, 实践可以参考 std::enable_shared_from_this
cpp 复制代码
// -std=c++17  is_convertible_v 是c17提供的,c11可能写法麻烦点
#include <iostream>
#include <type_traits>

struct TestB {};
struct TestC : private TestB {};
struct TestD : TestB {};

template <typename T>
typename std::enable_if<std::is_convertible_v<T, TestB *>>::type DoPrint(T t) {
    std::cout << "impl TestB*" << std::endl;
}

template <typename T>
typename std::enable_if<std::is_convertible_v<T, TestB>>::type DoPrint(T t) {
    std::cout << "impl TestB" << std::endl;
}

void DoPrint(...) {
    std::cout << "not impl TestB" << std::endl;
}

int main() {
    DoPrint(new TestD()); // impl TestB*
    DoPrint(TestD{}); // impl TestB
    DoPrint(TestC{}); // not impl TestB
}
  1. 继承是可以限定作用域的,继承是无法使用基类private的成员的,除非基类给你开启friend,struct的话默认是public继承,class默认是private继承. protect继承会使用继承的public属性的成员变成protect,private会使用继承的成员变成(public&protect -> private)。 基类申明为final表示禁止继承,override可以显示申明重写!

  2. 继承构造函数,当我们想直接服用父类的构造函数需要手动再申明一次!!

cpp 复制代码
// -std=c++11
#include <iostream>

struct TestB {
    TestB(int f1, int f2) : f1_(f1), f2_(f2) {
    }
    virtual int sum() {
        return f1_ + f2_;
    }

private:
    int f1_;
    int f2_;
};

struct TestC : TestB {
    using TestB::TestB; // 继承构造函数
};
struct TestD : TestB {
    // 需要手动申明,使用构造函数委托
    TestD(int f1, int f2) : TestB(f1, f2) {
    }
};

int main() {
    TestC c = TestC(1, 2);
    std::cout << c.sum() << std::endl;

    TestD d = TestD(1, 2);
    std::cout << c.sum() << std::endl;
}

参考文章

相关推荐
hai405872 分钟前
Spring Boot中的响应与分层解耦架构
spring boot·后端·架构
编程版小新7 分钟前
C++初阶:STL详解(四)——vector迭代器失效问题
开发语言·c++·迭代器·vector·迭代器失效
AlexMercer10121 小时前
【C++】二、数据类型 (同C)
c语言·开发语言·数据结构·c++·笔记·算法
Adolf_19931 小时前
Flask-JWT-Extended登录验证, 不用自定义
后端·python·flask
叫我:松哥1 小时前
基于Python flask的医院管理学院,医生能够增加/删除/修改/删除病人的数据信息,有可视化分析
javascript·后端·python·mysql·信息可视化·flask·bootstrap
海里真的有鱼2 小时前
Spring Boot 项目中整合 RabbitMQ,使用死信队列(Dead Letter Exchange, DLX)实现延迟队列功能
开发语言·后端·rabbitmq
工业甲酰苯胺2 小时前
Spring Boot 整合 MyBatis 的详细步骤(两种方式)
spring boot·后端·mybatis
小灰灰爱代码2 小时前
C++——求3个数中最大的数(分别考虑整数、双精度数、长整数的情况),用函数模板来实现。
开发语言·c++·算法
新知图书2 小时前
Rust编程的作用域与所有权
开发语言·后端·rust
BeyondESH3 小时前
Linux线程同步—竞态条件和互斥锁(C语言)
linux·服务器·c++