C++ 作为面向对象语言,其次面向对象语言的重要特性封装、继承、多态,所以理解继承底层设计对于我们学习C++是非常重要的,其次他是C++的灵魂所在,本人也是走了些弯路所以打算深度学习一下!
环境
- GCC 8.3.0(本人的运行环境)
- gcc开发选项: gcc.gnu.org/onlinedocs/...
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
- 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)
总结:
-
可以发现当定义了虚函数那么此时会生成一个虚函数表,虚函表记录了虚函数的函数地址,例如 TestA 内部会定义一个
vptr
指向Vtable for TestA
+16
,即(int (*)(...))TestA::foo1
函数开始 -
TestB 继承了 TestA,TestB内部也定义了一个
vptr
指向Vtable for TestB
, 定义了其申明的虚函数 -
TestC 继承 TestB ,此时也只会有一份vptr指向
vtable testc
-
单继承仅会有一个 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)
- TestD 交叉继承造成结构的大小升级到了 144 ,导致 A冗余了2份,B 冗余了1份 ,是不是发现问题了,这么继承的话遇到重复继承基类导致内存会成倍的增加,怎么解决呢,下文会介绍到!
- 多继承会为每个基类分配一个
vptr
指针!- vptr(TestD) 偏移量 0
- vptr(TestB) 偏移量 32
- vptr(TestC) 偏移量 88
- 多继承当涉及到类型转换的时候(向上/向下)类型转换的时候会涉及到指针的移动(下文会降到),具体的移动偏移量可以参考上面的class dump,向下转型需要使用
dynamic_cast
! 但是上面的例子多类型转换的时候会存在二义性,例如D向上换成A,会发现A内存中有3份到低是哪个,所以编译器不会让你转换,但是我们可以通过内存进行非安全转换!! - 可以结合下面这个代码看下
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)
}
- 虚继承后内存仅需 72,只需要维护基类的vptr 和 基类分配的内存即可,所以虚继承可以极大的降低内存开销!
- 虚继承后内存中有且仅有一份基类的内存(包含多层引用),具体的内存逻辑图可以通过
dump class
查看 - 多继承当进行强制类型转换时会通过移动指针实现,具体可以看下面例子,但是其实还有一些case,比如
TestE
中TestE
会和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)
}
- 虚继承表如下图所示, 这里以 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 。
总结
- 只要基类定义了虚函数,就会添加到基类的虚函数表中,子类重写后是否标记为虚函数(virtual修饰)都会添加到子类的虚函数表中,子类的子类也是
- 优先使用虚继承,可以降低内存开销
- 子类继承的时候最好用
override
修饰一下函数重写,方便代码阅读 - 如果你这个类不涉及到继承(不是抽象类),那么你没必要设置一个虚函数,因为会额外分配内存
- 抽象类一定要把析构函数设置为虚函数,否则会存在内存泄漏或者内存回收出现空引用问题
- 如果遇到特别不理解的,看一下 dump class 看看
- 虚表的设计或多或少有些冗余,因为在派生类中记录下来全部的虚函数的函数地址(是否重写都会记录),这个过程在编译期就决定了,可以通过查看汇编代码发现数据段中定义有virtual table.
- 多阅读源码、优秀的开源项目可以掌握不少技巧
其他小点
- 我们可以使用 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
}
-
继承是可以限定作用域的,继承是无法使用基类private的成员的,除非基类给你开启
friend
,struct的话默认是public继承,class默认是private继承. protect继承会使用继承的public属性的成员变成protect,private会使用继承的成员变成(public&protect -> private)。 基类申明为final表示禁止继承,override可以显示申明重写! -
继承构造函数,当我们想直接服用父类的构造函数需要手动再申明一次!!
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;
}
参考文章
-
C++对象模型: www.miaoerduo.com/2023/01/19/...
-
CPP-Virtual-Table:leimao.github.io/blog/CPP-Vi...
-
C++中的deleting destructor:zhuanlan.zhihu.com/p/26392392