多态的概念
感性认识
不同对象去完成某个相同行为时会呈现出不同的状态。举个例子:普通人和残疾人去买车票,但残疾人会有优先通道和优惠。
代码认识
多态是在继承的基础上的,基类对象和派生类对象去执行同一段函数会导致不同的结果。
#include <iostream>
class Person
{
public:
virtual void buyTicket() { std::cout << "Person买票全价" << std::endl; }
};
class Student : public Person
{
public:
virtual void buyTicket() { std::cout << "Student买票九折" << std::endl; }
};
void Func(Person& base)
{
base.buyTicket();
}
int main()
{
Person p;
Student s;
Func(p);
Func(s);
return 0;
}
多态的定义
构成多态需要两个条件:
1. 被调用的函数必须是虚函数,并且派生类必须对基类的虚函数进行重写;
1. 必须通过基类的指针或者引用调用虚函数。
虚函数
虚函数:被virtual
修饰的类成员函数。
上面代码中的Person::buyTicket()函数就是一个虚函数。
重写:派生类中跟基类相同的虚函数(返回值相同、函数名相同、参数列表相同)。
上面代码中的Student::buyTicket()函数就是基类虚函数的一个重写。
虚函数重写的两个例外:
-
协变
派生类重写基类虚函数时,返回值类型不同,但必须是一对父子类型(基类虚函数返回基类指针/引用,派生类虚函数返回派生类指针/引用)。
class A{}; class B : public A{}; class Person{ public: virtual Person& func1() { return *this; } virtual A* func2() { return this; } }; class Student : public Person{ public: virtual Student& func1() { return *this; } virtual B* func2() { return this; } };
两个func1和两个func2都分别构成协变。
-
析构函数的重写
当一个类被继承时,析构函数写成虚函数比较好,因为可能会出现如下错误:
#include <iostream> class Person{ public: ~Person() { std::cout << "~Person()" << std::endl; } }; class Student : public Person{ public: ~Student() { std::cout << "~Student()" << std::endl; } }; int main() { Person *p1 = new Person; Person *p2 = new Student; delete p1; delete p2; return 0; }
此时发生了内存泄漏,因为没有进行Student的析构函数。要解决这个问题,就给析构函数用virtual
修饰变成虚函数。
#include <iostream>
class Person{
public:
virtual ~Person() { std::cout << "~Person()" << std::endl; }
};
class Student : public Person{
public:
virtual ~Student() { std::cout << "~Student()" << std::endl; }
};
int main()
{
Person *p1 = new Person;
Person *p2 = new Student;
delete p1;
delete p2;
return 0;
}
Tips:派生类重写虚函数时,可以不添加关键字virtual
,所以此时Student的析构函数可直接写成~Student(),但不建议其它函数省略。
抽象类
在虚函数的后面写上=0,则该函数称为纯虚函数,包含纯虚函数的类称为抽象类。抽象类无法实例化,抽象类的派生类也无法实例化,只有重写了基类的虚函数才能够实例化。
#include <iostream>
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
void Drive()
{
std::cout << "Benz-舒适" << std::endl;
}
};
int main()
{
Benz b;
b.Drive();
return 0;
}
实现继承和接口继承
实现继承:普通函数的继承(派生类继承了基类普通函数的实现)
接口继承:虚函数的继承(派生类继承了基类虚函数的接口,目的是为了重写基类虚函数的实现)
所以如果不实现多态,函数一般不被修饰为虚函数。
超坑题目
以下程序输出结果是什么()?
#include<iostream> class A{ public: virtual void func(int val = 1){ std::cout << "A->" << val << std::endl; } virtual void test(){ func();} }; class B : public A{ public: void func(int val = 0){ std::cout<< "B->" << val << std::endl; } }; int main() { B *p = new B; p->test(); return 0; }
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
这里考察两个知识点:多态的两个条件和接口继承
解析:基类和派生类的func构成虚函数(缺省值不影响),p调用test函数,test函数通过隐藏的A *this指针调用func函数,满足基类指针调用虚函数,这满足了多态的两个条件,而由于this指向的对象是B,所以会调用B的虚函数的重写。而由于虚函数的继承是接口继承,所以其实调用的是基类的func函数接口和派生类的func函数实现,所以最终结果是B->1。
多态实现的原理
#include <iostream>
class Base{
public:
virtual void Func(){}
};
int main()
{
std::cout << sizeof Base() << std::endl;
return 0;
}
该类的大小是8bytes而不是1bytes,说明该类里面存在一个指针(64位指针大小为8bytes)或者一个函数。
#include <iostream>
class Base{
public:
virtual void Func1(){}
virtual void Func2(){}
virtual void Func3(){}
virtual void Func4(){}
};
int main()
{
std::cout << sizeof Base() << std::endl;
return 0;
}
该类的大小仍然是8个bytes,说明该类里面存的是一个指针而不是函数。
实际上,有虚函数的类中会保存一个指针,该指针称为虚函数表指针,指向一个虚函数表,这个虚函数表是一个函数指针数组,保存着该类的所有的虚函数的地址。
上图中的__vfptr就是虚函数表指针,表里的内容就是所有的虚函数的地址。
派生类中基类成员也仍然保留着虚函数指针,该指针指向的虚函数表和基类的一样,假若派生类中重写了基类虚函数,则派生类虚函数表中的该虚函数更新为重写的虚函数。
这就导致,基类有一张虚函数表,派生类有一张虚函数表,当访问虚函数时,若基类指针指向基类对象则去基类的虚函数表中找虚函数的地址,若基类指针指向派生类对象则去派生类的虚函数表中找虚函数的地址。
上图中有两个类,就有两个虚函数指针,而派生类重写了两个函数,所以派生类的虚函数表中有两个函数的地址跟基类的虚函数表不一致。
所以多态实现的原理就是虚函数、虚函数指针和虚函数指针表
虚函数跟普通成员函数一样都在代码段,只是虚函数在一个代码段中的函数指针数组中。
静态绑定与动态绑定
静态绑定:在程序编译期间就确定了程序的行为,也称为静态多态;
动态绑定:在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
通过汇编就可以看出:普通函数的调用其实就是一个静态绑定,而虚函数的调用就是动态绑定。
func1直到第四行汇编才根据rax里的内容call函数也就说明了虚函数调用是动态绑定;
func2第二行汇编就直接call函数了,这就是在编译期间就确定了函数的地址,也就是静态绑定。