多态的深入理解

Posted  落禅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多态的深入理解相关的知识,希望对你有一定的参考价值。

多态的深入理解

1.多态

通俗的说,就是多种形态,具体点的就是去完成莫格欣慰,当不同的对象去完成时会产生不同得形态

2.多态构成的条件

(1).必须通过基类的指针或者引用调用虚函数

(2).被调用得函数必须是虚函数,且派生类必须对基类的虚函数进行重写

class Person
{
    public:
    virtual void ByTicket()
    {
        cout<<"买全价票"<<endl;
    }
}

class Student:public Person
{
    public:
    virtual void BuyTicket()
    {
        cout<<"买半价票"<<endl;
    }
}

void Func(Person &people)
{
	people.BuyTicket(); 
}

void test()
{
    Person Mike;
    Func(Mike);
    Student Johnson;
    Func(Johnson);
}

3.虚函数

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

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

注:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后的虚函数也被继承了下来了在派生类中依旧保持虚函数属性)

虚函数重写的两个意外:

1.协变(基类与派生类虚函数返回值不同)

派生类在重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变

//父类虚函数返回父类对象的指针或者引用,子类虚函数返回值子类对象的指针或者引用
class A
{
    public:
    virtual A* f()
    {
        
        return new A;
    }
}
class B:public A
{
    virtual void B* f()
    {
        return new B;
    }
}

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

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数不同,但是编译器编译后析构函数的名称统一为destructor

class Person
{
    virtual ~Person()
    {
        cout<<"~Person()"<<endl;
    }
}
class Student:public Person
{
    public:
    virtual ~Student()
    {
        cout<<"~Student"<<endl;
    }
}
//只有派生类Student和析构函数重写了Person的析构函数,下面调用delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数
int main()
{
    Person*p1=new Person;
    Person*ps=new Student;
    delete p1;
    delete p2;
    return 0;
}

4.C++11中的override和final

1.final

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

class A
{
    public:
    virtual void func() final
    {
        
    }
}

class B:public A
{
    public:
    virtual void func ()
    {
        cout<<"呵呵"<<endl;
    }
}
//上述代码会出现错误

2.override

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

class A
{
    public:
    virtual void func() 
    {
        
    }
}

class B:public A
{
    public:
    virtual void func override ()
    {
        cout<<"呵呵"<<endl;
    }
}

5.重载,覆盖(重写),隐藏(重定义)的对比

1.函数重载:
两个函数必须同一个作用域

函数名/参数相同

2.重写(覆盖):

两个函数分别在基类和派生类的作用域

函数名/参数/返回值必须相同(协变例外)

两个函数必须是虚函数

3.重定义(隐藏):

两个函数分贝在积累和派生类的作用域

函数名相同

两个基类和派生类的同名函数不构成重写就是重定义

6.抽象类

1.概念:在虚函数的后面写上=0,则这个函数被称为纯虚函数,包含冲虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化对象,派生类继承后也不能实例化出对象,只有重新二纯虚函数,派生类才能实例化出对象,纯虚函数规范了派生类必须进行重写,另外冲虚函数体现出来接口继承

class car
{
    public:
    virtual void Drive()=0;
}

class BMW:public car
{
    public:
    virtual void Drive()
    {
        cout<<"宝马"<<endl;
    }
}
void test()
{
    car*C=new BWM;
    C->Drive();
}

接口类的继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承是函数的实现,纯虚函数的继承是一种接口继承,派生类继承的是基类函数的接口,目的是为了重写,达成多态,继承的是接口,所以如果不是先多态,不要把函数定义为虚函数

7.多态的实现原理

1虚函数表

class Base
{
    public:
    virtual void Func1()
    {
        cout<<"Func1"<<endl;
    }
    private:
    int _b=1;
}
//sizeof(Base)

通过观察测试我们发现b对象的大小为8bytes,除了_b成员外,还多了一个 _vfptr放在对象的前面,对象中的指针我们叫做虚函数表指针(v代表virtual,f代表指针),一个虚函数的类中至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,这个虚函数表指针(vfptr)指向虚函数表,虚函数表里面存放了虚函数的地址,根据虚函数表里面的地址找到对应的函数

class Base
{
    public:
    virtual void Func1()
    {
        cout<<"Func1"<<endl;
    }
    virtual void Fun2()
    {
        cout<<"Func2"<<endl;
    }
    void Fun3()
    {
        cout<<"Func3"<<endl;
    }
    private:
    int _b=1;
}
class Derive:public Base
{
    public:
    virtual void Func1()
    {
        cout<<"Derive ::Func1()"<<endl;
    }
    private:
    int _d=2;
}
int main()
{
    Base b;
    Derive d;
    return 0;
}

为什么多态是父类的指针或者引用指向子类或者父类的对象而不是对象

如果是父类的指针或者引用,切片时会将子类或者父类的虚函数表切过去,而如果是父类的对象,切片时则不会将子类对象的虚表切过去,无法构成多态

相同类型的对象共享同一张虚函数表

满足多态的条件后,构成多提:
指针或者引用,调用虚函数时,不是在编译时确定,是运行时指向的对象中的虚表中去找对应的虚函数调用
所以指向的父类函数对象,调用的就是父类的虚函数,指向子类就掉子类的虚函数

需要注意的时,如果不构成多态,那么这里调用的时候编译器确定那个调用那个函数,主要看p的类型
调用的时Person的BuyTciket,跟传什么没有关系

构成多态,指向谁调用谁的虚函数,跟对象有关
不构成多态,

多态调用时必须是父类的指针或者引用调用:
传参数会发生拷贝,如果对象调用会使得子类的虚表拷贝不过案例,找不到函数的地址,发生不了多态
父类的指针和引用,切片时时指向或者引用子类或者子类对象中切出来的一部分
父类对象时,
同类型的对象,他们共享同一个虚表

对象中的虚表指针在什么阶段初始化:构造函数初始化
虚表在那个地方生成:虚表是在编译是就生成好了

虚表里面放虚函数的指针,虚函数和普通函数一样,编译完成后,都放在代码段

所有的虚函数都会放在虚表中

重写是语法层的概念,覆盖是原理

虚表是存在于代码段
验证方法:取出虚表的地址(一个对象的前四个字节)与栈区,堆区,静态区,代码区进行对比

多继承:一个类继承其它两个类

#include<iostream>
using namespace std;
class A
{
public:
	virtual void Func1()
	{
	}
	virtual void Func2()
	{
	}
};
class B 
{
public:
	virtual void Func1()
	{
	}
};

class C :public A, public B
{
public:
	virtual void Func1()
	{
	}

};

int main()
{
	C c;

	return 0;
}

由上图我们可以看出,如果一个类继承与另外两个类,那么它将会有两张虚表,如上图中c的虚表继承自A,B,并且重写了里面的Func1函数

如下所示,如果c中除了重写的虚函数之外还有其它函数那么其它虚函数该存储在那个表里呢?

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

class C :public A, public B
{
public:
	virtual void Func1()
	{
		cout << "C::Func1()" << endl;
	}
	virtual void Fun5()
	{
		cout << "C::Func5()" << endl;
	}

};

答案是第一张表,我们可以根据地址打印上面的函数,代码如下所示:

#include<iostream>
#include<stdio.h>
using namespace std;
class A
{
public:
	virtual void Func1()
	{
		cout << "A::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "A::Func2()" << endl;
	}
	int a = 1;
};
class B
{
public:
	virtual void Func1()
	{
		cout << "B::Func1()" << endl;
	}
	int b = 1;
};

class C :public A, public B
{
public:
	virtual void Func1()
	{
		cout << "C::Func1()" << endl;
	}
	virtual void Fun5()
	{
		cout << "C::Func5()" << endl;
	}

	int c = 1;
};
typedef void(*VFunc)();//指向函数的指针
void PrintVFT(VFunc* ptr)//VFunc*ptr:指向函数指针的数组
{
	printf("虚表地址:%p\\n", ptr);
	for (int i = 0; ptr[i] != nullptr; ++i)
	{
		printf("VFT[%d]:%p->", i, ptr[i]);
		ptr[i]();
	}
	printf("\\n");
}

int main()
{
	C c;
	PrintVFT((VFunc*)(*(int*)&c));//拿到第一组表里面的函数地址
	PrintVFT((VFunc*)(*(int*)((char*)&c + sizeof(A))));//拿到第二张表里面的地址
	return 0;
}

结论:在多继承关系中子类中都多余的虚函数放在第一张表中

感谢您的阅读,期待下次相见!

以上是关于多态的深入理解的主要内容,如果未能解决你的问题,请参考以下文章

多态的深入理解

多态的深入理解

多态的深入理解

深入理解多态

深入理解多态

深入理解多态从“妈妈我想吃烤山药”讲起