12 C++的多态

12 C++的多态文章目录一、多态的概念1.1.多态的构成条件二、虚函数2.1.虚函数的重写2.2.虚函数重写的例外协变:父类与子类虚函数返回值类型不同析构函数的重写(基类与派生类析构函数的名字不同)三、C++11override和final3.1.final:修饰虚函数,表示该虚函数不能再被重写3.2.override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错四、重载、覆盖(重写)、隐藏(重定义)的对比五、抽象类5.1.抽象类有什么用六、多态的应用场景举例七、多态的实现原理7.1.虚函


一、多态的概念

多态就是函数调用的多种形态,使用多态能够使得不同的对象去完成同一件事时,产生不同的动作和结果。

多态又分为静态多态和动态多态
(1)静态多态,也称为静态绑定或前期绑定(早绑定):函数重载和函数模板实例化出多个函数(本质也是函数重载)。静态多态也称为编译期间的多态,编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。

(2)动态多态,也称为动态绑定或后期绑定(晚绑定):在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,即运行时的多态。在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。

  1. 父类指针或引用指向父类,调用的就是父类的虚函数
  2. 父类指针或引用指向子类,调用的就是子类的虚函数

为什么动态多态无法在编译时就确定

在编译的时候编译器并不知道用户选择的是哪种类型的对象。如果不是虚函数,则采用静态绑定,函数体与函数调用在程序运行之前(编译期间)就绑定了。
当函数声明为虚函数时,如果使用指针或引用的形式,那么由于指针指向的对象不确定是基类还是派生类,那么编译器就无法采用静态绑定,所以就只有通过动态绑定的方式。因此编译器通过创建一个虚函数表存放虚函数的地址,在运行时,编译器通过虚函数指针在虚函数表中找到正确的函数版本,然后调用。

总结起来就是:不同的类型对象,去完成同一件事情,产生的动作是不一样的
比如抢红包的动作是一样的,但是每个人抢到的金额的大小是不同的,或者买票的时候,做的是同一个事情(买票)、但是不同的人去买票的价格是不一样的、普通乘客去买票是全价、学生去买票是半价,这些就是所谓的多态。

1.1. 多态的构成条件

多态是指不同继承关系的类对象,去调用同一函数,产生了不同的行为。在继承中要想构成多态需要满足两个条件:

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

二、虚函数

被virtual修饰的类成员函数被称作虚函数:
在这里插入图片描述

注意:

  1. 只有类的非静态成员函数前可以加virtual普通函数前不能加virtual
  2. 虚函数这里的virtual和虚继承中的virtual是同一个关键字,但是它们之间没有任何关系。虚函数这里的virtual是为了实现多态,而虚继承的virtual是为了解决菱形继承的数据冗余和二义性

2.1. 虚函数的重写

虚函数的重写也叫做虚函数的覆盖,若派生类中有一个和基类完全相同的虚函数(返回值类型相同、函数名相同以及参数列表完全相同),此时我们称该派生类的虚函数重写了基类的虚函数。

现在我们就可以通过基类的指针或者引用调用虚函数,此时不同类型的对象,调用的就是不同的函数,产生的也是不同的结果,进而实现了函数调用的多种形态。

总结起来就是,调用哪个类型的虚函数,取决于基类指针指向或引用的对象是哪种类型的对象

#include<iostream>
using namespace std;
class A
{ 
   
public:
    virtual void Print() { 
    cout << "A::Print" << endl; }
};

// 继承A类
class B : public A
{ 
   
public:
    virtual void Print() { 
    cout << "B::Print" << endl; }
};
// 继承B类
class C : public B
{ 
   
    virtual void Print() { 
    cout << "C::Print" << endl; }
};
// 继承A类
class D : public A
{ 
   
public:
    virtual void Print() { 
    cout << "D::Print" << endl; }
};
void Func(A& p)
{ 
   
    //通过父类的引用调用虚函数
    p.Print();
}
int main()
{ 
   
    A a; B b; C c; D d;

    A* pa = &a;
    B* pb = &b;
    D* pd = &d;
    C* pc = &c;

    //通过父类的指针调用虚函数
    pa->Print();  // a.Print()被调用,输出:A::Print
    pa = pb;
    pa->Print(); // b.Print()被调用,输出:B::Print
    pa = pc;
    pa->Print(); // c.Print()被调用,输出:C::Print
    pa = pd;
    pa->Print(); // d.Print()被调用,输出:D::Print

    //通过父类的引用调用虚函数
    Func(a);// a.Print()被调用,输出:A::Print
    Func(b);// b.Print()被调用,输出:B::Print
    Func(c);// c.Print()被调用,输出:C::Print
    Func(d); // d.Print()被调用,输出:D::Print

    return 0;
}

A类、B类、C类、D类的关系如下图:
在这里插入图片描述

如果不使用基类的指针或引用去调用虚函数,则只会调用基类的虚函数:
在这里插入图片描述

注意
在重写基类虚函数时,派生类的虚函数不加virtual关键字也可以构成重写,主要原因是因为继承后基类的虚函数被继承下来了,在派生类中依旧保持虚函数属性。
但是这种写法不是很规范,因为派生类也有可能会被继承,为了区分虚函数,建议在派生类的虚函数前也加上virtual关键字。

注意
虚函数重写只是重写函数的实现,继承的是父类的接口定义(声明),不会重写函数的缺省参数

在这里插入图片描述

2.2. 虚函数重写的例外

协变:父类与子类虚函数返回值类型不同

虚函数要求返回值类型相同、函数名相同以及参数列表完全相同,但是协变是个例外,子类重写基类虚函数时,与基类虚函数返回值类型可以不同。但是返回值类型也必须满足父子关系
在这里插入图片描述

如果不满足则会报错:
在这里插入图片描述

析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时子类的析构函数只要定义,无论是否添加virtual关键字,都与基类的析构函数构成重写。

虽然基类与派生类析构函数名字不同,但是编译器对析构函数的名字进行了特殊的处理,基类和派生类的析构函数构成隐藏。编译后析构函数的名称同一处理成destructor(),其目的是为了实现析构函数的多态。

这也就导致了一个问题,基类与派生类的析构函数名字相同,那指向派生类的基类指针调用的就是基类的析构函数:
在这里插入图片描述

由于没有调用派生类的析构函数,派生类如果开辟了资源就会没有释放,最终导致内存泄漏的问题,为了解决这个问题需要把析构函数写为虚函数:
在这里插入图片描述

所以,为了不出现内存泄漏的问题,基类的析构函数要加virtual,由于派生类的虚函数不加virtual关键字也可以构成重写,这样在delete就能够实现多态的正确调用析构函数


三、C++11 override和final

3.1. final:修饰虚函数,表示该虚函数不能再被重写

在这里插入图片描述

3.2. override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

在这里插入图片描述

四、重载、覆盖(重写)、隐藏(重定义)的对比

在这里插入图片描述


五、抽象类

在虚函数的后面写上=0,则这个函数为纯虚函数,包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象
派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

实现继承: 普通函数的继承是一种实现继承,派生类继承了基类函数的实现,可以使用该函数。
接口继承: 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态。

建议: 所以如果不实现多态,就不要把函数定义成虚函数。

5.1. 抽象类有什么用

通俗一点的将,抽象类就是不能用来具体描述某一种对象,没有对应的实体 :比如一个抽象类叫做”植物”,这个植物就是一个抽象类,没有具体对应的实体,如果重写这个类,比如重写这个植物是蒲公英,就有了对应的实体。


六、多态的应用场景举例

在面向对象的程序设计中使用「多态」,能够增强程序的可扩充性,即程序需要修改或增加功能的时候,需要改动和增加的代码较少。

多态的应用场景非常广泛,下面用设计王者农药游戏的英雄的例子,说明多态为什么可以在修改或增加功能的时候,可以较少的改动代码。

首先不用多态,定义两个英雄,妲己和后羿,它们的动作为攻击对方的函数和收到攻击生命值减少的函数:

// 基类
class Hero
{ 
   
protected:
    int m_nPower=1; //代表攻击力
    int m_nLifeValue=10; //代表生命值
};
// 妲己类
class DAJI : public Hero
{ 
   
public:
    // 攻击后羿的攻击函数
    void Attack(HOUYI* pHOUYI)
    { 
        
        pHOUYI->Hurted(m_nPower);
    }
    // 受到攻击减少自身生命值
    void Hurted(int nPower)
    { 
   
        m_nLifeValue -= nPower;
    }

};
// 后羿类
class HOUYI : public Hero
{ 
   
public:
    // 攻击妲己的攻击函数
    void Attack(DAJI* pDAJI)
    { 
   
        pDAJI->Hurted(m_nPower);
    }

    // 受到攻击减少自身生命值
    void Hurted(int nPower)
    { 
   
       m_nLifeValue -= nPower;
    }
};
int main()
{ 
   
    DAJI daji;
    HOUYI houyi;
    daji.Attack(&houyi);  //妲己攻击后羿
    houyi.Attack(&daji); //后羿攻击妲己
}

但是这样就有一个问题,假设再有一个英雄李白,那后羿和妲己的攻击函数就得需要增加攻击李白的函数,非常麻烦。

如果使用多态就能很好的解决这个麻烦:

// 基类
class Hero
{ 
   
public:
		//将攻击函数和被攻击的函数写为虚函数
    virtual void Attack(Hero* pHero) = 0;
    virtual void Hurted(int nPower) = 0;
protected:
    int m_nPower=1; //代表攻击力
    int m_nLifeValue=10; //代表生命值
};
// 妲己类
class DAJI : public Hero
{ 
   
public:
    // 攻击函数
    virtual void Attack(Hero* pHero)
    { 
   
        pHero->Hurted(m_nPower);
    }
    // 受到攻击减少自身生命值
    void Hurted(int nPower)
    { 
   
        m_nLifeValue -= nPower;
    }
};
// 后羿类
class HOUYI : public Hero
{ 
   
public:
    // 攻击函数
    virtual void Attack(Hero* pHero)
    { 
   
        pHero->Hurted(m_nPower);
    }
    // 受到攻击减少自身生命值
    void Hurted(int nPower)
    { 
   
       m_nLifeValue -= nPower;
    }
};
// 李白类
class LIBAI : public Hero
{ 
   
public:
    // 攻击函数
    virtual void Attack(Hero* pHero)
    { 
   
        pHero->Hurted(m_nPower);
    }
    // 受到攻击减少自身生命值
    void Hurted(int nPower)
    { 
   
        m_nLifeValue -= nPower;
    }
};
int main()
{ 
   
    DAJI daji;
    HOUYI houyi;
    LIBAI libai;
    daji.Attack(&houyi);  //妲己攻击后羿
    houyi.Attack(&daji);  //后羿攻击妲己
    daji.Attack(&libai);  //妲己攻击李白
}

如果增加了其他新的英雄比如鲁班,只需要编写新类LUBAN,不再需要在已有的类里专门增加攻击鲁班的函数,所以已有的类可以原封不动,可见改动量是非常少的。


七、多态的实现原理

「多态」的关键在于通过基类指针或引用调用一个虚函数时,编译时不能确定到底调用的是基类还是派生类的函数,运行时才能确定。

7.1. 虚函数表

class A
{ 
   
public:
    int i;
    virtual void Print() { 
    } // 虚函数
};

class B
{ 
   
public:
    int n;
    void Print() { 
    }
};

int main()
{ 
   
    A a;
    B b;
    cout << sizeof(a) << "," << sizeof(b);
    return 0;
}

在这里插入图片描述

A对象当中除了i成员外,实际上还有一个指针_vfptr放在对象的前面(有些平台可能会放到对象的最后面,这个跟平台有关)。
在这里插入图片描述

对象中的这个指针叫做虚函数表指针,简称虚表指针,虚表指针指向一个虚函数表,简称虚表,每一个含有虚函数的类中都至少有一个虚表指针。

#include <iostream>
using namespace std;
//父类
class Base
{ 
   
public:
	//虚函数
	virtual void Func1()
	{ 
   
		cout << "Base::Func1()" << endl;
	}
	//虚函数
	virtual void Func2()
	{ 
   
		cout << "Base::Func2()" << endl;
	}
	//普通成员函数
	void Func3()
	{ 
   
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
//子类
class Derive : public Base
{ 
   
public:
	//重写虚函数Func1
	virtual void Func1()
	{ 
   
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{ 
   
	Base b;
	Derive d;
	return 0;
}

在这里插入图片描述

虚表当中存储的就是虚函数的地址,因为父类当中的Func1和Func2都是虚函数,所以父类对象b的虚表当中存储的就是虚函数Func1和Func2的地址。
在这里插入图片描述

而子类虽然继承了父类的虚函数Func1和Func2,但是子类对父类的虚函数Func1进行了重写,因此,子类对象d的虚表当中存储的是父类的虚函数Func2的地址和重写的Func1的地址。这就是为什么虚函数的重写也叫做覆盖,覆盖就是指虚表中虚函数地址的覆盖,重写是语法的叫法,覆盖是原理层的叫法。

其次需要注意的是:Func2是虚函数,所以继承下来后放进了子类的虚表,而Func3是普通成员函数,继承下来后不会放进子类的虚表。此外,虚函数表本质是一个存虚函数指针的指针数组,一般情况下会在这个数组最后放一个nullptr。

虚表存在哪里

在这里插入图片描述

虚表是什么阶段初始化的

在这里插入图片描述

  1. 虚表指针在构造函数阶段(初始化列表)填入到对象中,虚表则是在编译时就生成好了。
  2. 虚表里面放的是虚函数地址,虚函数和普通函数一样,编译完成以后,都是放在代码段中。
  3. 一个类中所有的虚函数,都会放在虚表中。
  4. 子类会将父类的虚表拷贝一份,然后用重写的虚函数地址覆盖掉原来虚表中的函数地址,因此虚函数的重写,也叫虚函数的覆盖。

7.2. 虚表如何实现多态

当满足多态条件以后,父类的指针或引用调用虚函数时,不是编译时确定的,而是运行时到指向的对象中的虚表中去找对应的虚函数调用,并且引用的底层也是由指针实现,父类在指向子类时会发生切片。所以指针指向父类的对象,调用的就是父类的虚函数,指向的是子类对象,调用的就是子类的虚函数。

#include <iostream>
using namespace std;
//父类
class Person
{ 
   
public:
	virtual void BuyTicket()
	{ 
   
		cout << "买票-全价" << endl;
	}
	int _p = 1;
};
//子类
class Student : public Person
{ 
   
public:
	virtual void BuyTicket()
	{ 
   
		cout << "买票-半价" << endl;
	}
	int _s = 2;
};
int main()
{ 
   
	Person Mike;
	Student Johnson;
	Johnson._p = 3; //以便观察是否完成切片
	Person* p1 = &Mike;
	Person* p2 = &Johnson;
	p1->BuyTicket(); //买票-全价
	p2->BuyTicket(); //买票-半价
	return 0;
}

在这里插入图片描述

如果使用父类对象而不是父类指针时,切片得到部分成员变量后,会调用父类的拷贝构造函数对那部分成员变量进行拷贝构造,虚表指针是不会拷贝的,否则就会发生混乱(父类的虚表里有子类的虚函数),因此拷贝构造出来的父类对象p1和p2当中的虚表指针指向的都是父类对象的虚表。因为同类型的对象共享一张虚表,他们的虚表指针指向的虚表是一样的。

Person p1 = Mike;
Person p2 = Johnson;

在这里插入图片描述

通过查看汇编的方式进一步理解静态绑定和动态绑定:

在这里插入图片描述


八、多继承关系的虚函数表

单继承的虚表比较简单:
在这里插入图片描述

多继承:

#include<iostream>
using namespace std;
//基类1
class Base1
{ 
   
public:
	virtual void func1() { 
    cout << "Base1::func1()" << endl; }
	virtual void func2() { 
    cout << "Base1::func2()" << endl; }
private:
	int _b1;
};
//基类2
class Base2
{ 
   
public:
	virtual void func1() { 
    cout << "Base2::func1()" << endl; }
	virtual void func2() { 
    cout << "Base2::func2()" << endl; }
private:
	int _b2;
};
//多继承派生类
class Derive : public Base1, public Base2
{ 
   
public:
	virtual void func1() { 
    cout << "Derive::func1()" << endl; }
	virtual void func3() { 
    cout << "Derive::func3()" << endl; }
private:
	int _d1;
};

typedef void(*VFPTR)(); //虚函数指针类型重命名
//打印虚表地址及其内容
void PrintVFT(VFPTR* ptr)
{ 
   
	printf("虚表地址:%p\n", ptr);
	for (int i = 0; ptr[i] != nullptr; i++)
	{ 
   
		printf("ptr[%d]:%p-->", i, ptr[i]); //打印虚表当中的虚函数地址
		ptr[i](); //使用虚函数地址调用虚函数
	}
	printf("\n");
}
int main()
{ 
   
	Base1 b1;
	Base2 b2;
	PrintVFT((VFPTR*)(*(void**)&b1)); //打印基类对象b1的虚表地址及其内容
	PrintVFT((VFPTR*)(*(void**)&b2)); //打印基类对象b2的虚表地址及其内容
	

	Derive d;
	PrintVFT((VFPTR*)(*(void**)&d)); //打印派生类对象d的第一个虚表地址及其内容
	PrintVFT((VFPTR*)(*(void**)((char*)&d + sizeof(Base1)))); //打印派生类对象d的第二个虚表地址及其内容
	return 0;
}

在这里插入图片描述

重写了func1,但是在两个虚表之中的func1地址却不一样,这主要是因为虚表之中的指针发生了跳转,最后都跳转到同一个地址上,最终打印的都是func1。


总结

  1. 什么是多态?

多态分为静态多态和动态多态,静态多态在编译时就已经确定好了,动态多态在运行时才会确定
常见静态多态是函数重载,动态多态则是通过对父类虚函数的重写,通过虚表,达到传入谁的指针就调用谁的函数的目的。

  1. 什么是重载、重写(覆盖)、重定义(隐藏)?

重载:同一作用域内,函数名相同参数不同
重写:子类和父类的虚函数,名称、返回值、参数都相同,称子类重写了父类的虚函数
重定义:子类和父类的函数名相同,称子类隐藏了父类的某个函数。

  1. 多态的实现原理:

父类和子类之中保存的虚表指针是不一样的,通过传入指针或者引用(本质也是指针)确定去子类还是父类之中去寻找虚表指针,最后达到调用不同虚函数的目的。

  1. inline可以是虚函数吗?

可以,在VS之下,如果构成多态,编译器会舍弃inline属性,这个函数就不是内联函数了,因为内联函数会被展开,是没有地址的。而虚函数会将其地址放入至虚表之中。

如果不构成多态直接调用,则内联展开。在类里面定义的函数默认内联

  1. 静态成员可以是虚函数吗?

不能,静态成员没有this指针,也就是没有对象,没有对象代表没有虚表指针指向虚表(虚表指针是存储在对象之中的)。

  1. 构造函数可以是虚函数吗?

不可以。
定义虚函数的目的是构成多态,多态调用要去对象的虚表中找到虚函数。因为虚函数表指针是在构造函数初始化阶段才初始化的(将虚函数地址填入虚函数表之中)。所以在构造函数阶段,对应的虚表指针还没有初始化,自然无法找到虚表中的构造函数。这是典型的“先有鸡还是先有蛋”问题。

  1. 析构函数可以是虚函数吗?

最好将析构函数定义成虚函数,否则会出现指向子类对象的父类指针调用父类析构函数而不会调用子类虚构函数的情况,出现内存泄漏。

  1. 对象访问普通函数快还是虚函数更快?

不构成多态的时候,在编译时就确定了如何调用,因此是一样快的。
构成多态时,访问普通函数快,因为访问虚函数首先需要去对象之中找到指针,然后通过指针找到虚表之中去寻找函数地址。

  1. 虚函数表示在什么阶段、在哪里生成的?

虚函数表是在编译阶段生成的,一般情况下存在代码段。

  1. C++菱形继承的问题?虚继承的原理?

菱形虚拟继承因为子类对象当中会有两份父类的成员,因此会导致数据冗余和二义性的问题。
虚继承对于相同的虚基类在对象当中只会存储一份,若要访问虚基类的成员需要通过虚基表获取到偏移量,进而找到对应的虚基类成员,从而解决了数据冗余和二义性的问题。

  1. 什么是抽象类,抽象类的作用?

抽象类不能实例化出对象,描述的事物没有对应的实体,另外抽象类体现出了接口继承的关系。


补充内容

子类的虚函数为私有成员

按理来讲,一个类的私有函数是无法在类外访问的,但是如果其为虚函数,则可以被基类指针或引用访问。

#include <iostream>
using namespace std;
//父类
class Base
{ 
   
public:
	//虚函数
	virtual void Func1()
	{ 
   
		cout << "Base::Func1()" << endl;
	}
private:
	int _b = 1;
};
//子类
class Derive : public Base
{ 
   
private://Derive的Func1函数为私有函数
	//重写虚函数Func1
	virtual void Func1()
	{ 
   
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
void Func(Base& p)
{ 
   
	//通过父类的引用调用虚函数
	p.Func1();
}
int main()
{ 
   
	Derive d;
	Func(d);
	return 0;
}

在这里插入图片描述

这主要是因为虚表中有虚函数的地址,该虚函数在父类中是公有的,那在子类中也还是公有的,如果把父类中的函数设为私有,那就无法访问。
在这里插入图片描述

虚函数并不一定就是动态绑定

首先,动态绑定的前提是引用或指针指向的对象不确定是基类还是派生类,这句话是什么意思呢?看下面的代码:

#include <iostream>
using namespace std;
//父类
class Base
{ 
   
public:
	//虚函数
	virtual void Func1()
	{ 
   
		cout << "Base::Func1()" << endl;
	}
private:
	int _b = 1;
};
//子类
class Derive : public Base
{ 
   
public://Derive的Func1函数为私有函数
	//重写虚函数Func1
	virtual void Func1()
	{ 
   
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};

int main()
{ 
   
	Base b;
	Derive d;

	b.Func1();
	d.Func1();

	Base* pb1 = &b;
	Base* pb2 = &d;
	pb1->Func1();
	pb2->Func1();
	return 0;
}

来看一下上面四个Func1函数调用的的汇编代码:

	b.Func1();
0023278F  lea         ecx,[b]  
00232792  call        Base::Func1 (0231087h)  
	d.Func1();
00232797  lea         ecx,[d]  
0023279A  call        Derive::Func1 (0231465h)  

	Base* pb1 = &b;
0023279F  lea         eax,[b]  
002327A2  mov         dword ptr [pb1],eax  
	Base* pb2 = &d;
002327A5  lea         eax,[d]  
002327A8  mov         dword ptr [pb2],eax  


	pb1->Func1();
002327AB  mov         eax,dword ptr [pb1]  
002327AE  mov         edx,dword ptr [eax]  
002327B0  mov         esi,esp  
002327B2  mov         ecx,dword ptr [pb1]  
002327B5  mov         eax,dword ptr [edx]  
002327B7  call        eax  
002327B9  cmp         esi,esp  
002327BB  call        __RTC_CheckEsp (02312ADh)  
	pb2->Func1();
002327C0  mov         eax,dword ptr [pb2]  
002327C3  mov         edx,dword ptr [eax]  
002327C5  mov         esi,esp  
002327C7  mov         ecx,dword ptr [pb2]  
002327CA  mov         eax,dword ptr [edx]  
002327CC  call        eax  
002327CE  cmp         esi,esp  
002327D0  call        __RTC_CheckEsp (02312ADh)  

	Derive* pd = &d;
002327D5  lea         eax,[d]  
002327D8  mov         dword ptr [pd],eax  
	pd->Func1();
002327DB  mov         eax,dword ptr [pd]  
002327DE  mov         edx,dword ptr [eax]  
002327E0  mov         esi,esp  
002327E2  mov         ecx,dword ptr [pd]  
002327E5  mov         eax,dword ptr [edx]  
002327E7  call        eax  
002327E9  cmp         esi,esp  
002327EB  call        __RTC_CheckEsp (02312ADh)

可以看到b.Func1();d.Func1();这两句中,函数地址都是确定的,直接进行call调用。也就是说,函数地址在编译时就确定,属于静态绑定。

而通过指针调用的函数,不管是父类指针调用,还是子类指针调用,它们调用的汇编代码都是一样的:

// pb1中存的是b对象的指针,将pb1移动到eax中
002327AB  mov         eax,dword ptr [pb1]  
// [eax]就是取eax值指向的内容,这里相当于把b对象头4个字节(虚表指针)移动到了edx
002327AE  mov         edx,dword ptr [eax]  
002327B0  mov         esi,esp  
002327B2  mov         ecx,dword ptr [pb1]  
// [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
002327B5  mov         eax,dword ptr [edx]  
// call eax中存虚函数的指针。
//这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后到对象的中取找的。
002327B7  call        eax  
002327B9  cmp         esi,esp  
002327BB  call        __RTC_CheckEsp (02312ADh)  

从这里可以看出,动态多态是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数。

并且,如果虚函数直接通过对象来运行,则是在编译时就确定地址,如果通过指针或引用来运行,则是在运行时确定地址。

虚表中存的是虚函数的跳转地址

前面说过,两个虚表之中的func1地址却不一样,这主要是因为虚表之中的指针发生了跳转,最后都跳转到同一个地址上,最终打印的都是func1。
在这里插入图片描述

来看一下是如何跳转的,以下面的代码为例:

#include <iostream>
using namespace std;
//父类
class Base
{ 
   
public:
	//虚函数
	virtual void Func1()
	{ 
   
		cout << "Base::Func1()" << endl;
	}
private:
	int _b = 1;
};
//子类
class Derive : public Base
{ 
   
public:
	//重写虚函数Func1
	virtual void Func1()
	{ 
   
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};

int main()
{ 
   
	Base b;
	Derive d;
	return 0;
}

在这里插入图片描述

当多继承时重写同一个函数,会把最终跳转地址放在先继承的虚表中

比如下面的代码:

#include <iostream>
using namespace std;
//父类
class Base1 { 
   
public:
	virtual void func1() { 
    cout << "Base1::func1" << endl; }
	virtual void func2() { 
    cout << "Base1::func2" << endl; }
	int b1;
private:
};
class Base2 { 
   
public:
	virtual void func1() { 
    cout << "Base2::func1" << endl; }
	virtual void func2() { 
    cout << "Base2::func2" << endl; }
private:
	int b2;
};
class Derive : public Base1, public Base2
{ 
   
public:
	//重写func1
	virtual void func1()
	{ 
   
		cout << "Derive::func1()" << endl;
	}
private:
	int _d = 2;
};

int main()
{ 
   
	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;

	p1->func1();
	p2->func1();

	return 0;
}

在这里插入图片描述

可以看到,先继承Base1,最终跳转的jmp命令放在Base1的虚表中,而Base2的虚表中,则放的是跳转到Base1虚表指向的jmp命令的命令。

今天的文章12 C++的多态分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:http://bianchenghao.cn/8526.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注