就是去完成某一项行为的时候,对于不同的对象会产生不同的效果。比如当一个人去开车的时候,他开的是五菱宏光还是劳斯莱斯所带来的感受是完全不一样的。
多态是在不同关系的类对象,去调用同一函数,产生了不同的效果。
多态使用的两个条件:
1.必须是通过父类的指针或者引用调用虚函数。
2.被调用的函数必须是虚函数,而且子类重写了父类这个虚函数。
虚函数:被virtual修饰的类成员函数就是虚函数。
注意,此处不要和虚继承搞混淆了,在继承关键字前加virtual是虚继承。虚继承是为了解决菱形继承带来的数据冗余和二义性问题的。
虚函数的重写(覆盖):就是子类中有一个完全跟父类虚函数相同的虚函数(即满足三同)。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
注意,此处在子类重写的虚函数前可以不加virtual(因为继承后父类的虚函数在子类中依旧保留了虚函数属性),但是这个写法不规范,不建议这样写。
关于虚函数重写有两个例外:
1. 协变(父类和子类虚函数的返回值不同)。
正常情况下,重写需要三同。但是当返回值是父类关系且同时为指针或者同时为引用时,可以不同,就是协变。
class A{};
class B : public A {};
class Person {
public:
virtual A* f() {return new A;}
};
class Student : public Person {
public:
virtual B* f() {return new B;}
};
就算不是这个类的,只要满足了父子关系就行。
2.析构函数的重写。(父类与子类的析构函数名不相同)
析构函数也可以重写,但是它们的函数名确不同。其实,在这里编译器对于析构函数名做了特殊处理,统一将函数名改为destructor
class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
这个地方还是很重要的,这能帮助编译器调用到正确的析构函数。假设上述代码中,假设父类指针指向了子类对象构成了多态,但是在结束时析构的时候又没有多态,那么当子类动态开辟了空间时,它又没有调用到子类的析构函数,那么就会造成内存泄漏。
这两个关键字是帮助程序猿,防止在需要进行重写的地方忘记重写等原因导致编译期间不会出错,但是运行的时候又出幺蛾子的情况。所以C++11提供了这两个关键字,帮助用户检查是否重写。
1.final:修饰虚函数,表示该虚函数不能再被重写。
class Car
{
public:
virtual void Drive() final
{}
};
2.override:检查子类的虚函数是否重写了父类的某个虚函数,如果没有重写就会编译报错。
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
2.5 重载,重写(覆盖),隐藏(重定义)的对比
在虚函数后面加上一个=0,这个函数为纯虚函数。包含纯虚函数的类叫做抽象类,抽象类不能被实例化。子类继承后如果没有重写纯虚函数,那么它也不能实例化出对象。
纯虚函数规范了子类必须重写,另外虚函数更体现了接口继承。
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
普通函数的继承是一种实现继承,它继承的是父类的函数实现。虚函数的继承是一种接口继承,子类继承的是父类的函数接口,目的是为了重写,实现多态,如果不需要多态就不要定义成虚函数。
简称虚表。当一个类中有虚函数的时,那么它的对象中就会多一个虚函数表地址,它指向了一个虚函数表,里面放着该类虚函数的地址。
当子类继承了有虚函数的父类时,那么它的对象中也会有一个虚表指针,不同父类的是,子类的虚表是在父类的基础上进行拷贝后,把自己重写的虚函数在虚函数表中进行了覆盖。即重写是语法层面的叫法,覆盖是原理层的叫法。注意:只有虚函数才会被放入虚表。
虚函数表的本质是一个存了虚函数指针的指针数组,一般情况下,这个数组的最后一个会存nullptr。
注意:纯虚函数是没有虚函数指针的。
总结虚表的生成:先将父类的虚表内容拷贝一份到子类的虚函数表中,然后子类将自己重写的虚函数覆盖到虚表中。另外如果子类自己增加的虚函数会按声明顺序添加到虚表的最后。
最后关于一些易错的地方:虚函数表在vs下是存在代码区(常量区)的。对象中存的是虚表指针而不是虚表,一个类它只能生成一张虚表,它可以通过继承即多继承获得一张或多张虚表。当多继承时,子类有自己的虚函数时,它会把这个虚函数放到第一个父类的虚表的最后。注意:继承是对父类虚表的覆盖,比如B继承A,那么它里面的虚表就是B对A虚表的覆盖,多继承时也是如此,比如C继承A和B,那么它里面就俩张虚表,一张对A的覆盖,另一张对B的覆盖。
看代码示例
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
在上述代码中,当父类引用实例化父类对象时,在调用函数时,它就会去在父类对象中的虚表中去查找,如果实例化子类对象时,那么它就在子类对象中的虚表去查找,以此来达成多态。可以说,满足多态以后的函数的调用,它不是在编译阶段确定的,它是在运行阶段找到的。而不满足多态的函数的调用,它是在编译阶段就确定的。
静态绑定也叫前期绑定(早绑定),它是在程序编译阶段就确定好的,比如:函数重载。
动态绑定也叫晚期绑定(晚绑定),是在程序运行阶段,根据具体拿到的类型,确定程序具体的行为,调用不同函数。比如:多态。
这里比较简单,就如之前所说,先将父类的虚表进行拷贝给子类,然后子类再进行覆盖,另外就是子类中有自己的虚函数,那么会加在虚函数表的最后(按声明顺序)。
多继承的子类的为重写的虚函数会放在第一个继承的父类部分的虚表当中。另外就是后面的虚表其实也是通过编译器的操作用偏移量来实现多态的。
另外就是要尽量避免菱形继承和零星虚拟继承。
1.什么是多态?
答:完成某个行为时,当不同的对象去完成时会产生不同的状态。
2.什么是重载,重定义,重写?
答:
重载就是在一个类中定义多个函数名相同,但参数不同的函数。 它要求在同一作用域中,且函数名要相同,参数不相同。
重定义就是子类中有与父类相同名称的成员,可以是变量也可以是函数,子类会屏蔽对父类同名成员的直接访问。
重写就是子类有跟父类完全相同的虚函数。它要求返回值,函数名,参数相同。当然也会有协变和析构函数重写的特殊情况。
3.多态的实现原理?
答:在程序运行时期,在父类指针或引用调用重写的函数的时候,会通过子类对象中的虚表指针找到虚表,在虚表中找到被对应覆盖的函数进行调用。
4.inline函数可以是虚函数吗?
答:可以。但是编译器就会忽略inline属性,这个函数就不再是inline函数,因为它会被放到虚表中去。
5.静态函数可以是虚函数吗?
答:不可以。因为静态函数没有this指针,使用类型::成员函数的方式访问不到虚函数表,所以静态成员函数无法放入虚函数表。
6.构造函数可以是虚函数吗?
答:不可以。因为虚函数表指针是在初始化列表才初始化的。
7.析构函数可以是虚函数吗?在什么场景下使用?
答:可以。并且把最好父类的析构函数定义为虚函数。比如当子类对象动态开辟了空间,如果没有多态去调用子类对象的话,就会导致内存泄露。
8.对象访问是普通函数快还是虚函数快?
答: 首先如果是普通对象,那么是一样快的。如果是指针对象或者是引用对象构成了多态,那么是调用普通函数要快一些,因为构成多态,调用虚函数需要到虚函数表中去查找。
9.虚函数表是在哪个阶段生成的,存在哪?
答: 是在编辑阶段生成的,一般情况存在代码段(常量区)。
10.C++菱形继承的问题?虚继承的原理?
答: 菱形继承会导致数据冗余和二义性问题。在虚继承后,当子类继承多个父类时,父类的公有部分的成员只有一份且放在子类的底端,在父类的内部原本公有部分的成员通通化身为一个虚基表指针,它指向了一个虚基表,里面存的是与子类底端爷爷部分成员的与虚基表指针的偏移量,他会通过这个偏移量来找到爷爷的成员进行访问。
11.什么是抽象类?抽象类的作用?
答: 包含纯虚函数的类叫做抽象类。抽象类强制重写了虚函数,另外抽象类体现出接口继承关系。
补充:
1.一个类的多个对象共享该类的虚表。
2.有虚函数的父类和子类对象前四个字节都是虚表地址,且各不相同,指向各自的虚表。
3.子类自己的虚函数只会放到第一个父类的虚表后面,其它父类的虚表不需要储存,因为储存了也不能调用。
4.如果满足多态的子类的函数是私有的,它也能调用。因为它是通过虚表地址找到虚表里面的覆盖了的函数地址,通过地址可以调用。
---------------------------------------------------------------------------------------------------------------------------------
2023-8-30
答:编译器在编译时会对函数进行名称修饰,它是根据函数名和参数列表类型等信息组合成一个唯一的函数名,比如在linux系统下,有多个函数名相同构成重载的函数,有一个参数类型是int,那么在编译时它的函数名在经过修饰后末尾会加上一个i来表示它的参数类型是int,double的话就同理加上一个d,这样在调用的时候就可以根据传入的参数来匹配合适的函数。