1. typeinfo定义
typeinfo中存储的是关于类型的信息,可以通过typeid操作符获取,对于没有虚函数的场景,typeid返回的是编译器静态类型信息,对于一个基类类型指针,哪怕其真实指向是个派生类,如果没有虚函数,那么typeid返回的也还是基类类型,而对于有虚函数的类来说,由于虚表的存在,可以在运行期动态判断类型。
以下代码都是在X86_64 Linux中完成,g++编译。
2. 没有虚函数的类继承
示例代码:
#include <iostream>
#include <typeinfo>
class Base {
public:
int baseValue;
Base(int v) : baseValue(v) {}
};
class Derived : public Base {
public:
int derivedValue;
Derived(int b, int d) : Base(b), derivedValue(d) {}
};
int main() {
Base b(10); // 基类对象
Derived d(20, 30); // 派生类对象
Base* ptr = &d; // 基类指针指向派生类对象
// 打印类型信息
std::cout << "Type of b: " << typeid(b).name() << std::endl; // 输出: Base
std::cout << "Type of d: " << typeid(d).name() << std::endl; // 输出: Derived
std::cout << "Type of ptr: " << typeid(*ptr).name() << std::endl; // 输出: Base
return 0;
}
运行结果如下:
$ ./vtable_1b
Type of b: 4Base
Type of d: 7Derived
Type of ptr: 4Base
可见,在没有虚函数的情况下,即使指针 ptr实际指向的是派生类对象,但是没有虚表的帮助,只能返回编译器静态类型,也就是基类类型。
3. 重写虚函数的类继承
将上面的代码稍作修改,引入一个虚函数,代码如下:
#include <iostream>
#include <typeinfo>
class Base {
public:
int baseValue;
Base(int v) : baseValue(v) {}
virtual void fool() {std::cout << "hello, I'm Base" << std::endl;}
};
class Derived : public Base {
public:
int derivedValue;
Derived(int b, int d) : Base(b), derivedValue(d) {}
virtual void fool() {std::cout << "hello, I'm Derived" << std::endl;}
};
int main() {
Base b(10); // 基类对象
Derived d(20, 30); // 派生类对象
Base* ptr = &d; // 基类指针指向派生类对象
// 打印类型信息
std::cout << "Type of b: " << typeid(b).name() << std::endl; // 输出: Base
std::cout << "Type of d: " << typeid(d).name() << std::endl; // 输出: Derived
std::cout << "Type of ptr: " << typeid(*ptr).name() << std::endl; // 输出: Base
b.fool();
d.fool();
ptr->fool();
return 0;
}
运行结果:
$ ./vtable_1c
Type of b: 4Base
Type of d: 7Derived
Type of ptr: 7Derived
hello, I'm Base
hello, I'm Derived
hello, I'm Derived
内存布局
对于上面的虚函数继承例子,gdb打印对象内存:
(gdb) set print asm-demangle on
(gdb) set print demangle on
(gdb) p b
$6 = {_vptr.Base = 0x400cd0 <vtable for Base+16>, baseValue = 10}
(gdb) p d
$7 = {<Base> = {_vptr.Base = 0x400cb8 <vtable for Derived+16>, baseValue = 20}, derivedValue = 30}
(gdb) p sizeof(b)
$8 = 16
(gdb) p sizeof(d)
$9 = 16
对于基类来说,对象里包含一个虚表指针和一个int成员, 考虑到指针的8字节对齐,因此整个对象占用16字节。
对于派生类来说,对象里包含一个虚表指针和两个int成员,同样考虑到指针的8字节对齐,两个int成员共享第二个8字节,因此对象仍然占用16字节。
继续查看虚表:
3.1 基类虚表
从上面的打印可以看出,基类对象的第一个内存成员是vptr, 紧跟着的是成员变量
对象中的vptr指向的是vtable偏移16,因此,vtable的起始地址是0x400cc0
(gdb) x/10xg 0x400cc0
0x400cc0 <vtable for Base>: 0x0000000000000000 0x0000000000400d00
0x400cd0 <vtable for Base+16>: 0x0000000000400b1e 0x0000000000601d98
0x400ce0 <typeinfo for Derived+8>: 0x0000000000400cf0 0x0000000000400d00
0x400cf0 <typeinfo name for Derived>: 0x6465766972654437 0x0000000000000000
0x400d00 <typeinfo for Base>: 0x0000000000601d40 0x0000000000400d10
(gdb) info symbol 0x0000000000400d00
typeinfo for Base in section .rodata of /home/lss/test/vtable_1c
(gdb) info symbol 0x0000000000400b1e
Base::fool() in section .text of /home/lss/test/vtable_1c
vtable + 8 指向的是基类的typeinfo
vtable + 16, 也就是对象中的vptr, 指向基类的虚函数foo()
查看基类typeinfo:
(gdb) x/20xg 0x0000000000400d00
0x400d00 <typeinfo for Base>: 0x0000000000601d40 0x0000000000400d10
0x400d10 <typeinfo name for Base>: 0x0000006573614234 0x000000743b031b01
0x400d20: 0xfffffad80000000d 0xfffffb58000000b8
0x400d30: 0xfffffb8800000090 0xfffffc3e000000a4
0x400d40: 0xfffffd5e00000180 0xfffffd9c000001a4
0x400d50: 0xfffffdb2000001c4 0xfffffde2000000e0
0x400d60: 0xfffffe0600000100 0xfffffe3200000120
0x400d70: 0xfffffe6e00000140 0xfffffea800000160
0x400d80: 0xffffff18000001e8 0x0000000000000230
0x400d90: 0x0000000000000014 0x0110780100527a01
(gdb) info symbol 0x0000000000601d40
vtable for __cxxabiv1::__class_type_info@@CXXABI_1.3 + 16 in section .data.rel.ro of /home/lss/test/vtable_1c
(gdb) info symbol 0x0000000000400d10
typeinfo name for Base in section .rodata of /home/lss/test/vtable_1c
(gdb) x/s 0x0000000000400d10
0x400d10 <typeinfo name for Base>: "4Base"
图解如下:
3.2 派生类虚表
从之前的打印可以看出,派生类对象的第一个内存成员是vptr, 紧跟着的是基类成员变量,最后是派生类成员变量。
对象中的vptr指向的是vtable偏移16,因此,vtable的起始地址是0x400ca8
(gdb) x/16xg 0x400ca8
0x400ca8 <vtable for Derived>: 0x0000000000000000 0x0000000000400cd8
0x400cb8 <vtable for Derived+16>: 0x0000000000400b86 0x0000000000000000
0x400cc8 <vtable for Base+8>: 0x0000000000400d00 0x0000000000400b1e
0x400cd8 <typeinfo for Derived>: 0x0000000000601d98 0x0000000000400cf0
0x400ce8 <typeinfo for Derived+16>: 0x0000000000400d00 0x6465766972654437
0x400cf8 <typeinfo name for Derived+8>: 0x0000000000000000 0x0000000000601d40
0x400d08 <typeinfo for Base+8>: 0x0000000000400d10 0x0000006573614234
0x400d18: 0x000000743b031b01 0xfffffad80000000d
vtable + 8 指向的是派生类的typeinfo
vtable + 16, 也就是对象中的vptr, 指向派生类的虚函数foo()
查看派生类typeinfo:
Derived::fool() in section .text of /home/lss/test/vtable_1c
(gdb) x/20xg 0x0000000000400cd8
0x400cd8 <typeinfo for Derived>: 0x0000000000601d98 0x0000000000400cf0
0x400ce8 <typeinfo for Derived+16>: 0x0000000000400d00 0x6465766972654437
0x400cf8 <typeinfo name for Derived+8>: 0x0000000000000000 0x0000000000601d40
0x400d08 <typeinfo for Base+8>: 0x0000000000400d10 0x0000006573614234
0x400d18: 0x000000743b031b01 0xfffffad80000000d
0x400d28: 0xfffffb58000000b8 0xfffffb8800000090
0x400d38: 0xfffffc3e000000a4 0xfffffd5e00000180
0x400d48: 0xfffffd9c000001a4 0xfffffdb2000001c4
0x400d58: 0xfffffde2000000e0 0xfffffe0600000100
0x400d68: 0xfffffe3200000120 0xfffffe6e00000140
(gdb) info symbol 0x0000000000601d98
vtable for __cxxabiv1::__si_class_type_info@@CXXABI_1.3 + 16 in section .data.rel.ro of /home/lss/test/vtable_1c
(gdb) info symbol 0x0000000000400cf0
typeinfo name for Derived in section .rodata of /home/lss/test/vtable_1c
(gdb) info symbol 0x0000000000400d00
typeinfo for Base in section .rodata of /home/lss/test/vtable_1c
(gdb) x/s 0x0000000000400cf0
0x400cf0 <typeinfo name for Derived>: "7Derived"
图解如下:
由此可见,派生类的typeinfo中包含了指向基类typeinfo的指针。
4. 没有重写虚函数的类继承
如果派生类没有重写基类的虚函数,内存布局会是什么样呢?
修改上面的类:
#include <iostream>
#include <typeinfo>
class Base {
public:
int baseValue;
Base(int v) : baseValue(v) {}
virtual void fool() {std::cout << "hello, I'm Base" << std::endl;}
};
class Derived : public Base {
public:
int derivedValue;
Derived(int b, int d) : Base(b), derivedValue(d) {}
};
int main() {
Base b(10); // 基类对象
Derived d(20, 30); // 派生类对象
Base* ptr = &d; // 基类指针指向派生类对象
// 打印类型信息
std::cout << "Type of b: " << typeid(b).name() << std::endl; // 输出: Base
std::cout << "Type of d: " << typeid(d).name() << std::endl; // 输出: Derived
std::cout << "Type of ptr: " << typeid(*ptr).name() << std::endl; // 输出: Base
b.fool();
d.fool();
ptr->fool();
return 0;
}
编译后运行:
$ ./vtable_1d
Type of b: 4Base
Type of d: 7Derived
Type of ptr: 7Derived
hello, I'm Base
hello, I'm Base
hello, I'm Base
内存布局
(gdb) set print asm-demangle on
(gdb) set print demangle on
(gdb) p b
$1 = {_vptr.Base = 0x400c88 <vtable for Base+16>, baseValue = 10}
(gdb) p d
$2 = {<Base> = {_vptr.Base = 0x400c70 <vtable for Derived+16>, baseValue = 20}, derivedValue = 30}
(gdb) p sizeof(b)
$3 = 16
(gdb) p sizeof(d)
$4 = 16
(gdb)
下面分析虚表,至于typeinfo的分析和上面类似
4.1 基类虚表
虚表起始地址:0x400c78
(gdb) x/10xg 0x400c78
0x400c78 <vtable for Base>: 0x0000000000000000 0x0000000000400cb8
0x400c88 <vtable for Base+16>: 0x0000000000400b1e 0x0000000000601d98
0x400c98 <typeinfo for Derived+8>: 0x0000000000400ca8 0x0000000000400cb8
0x400ca8 <typeinfo name for Derived>: 0x6465766972654437 0x0000000000000000
0x400cb8 <typeinfo for Base>: 0x0000000000601d40 0x0000000000400cc8
(gdb) info symbol 0x0000000000400b1e
Base::fool() in section .text of /home/lss/test/vtable_1d
(gdb)
foo() 虚函数地址为:0x0000000000400b1e
4.2 派生类虚表
虚表起始地址:0x400c60
(gdb) x/16xg 0x400c60
0x400c60 <vtable for Derived>: 0x0000000000000000 0x0000000000400c90
0x400c70 <vtable for Derived+16>: 0x0000000000400b1e 0x0000000000000000
0x400c80 <vtable for Base+8>: 0x0000000000400cb8 0x0000000000400b1e
0x400c90 <typeinfo for Derived>: 0x0000000000601d98 0x0000000000400ca8
0x400ca0 <typeinfo for Derived+16>: 0x0000000000400cb8 0x6465766972654437
0x400cb0 <typeinfo name for Derived+8>: 0x0000000000000000 0x0000000000601d40
0x400cc0 <typeinfo for Base+8>: 0x0000000000400cc8 0x0000006573614234
0x400cd0: 0x0000006c3b031b01 0xfffffb200000000c
(gdb) info symbol 0x0000000000400b1e
Base::fool() in section .text of /home/lss/test/vtable_1d
(gdb)
foo()虚函数地址和基类一样:0x0000000000400b1e
图解如下,typeinfo忽略不展示:
由此可见,如果派生类没有重写基类的虚函数,派生类仍然有自己的虚表,但是里面的虚函数指向基类的虚函数。