c++多态(包含例子)

c++多态

c++中的多态分为静态多态和动态多态。静态指的是在编译期间通过函数匹配就能确定调用的函数是哪一个版本。而动态多态指的是在程序运行时根据对象的动态类型而确定调用的函数是哪一个版本。

静态多态

函数重载

函数重载(Function Overloading)是指在同一个作用域内,可以定义多个同名函数,但它们的参数列表(参数类型或个数)必须不同,以便根据不同的参数类型或个数来调用不同的函数。函数重载让程序员可以使用相同的函数名来实现不同的功能,提高了代码的可读性和灵活性。

重载条件:

  • 函数名必须相同
  • 参数列表必须不同(需满足以下一种及以上的不同):
    • 参数个数不同
    • 参数类型不同
    • 参数顺序不同

注意函数的返回值类型不同不足以构成函数重载。因为 函数签名 = 函数名 + 参数列表。

泛型编程

可以使用模板来实现静态多态,即在编译时确定不同类型的行为。通常通过模板参数多态(template parameter polymorphism)来实现。

动态多态

实现

  • 有继承关系,最少两层。一个基类,一个派生类。
  • 基类中至少有一个虚函数,派生类至少重写(override)基类的一个虚函数。
  • 通过基类的指针或者引用去指向(绑定)基类对象或者派生类对象。
  • 通过基类的指针或者引用去调用虚函数(基类和派生类都实现的虚函数)就会发生动态绑定,从而实现动态多态。

内存布局

无继承
class Base {
public:
    virtual void func1() {}
    virtual void func2() {}
};

Base b1;

Base类对象会多出一个虚表指针(vptr)指向Base类的虚函数表。虚函数表其实就是一个函数指针数组。数组元素顺序与虚函数声明顺序一致。

(gdb) info vtbl b1
vtable for 'Base' @ 0x555555755c10 (subobject @ 0x7fffffffe570):
[0]: 0x555555555034 <Base::func1()>
[1]: 0x555555555040 <Base::func2()>
单继承
class Base {
public:
    virtual void func1() {}
    virtual void func2() {}
};

class Derived : public Base {
public:
    virtual void func1() {}
    virtual void func3() {}
};

Base b1;
Derived d1;

Derived重写了func1、新增了func3。

单继承中,派生类的虚表会先复制基类的虚表。

  1. 然后编译器检查派生类是否有对基类的虚函数重写,若有,则将虚函数表中对应表项替换成派生类的虚函数,例如:func1。
  2. 派生类没有重写的,直接复用基类的,例如:func2。
  3. 派生类新增的,直接追加到原有的虚表中,例如:func3.
(gdb) info vtbl b1
vtable for 'Base' @ 0x555555755c10 (subobject @ 0x7fffffffe570):
[0]: 0x555555555034 <Base::func1()>
[1]: 0x555555555040 <Base::func2()>
(gdb) info vtbl d1
vtable for 'Derived' @ 0x555555755be8 (subobject @ 0x7fffffffe580):
[0]: 0x55555555504c <Derived::func1()>
[1]: 0x555555555040 <Base::func2()>
[2]: 0x555555555058 <Derived::func3()>
多继承
class Base1 {
public:
    virtual void func1() {}
    virtual void func2() {}
};

class Base2 {
public:
    virtual void func1() {}
    virtual void func3() {}
};

class Derived1 : public Base1, public Base2 {
public:
    virtual void func1() {}
    virtual void func4() {}
};

class Derived2 : public Base2, public Base1 {
public:
    virtual void func1() {}
    virtual void func4() {}
};

在基类多于一个的时候,即多继承情况下。派生类会选取第一个基类的虚表作为自己的第一个虚表。然后按照单继承的情况(无修改、有修改、新增)来对第一个虚表进行修改。注意新增的虚函数一定是放在派生类的第一个虚表后面。而其他基类的虚表,派生类也会继承下来,然后按照单继承的情况来对虚表继续修改。 注意:对第二个及后面的基类的虚表进行的只有替换的修改,并没有新增的情况

派生类有多少个直接基类,就会有多少个虚表(虚表指针)。

(gdb) info vtbl b1
vtable for 'Base1' @ 0x555555755ba8 (subobject @ 0x7fffffffe560):
[0]: 0x55555555523e <Base1::func1()>
[1]: 0x55555555524a <Base1::func2()>

(gdb) info vtbl b2
vtable for 'Base2' @ 0x555555755b88 (subobject @ 0x7fffffffe568):
[0]: 0x555555555256 <Base2::func1()>
[1]: 0x555555555262 <Base2::func3()>

(gdb) info vtbl d1
vtable for 'Derived1' @ 0x555555755b40 (subobject @ 0x7fffffffe570):
[0]: 0x55555555526e <Derived1::func1()>
[1]: 0x55555555524a <Base1::func2()>
[2]: 0x555555555280 <Derived1::func4()>

vtable for 'Base2' @ 0x555555755b68 (subobject @ 0x7fffffffe578):
[0]: 0x555555555279 <non-virtual thunk to Derived1::func1()>
[1]: 0x555555555262 <Base2::func3()>

(gdb) info vtbl d2
vtable for 'Derived2' @ 0x555555755af8 (subobject @ 0x7fffffffe580):
[0]: 0x55555555528c <Derived2::func1()>
[1]: 0x555555555262 <Base2::func3()>
[2]: 0x55555555529e <Derived2::func4()>

vtable for 'Base1' @ 0x555555755b20 (subobject @ 0x7fffffffe588):
[0]: 0x555555555297 <non-virtual thunk to Derived2::func1()>
[1]: 0x55555555524a <Base1::func2()>
菱形继承
class Base {
public:
    virtual void func1() {}
    virtual void func2() {}
};

class Derived1 : public Base {
public:
    virtual void func1() {}
    virtual void func3() {}
};

class Derived2 : public Base {
public:
    virtual void func1() {}
    virtual void func4() {}
};

class DDerived : public Derived1, public Derived2 {

};

Base b1;
Derived1 d1;
Derived2 d2;
DDerived dd;  

其实与多继承是类似的。

(gdb) info vtbl b1
vtable for 'Base' @ 0x555555755bc0 (subobject @ 0x7fffffffe568):
[0]: 0x555555555224 <Base::func1()>
[1]: 0x555555555230 <Base::func2()>
(gdb) info vtbl d1
vtable for 'Derived1' @ 0x555555755b98 (subobject @ 0x7fffffffe570):
[0]: 0x55555555523c <Derived1::func1()>
[1]: 0x555555555230 <Base::func2()>
[2]: 0x555555555248 <Derived1::func3()>
(gdb) info vtbl d2
vtable for 'Derived2' @ 0x555555755b70 (subobject @ 0x7fffffffe578):
[0]: 0x555555555254 <Derived2::func1()>
[1]: 0x555555555230 <Base::func2()>
[2]: 0x555555555260 <Derived2::func4()>
(gdb) info vtbl dd
vtable for 'DDerived' @ 0x555555755b20 (subobject @ 0x7fffffffe580):
[0]: 0x55555555523c <Derived1::func1()>
[1]: 0x555555555230 <Base::func2()>
[2]: 0x555555555248 <Derived1::func3()>

vtable for 'Derived2' @ 0x555555755b48 (subobject @ 0x7fffffffe588):
[0]: 0x555555555254 <Derived2::func1()>
[1]: 0x555555555230 <Base::func2()>
[2]: 0x555555555260 <Derived2::func4()>
具体内存布局
#include <iostream>
#include <cstdio>

using namespace std;

class Base {
public:
    virtual void func1() {}
    virtual void func2() {}

private:
    int a = 0x11111111;
};

class Derived1 : public Base {
public:
    virtual void func1() {}
    virtual void func3() {}
    
private:
    int b = 0x22222222;
};

class Derived2 : public Base {
public:
    virtual void func1() {}
    virtual void func4() {}
    
private:
    int c = 0x33333333;
};

class DDerived : public Derived1, public Derived2 {
private:
    int d = 0x44444444;
};

int main() {
    Base b1;
    Derived1 d1;
    Derived2 d2;
    DDerived dd;
    cout << "sizeof(Base) : " << sizeof(Base) << endl;  // 16
    cout << "sizeof(Derived1) : " << sizeof(Derived1) << endl;  // 16
    cout << "sizeof(Derived2) : " << sizeof(Derived2) << endl;  // 16
    cout << "sizeof(DDerived) : " << sizeof(DDerived) << endl;  // 40
}

四个对象的起始地址:

(gdb) p &b1
$2 = (Base *) 0x7fffffffe550
(gdb) p &d1
$3 = (Derived1 *) 0x7fffffffe560
(gdb) p &d2
$4 = (Derived2 *) 0x7fffffffe570
(gdb) p &dd
$5 = (DDerived *) 0x7fffffffe580

四个对象的虚函数表:

(gdb) info vtbl b1
vtable for 'Base' @ 0x555555755cf8 (subobject @ 0x7fffffffe550):
[0]: 0x555555554f1c <Base::func1()>
[1]: 0x555555554f28 <Base::func2()>
(gdb) info vtbl d1
vtable for 'Derived1' @ 0x555555755cd0 (subobject @ 0x7fffffffe560):
[0]: 0x555555554f34 <Derived1::func1()>
[1]: 0x555555554f28 <Base::func2()>
[2]: 0x555555554f40 <Derived1::func3()>
(gdb) info vtbl d2
vtable for 'Derived2' @ 0x555555755ca8 (subobject @ 0x7fffffffe570):
[0]: 0x555555554f4c <Derived2::func1()>
[1]: 0x555555554f28 <Base::func2()>
[2]: 0x555555554f58 <Derived2::func4()>
(gdb) info vtbl dd
vtable for 'DDerived' @ 0x555555755c58 (subobject @ 0x7fffffffe580):
[0]: 0x555555554f34 <Derived1::func1()>
[1]: 0x555555554f28 <Base::func2()>
[2]: 0x555555554f40 <Derived1::func3()>

vtable for 'Derived2' @ 0x555555755c80 (subobject @ 0x7fffffffe590):
[0]: 0x555555554f4c <Derived2::func1()>
[1]: 0x555555554f28 <Base::func2()>
[2]: 0x555555554f58 <Derived2::func4()>
(gdb) x/30x 0x7fffffffe550
  • b1: 从0x7fffffffe550开始的16字节是对象b1的内存布局:先是Base虚表指针(0x555555755cf8),再是成员变量Base::a(0x11111111)。(再加上4字节的padding,大小刚好为16字节)
  • d1: 从0x7fffffffe560开始的16字节是对象d1的内存布局:先是Derived1虚表指针(0x555555755cd0),再是成员变量Base::a(0x11111111)、Derived1::b(0x22222222)。(大小为16字节)
  • d2: 从0x7fffffffe570开始的16字节是对象d2的内存布局:先是Derived2虚表指针(0x555555755ca8),再是成员变量Base::a(0x11111111)、Derived2::c(0x33333333)。(大小为16字节)
  • dd: 从0x7fffffffe580开始的40字节是对象dd的内存布局:先是DDerived(Derived1)虚表指针(0x555555755c58),再是成员变量Base::a(0x11111111)、Derived1::b(0x22222222)。(分界线)紧接着是 DDerived(Derived2)虚表指针(0x555555755c80),再是成员变量Base::a(0x11111111)、Dervied2::c(0x33333333)。(分界线)最后是DDerived::d(0x44444444)。(前面这部分就是d1的内存布局,后面半部分是d2的内存布局。总大小 = 16+16+4+4(padding) = 40 字节)
(gdb) x/50x 0x7fffffffe550
0x7fffffffe550: 0x55755cf8      0x00005555      0x11111111      0x00000001
0x7fffffffe560: 0x55755cd0      0x00005555      0x11111111      0x22222222
0x7fffffffe570: 0x55755ca8      0x00005555      0x11111111      0x33333333
0x7fffffffe580: 0x55755c58      0x00005555      0x11111111      0x22222222
0x7fffffffe590: 0x55755c80      0x00005555      0x11111111      0x33333333
0x7fffffffe5a0: 0x44444444      0x00007fff      0xd7956600      0xdc254519
0x7fffffffe5b0: 0x55554f70      0x00005555      0xf767ac87      0x00007fff
0x7fffffffe5c0: 0xffffff90      0xffffffff      0xffffe698      0x00007fff
0x7fffffffe5d0: 0xffffff90      0x00000001      0x55554d3a      0x00005555
0x7fffffffe5e0: 0x00000000      0x00000000      0x87c1f01a      0x14da98bf
0x7fffffffe5f0: 0x55554c30      0x00005555      0xffffe690      0x00007fff
0x7fffffffe600: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffe610: 0xd2a1f01a      0x418fcdea

初始化时机

  • 一个类的虚表初始化时机:编译期间,虚表位于.rodata(只读数据段),虚函数位于.text(代码段)。
  • 一个类对象的虚表指针(vptr)初始化时机:在类的构造函数期间。更具体的:由编译器插入的初始化动作,位于基类的构造函数之后,位于初始化列表之前。
#include <iostream>

using namespace std;

class Base {
public:
	virtual void func() {
		cout << "Base::func()" << endl;
	}

    Base() {
        cout << "Base::Base()" << endl;
    }
};

class Derived : public Base {
public:
	virtual void func() {
		cout << "Base::func()" << endl;
	}

    Derived() : a(0x12345678) {
        cout << "Derived::Derived()" << endl;
    }

private:
    int a;
};

int main() {
    Derived d;
}

callq 0x555555554b6e <Base::Base()> # 调用基类的构造函数

lea 0x201135(%rip),%rdx # 0x555555755d40 <_ZTV7Derived+16> # 初始化虚表指针(_ZTV代表虚函数表)

movl $0x12345678,0x8(%rax) # 初始化列表

lea 0x2013f5(%rip),%rdi # 0x555555756020 <_ZSt4cout@@GLIBCXX_3.4> # 执行构造函数主体

(gdb) disassemble /m Derived::Derived
Dump of assembler code for function Derived::Derived():
23          Derived() : a(0x12345678) {
   0x0000555555554bec <+0>:     push   %rbp
   0x0000555555554bed <+1>:     mov    %rsp,%rbp
   0x0000555555554bf0 <+4>:     sub    $0x10,%rsp
   0x0000555555554bf4 <+8>:     mov    %rdi,-0x8(%rbp)
   0x0000555555554bf8 <+12>:    mov    -0x8(%rbp),%rax
   0x0000555555554bfc <+16>:    mov    %rax,%rdi
   0x0000555555554bff <+19>:    
   0x0000555555554c04 <+24>:    lea    0x201135(%rip),%rdx        # 0x555555755d40 <_ZTV7Derived+16>
   0x0000555555554c0b <+31>:    mov    -0x8(%rbp),%rax
   0x0000555555554c0f <+35>:    mov    %rdx,(%rax)
   0x0000555555554c12 <+38>:    mov    -0x8(%rbp),%rax
   0x0000555555554c16 <+42>:    movl   $0x12345678,0x8(%rax)

24              cout << "Derived::Derived()" << endl;
   0x0000555555554c1d <+49>:    lea    0xcb(%rip),%rsi        # 0x555555554cef
   0x0000555555554c24 <+56>:    lea    0x2013f5(%rip),%rdi        # 0x555555756020 <_ZSt4cout@@GLIBCXX_3.4>
   0x0000555555554c2b <+63>:    callq  0x555555554940 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
   0x0000555555554c30 <+68>:    mov    %rax,%rdx
   0x0000555555554c33 <+71>:    mov    0x201396(%rip),%rax        # 0x555555755fd0
   0x0000555555554c3a <+78>:    mov    %rax,%rsi
   0x0000555555554c3d <+81>:    mov    %rdx,%rdi
   0x0000555555554c40 <+84>:    callq  0x555555554950 <_ZNSolsEPFRSoS_E@plt>

25          }
   0x0000555555554c45 <+89>:    nop
   0x0000555555554c46 <+90>:    leaveq 
   0x0000555555554c47 <+91>:    retq   

End of assembler dump.
(gdb) info vtbl d
vtable for 'Derived' @ 0x555555755d40 (subobject @ 0x7fffffffe5a0):
[0]: 0x555555554bb4 <Derived::func()>
(gdb) 

构造函数是否可以为虚函数

结论:不可以,编译器会报错。提示构造函数不能为虚函数。

error: constructors cannot be declared ‘virtual’ [-fpermissive]

因为调用虚函数需要通过虚函数表查找对应的虚函数。而虚函数表是通过对对象的虚表指针vptr指向的。而虚表指针又是在类的构造函数中完成初始化。

假设,构造函数是虚函数。我们new对象时会调用构造函数,由于构造函数是虚函数,需要通过对象的虚表指针,找到虚函数表,从而找到虚函数。但由于此时对象的虚表指针并没有初始化,所以出错。

析构函数是否可以为虚函数

结论:建议虚构函数为虚函数。除非能明确该类不会作为基类出现在继承层次中(final)。否则会出现错误(只调用基类的析构函数)。

出什么错呢?

如果基类没有将析构函数声明为虚函数,则当基类的指针执行派生类对象时,delete 基类指针。只会调用基类的构造函数。

因为,析构函数不为虚函数,不需要通过虚表查找,直接根据静态类型决定调用哪一个析构函数。

而将析构函数声明为虚函数时,就需要通过虚表查找,调用的是派生类的析构函数(派生类的析构函数会隐式调用基类的析构函数),从而完全释放资源。

#include <iostream>
#include <cstdio>

using namespace std;

class Base {
public:
    virtual ~Base() {
        cout << "Base::~Base()" << endl;
    }
};

class Base1 {
public:
    ~Base1() {
        cout << "Base1::~Base1()" << endl;
    }
};

class Derived : public Base {
public:
    ~Derived() {
        cout << "Derived::~Derived()" << endl;
    }
};

class Derived1 : public Base1 {
public:
    ~Derived1() {
        cout << "Derived1::~Derived1()" << endl;
    }
};

int main() {
    Base *b = new Derived();
    delete b;
    cout << "------------------" << endl;
    Base1 *b1 = new Derived1();
    delete b1;
}

输出:

Derived::~Derived()
Base::~Base()
------------------
Base1::~Base1()

构造函数和析构函数中是否可以调用虚函数

结论:语法上是可以的,但是没什么必要。

由于前面的“初始化时机”可以知道,虚表指针是在初始化列表前就已经完成初始化,按理说在构造函数和析构函数时,是可以调用虚函数的。

#include <iostream>
#include <cstdio>

using namespace std;

class Base {
public:
    virtual void func() {
        cout << "Base::func()" << endl;
    }
    Base() {
        cout << "Base::Base()" << endl;
        func();
    }
    ~Base() {
        cout << "Base::~Base()" << endl;
        func();
    }
};

class Derived : public Base {
public:
    virtual void func() override {
        cout << "Derived::func()" << endl;
    }
    Derived() {
        cout << "Derived::Derived()" << endl;
        func();
    }
    ~Derived() {
        cout << "Derived::~Derived()" << endl;
        func();
    }
};

int main() {
    cout << "------------------------" << endl;
    Base *b1 = new Base();
    delete b1;
    cout << "------------------------" << endl;
    Base *b2 = new Derived();
    delete b2;
    cout << "------------------------" << endl;
}
  • 可见在Base::Base() 和 Base::~Base()调用的都是Base::func(), 在Derived::Derived() 和 Derived::~Derived()调用的都是Derived::func()。

  • 类设计者可能希望在基类指针指向子类对象时,通过该基类指针调用的虚函数版本应该是子类的虚函数版本。但事实上并非如此。所以为了避免歧义,尽量不要在构造函数和析构函数调用虚函数。

  • 解析:派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。 同样,进入基类析构函数时,对象也是基类类型。

------------------------
Base::Base()
Base::func()
Base::~Base()
Base::func()
------------------------
Base::Base()
Base::func()
Derived::Derived()
Derived::func()
Base::~Base()
Base::func()
------------------------

更深入的

参考:c++ vtable 深入解析_non-virtual thunk to-优快云博客

  • 编译器是如何决定调用哪个虚函数:通过偏移量(下标)。比如类A有三个虚函数func1\func2\func3,调用func1时直接解引用虚表指针vptr(偏移量为0,下标为0),调用func2时,vptr加上偏移量0x8(下标为1)(64位,一个指针占8B),func3同理。
  • 虚表上面有类型信息typeinfo

哪些函数不可以为虚函数

  • 构造函数:原因见上
  • 静态函数:没有this指针,无法查找虚函数表
  • 友元函数:不属于类,没有this指针
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值