多态的实现及原理

多态的概念

        多态从字面意思上来看就是多种形态,具体的就是完成某个行为,当不同的对象去完成时会产生不同的状态,比如当学生和普通人买票的时候,针对不同用户的票价是不一样的。

多态的实现

要想实现多态,必须满足以下条件:1、在继承条件下。2、必须通过基类的指针或者引用调用虚函数。3、被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。

虚函数:被virtual修饰的类成员函数称为虚函数。

class Person{
public:
    virtual void BuyTicket(){}
};

 虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的 返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。     

class Person
{
public:
    virtual void BuyTicket() {
        cout << "person--买票--全票" << endl;
    }
};
class Student:public Person
{
public:
    virtual void BuyTicket() {
        cout << "Student--买票--半票" << endl;
    }
};
class Soldier :public Person
{
public:
    virtual void BuyTicket() {
        cout << "Soldier--买票--优先半票" << endl;
    }
};

上面三个函数构成了(重写)覆盖关系。

主函数:

void func(Person& p)
{
    p.BuyTicket();
}


int main()
{
    Person p;
    Student st;
    Soldier sd;

    func(p);
    func(st);
    func(sd);

    //多态调用:(指针或者引用)指向的对象有关系
    //普通对象调用:跟调用的对象有关

    return 0;
}

这里的func里面的参数是个引用,所以这里构成了多态

如果将这里的引用去掉,改成

void func(Person p)
{
    p.BuyTicket();
}


int main()
{
    Person p;
    Student st;
    Soldier sd;

    func(p);
    func(st);
    func(sd);

    //多态调用:(指针或者引用)指向的对象有关系
    //普通对象调用:跟调用的对象有关

    return 0;
}

 这里就只是一个普通调用

 多态调用:跟指针或者引用 的对象有关系。普通调用:只跟调用对象有关系。

对于虚函数的重写的两个例外:

1.协变(基类与派生类虚函数返回值类型不同):派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(这里的返回值也可以是其他父类和子类的引用或者指针)

//协变的代码
class Person {
public:
    virtual void BuyTicket()     //虚函数
    {
        cout << "Person---买票----全票" << endl;
    }
};
class Student :public Person
{
public:
    virtual Person* BuyTicket()
    {
        cout << "Student---买票----半票" << endl;
    }

};
class Soldier :public Person {
public:
    virtual Person* BuyTicket()
    {
        cout << "Soldier---买票----优先买票" << endl;
    }
};

2.对于子类虚函数,可以省略掉virtual,建议都加上,子类中不写virtual也是可以的,对虚函数进行重写,重写的是实现。

对于析构函数而言,建议继承关系下的析构函数写成虚函数。

class Person {
public:
	 ~Person()
	{
		cout << "~Person的析构函数" << _p << endl;
		delete[] _p;
	}
protected:
	int* _p = new int[10];
};

class Student :public Person {
public:
	~Student()
	{
		cout << "~Student的析构函数" << _s << endl;
		delete[] _s;
	}
protected:
	int* _s = new int[10];
};
int main()
{
	Person p;
	Student s;

	return 0;
}

这里能够正确的析构,先析构s,在析构p

 对于普通成员而言,这里是否是虚函数影响不大,但是对于指针的场景下

class Person {
public:
	 ~Person()
	{
		cout << "~Person的析构函数" << _p << endl;
		delete[] _p;
	}
protected:
	int* _p = new int[10];
};

class Student :public Person {
public:
	~Student()
	{
		cout << "~Student的析构函数" << _s << endl;
		delete[] _s;
	}
protected:
	int* _s = new int[10];
};
int main()
{

	Person* ptr1 = new Person;
	Person* ptr2 = new Student;  //前面这个Person 是类型  后面是对象
	delete ptr1;
	delete ptr2;
	return 0;
}

这里发生了内存泄漏,student类并没有调用到自己的析构函数,为什么呢?

首先明确一下delete的行为:1、使用指针调用析构函数;2、operator delete(ptr)(本质是free)。

这里使用指针调用,是个普通调用,跟对象的类型有关,ptr1和ptr2都是person类,但是对于ptr2而言,他其实是一个student类,并没有释放掉_s的指针,发生了内存泄漏。使用指针调用析构函数这块儿,跟指针类型无关系,他只跟你指向对象的有关系。多态调用--->调用对应的析构函数---->跟引用或者指针所指的对象有关。所以这里应该是一个多态调用。

其实对于这两个类的析构函数的函数名是相同的,编译器会自动把析构函数的函数名进行修饰成destructor,再加上virtual 构成函数重写,既可以达到多态调用的目的。

class Person {
public:
	virtual ~Person()
	{
		cout << "~Person的析构函数" << _p << endl;
		delete[] _p;
	}
protected:
	int* _p = new int[10];
};

class Student :public Person {
public:
	virtual ~Student()
	{
		cout << "~Student的析构函数" << _s << endl;
		delete[] _s;
	}
protected:
	int* _s = new int[10];
};

 所以对于父类和子类的重写函数而言,父类析构函数加上virtual,子类析构函数不加virtual在这里会更有意义。

所以无脑给继承到析构函数加上virtual即可,唯一的缺点就是析构函数会进入虚表,效率会有一定的损失。

重载、重写(覆盖)、隐藏(重定义)的区别:

 

 关键字override 和final

如何构造一个最终类:1、把构造函数放到private 里面是无法被继承的。2、给final:禁止该类被继承   --最终类,这是给父类中的虚函数声明用的,在编译的时候就会开始检查

override 关键字是对子类的虚函数是否完成重写进行检查。

抽象类

在虚函数的后面写上=0,则这个函数为纯虚函数,包含纯虚函数的类就做抽象类,抽象类不能够实例化对香港,派生类继承之后也不能实例化出对象,只有重写纯虚函数,派生 类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。这里就引入了接口继承实现继承的概念:普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实 现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成 多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

多态的原理

class Base
{
public:
    virtual void A()
    {}
private:
    int _b;
};
using namespace std;
int main()
{
    Base b;
    cout<<sizeof(Base)<<endl;
    return 0;
}

 这个A对象的大小为8byte,因为带有虚函数表 ,_vfptr,是个指针,指向的是虚基表,一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

虚表(通过打印栈,堆,静态区,常量区)在windows系统下是存储在常量区/代码区的。

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

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

class Derive:public Base{
public:
    virtual void Func1()
    {
        cout << "Derive::Func1()" << endl;
    }
    virtual void Func2()
    {
        cout << "Derive::Func2()" << endl;
    }
    virtual void Func5()
    {
        cout << "Derive::Func5()" << endl;
    }
    void Func3()
    {
        cout << "Derive::Func3()" << endl;
    }

};

int main()
{
    Base b;
    Derive d;
    //普通调用,什么类型调用什么函数,是在编译阶段绑定
    Base* ptr = &b;
    ptr->Func3();
    ptr = &d;
    ptr->Func3();

    //多态调用,跟类型无关,跟对象有关系
    //动态绑定,在运行的时候进行绑定
    ptr = &b;
    ptr->Func1();
    ptr = &d;
    ptr->Func1();
}

1.派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针就是其中一部分;另一部分是自己的成员。

2.基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

3.另外Func3继承下来后不是虚函数,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。

4.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后放了一个nullptr

总结派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中。b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数。c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

另外注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是 他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。

还涉及到了一个动静态绑定的问题,对于非虚函数,同名成员函数构成了隐藏(重定义)。

1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载

2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

普通调用,什么对象调用什么函数,在编译的时候及逆行静态绑定,程序在编译阶段,就可以通过此时编译的类的类型,锁定调用的函数。

多态调用,是运行中进行动态绑定,多态调用是一种动态调用,在编译的时候无法确定调用那个函数(父类或者子类),因为不管指向的这个对象是子类还是父类对象,父类指针指向父类,看到的父类对象,指向子类,看到的是子类对象中父类的部分。

所以满足多态的条件:1.虚函数的覆盖。2.父类对象的指针或者引用调用虚函数,通过汇编代码可以看出,满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象中去寻找到的,不满足多态函数调用是在编译阶段确认好的。

单继承和多继承关系的虚函数表

单继承模型下的虚表模型

对于vs的监视窗口而言,当虚表中的虚函数超过三个之后,不能够完全显示,所以需要用到地址窗口进行查看。下图左边是内存窗口,右边是地址窗口

 打印虚函数地址的代码

#include <iostream>
using namespace std;


class Base {
public:
    virtual void func1() { cout << " Base ::func1()" << endl; }
    virtual void func2() { cout << " Base ::func2()" << endl; }
};
class Derive :public Base {
public:
    virtual void func1() { cout << " Derive::func1()" << endl; }
    virtual void func3() { cout << " Derive::func3()" << endl; }
    virtual void func4() { cout << " Derive::func4()" << endl; }
};

typedef void(*VF_Ptr)() ;   //函数的重命名--函数指针   -

void PrintVale(VF_Ptr vft[])   //传了一个函数指针数组
{
    for(int i=0;vft[i] !=nullptr;i++)   //windows下的虚表的结尾是以空指针进行结尾
    {
        printf("[%d]-->%p    ", i, vft[i]);
        vft[i]();
    }
    cout << endl;
}


int main()
{
    Base b;
    Derive d;

    PrintVale((VF_Ptr*)(*(void **)&b));   //32位的    ///64位的机器下  //  32/64位下都能运行,void** 
    PrintVale((VF_Ptr*)(*(void **)&d));
}

 结果

 

 对于被重写的虚函数,会将新地址覆盖了原来虚函数的地址,而没有被重写的地址将会被保留。

对于打印虚函数的地址而言,虚表位于整个类的前四个字节,因为是一个指针,所以只占用四个字节,然后遍历整个虚表,用函数指针,指向对应的函数并且调用。

多继承模型下的虚表模型

多继承模型下的虚表模型,则是把两个父类的虚表均继承下来,完成重写的覆盖即可,未完成重写的进行保留。

 1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)

 如果完成覆盖,在相应位置修改被覆盖的函数地址即可。

菱形继承下的虚表模型

菱形继承中为了解决数据的冗余性和二义性,引入了虚继承,再虚继承后会有一张虚基表,

当同时有了虚表和虚基表的对象模型则是如下:

 

 

#include <iostream>
using namespace std;
class A {
public:
    int _a;
public:
    virtual void func1(){}
};

class B :virtual public A{
public:
    int _b;
public:
    virtual void func1() {}
    virtual void funcb(){}
};
 
class C :virtual public A {     //虚继承--》数据的冗余和二义性
public:
    int _c;
    virtual void func1(){}     //虚函数   --多态有关系
    virtual void funcc() {}
};

class D :public B, public C {
public:
    int _d;
public:
    virtual void func1() {}
};


int main()
{
    D d;
    d.B::_a = 1;
    d.C::_a = 2;
    d._b = 3;
    d._c = 4;
    d._d = 5;

    return 0;
}

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值