目录
一.多态的概念
多态通俗来说就是多种形态,做一件事不同的人去完成会产生不同的状态
二.多态的定义和实现
1.多态构成的条件
多态是在不同继承关系的类对象,去调用同一函数而产生不同的行为
1.1多态构成条件
在继承中构成多态的条件:
1.必须通过基类的指针或者引用去调用虚函数
2.被调用的函数必须是虚函数,且派生类必须对基类虚函数进行重写
1.2虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数。
class Person {
public:
virtual void Buy() { cout << "原价" << endl;}
};
1.3虚函数重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的 返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
例如学生购买有的商家有教育优惠:
#include<iostream>
using namespace std;
class Person {
public:
virtual void Buy() { cout << "原价" << endl; }
};
class Student : public Person {
public:
virtual void Buy() { cout << "教育优惠" << endl; }
};
void Func(Person& p)
{
p.Buy();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
结果:
注意:虚函数重写时,基类为函数添加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.析构函数的重写(基类与派生类的析构函数名不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处 理,编译后析构函数的名称统一处理成destructor。
class Person {
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
1.4 C++11 override 和 final
C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
a.final:修饰虚函数表示该虚函数不能再被重写
class Person {
public:
virtual void Buy() final { cout << "原价" << endl; }
};
class Student : public Person {
public:
virtual void Buy() { cout << "教育优惠" << endl; }
};
b.override:用于检查派生类虚函数是否重写了基类的某个虚函数,没重写就编译错误
class Person {
public:
virtual void Buy() { cout << "原价" << endl; }
};
class Student : public Person {
public:
virtual void Buy() override { cout << "教育优惠" << endl; }
};
1.5 重载、覆盖(重写)、隐藏(重定义)的对比
三.抽象类
1.概念
在虚函数后面加上=0,这个函数就是纯虚函数,包括纯虚函数的类叫抽象类(接口类),抽象类不能实例化出对象。就算派生类继承了基类在没重写纯虚函数时也不能实例化出对象,只有派生类重写了纯虚函数才能实例化出对象。
例如:
class Person
{
public:
virtual void Buy() = 0;
};
class Student : public Person
{
public:
virtual void Buy() override
{
cout << "教育优惠" << endl;
}
};
class Teacher
{
public:
virtual void Buy()
{
cout << "教育优惠" << endl;
}
};
int main()
{
//Person p; 纯虚函数不能实例化出对象
Student st;
Teacher tt;
return 0;
}
2.接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实 现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成 多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
四.多态的原理
4.1原理
先提一个问题:
class A
{
public:
virtual void f()
{
cout<<"f()" << endl;
}
protected:
int _a;
};
int main()
{
A a;
cout << sizeof(a) << endl;
return 0;
}
上面这段代码中sizeof(a)大小是多少?
输出结果:
答案是8个字节(x86环境)。
我们通过调试可以看到A里面除了存_a还存了一个虚表
除了_a成员,还多一个__vfptr放在对象的前面(注意有些 平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代 表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,虚函数表也简称虚表。
继续看下面这个例子:
class A
{
public:
virtual void f()
{
cout<<"f()" << endl;
}
virtual void f1()
{
cout << "f1()" << endl;
}
void f2()
{
cout << "f2()" << endl;
}
private:
int _a = 1;
};
class B : public A
{
public:
virtual void f()
{
cout<< "B::f()" << endl;
}
private:
int _b = 2;
};
int main()
{
A a;
B b;
return 0;
}
1. 派生类对象b中也有一个虚表指针,db对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
2. 基类a对象和派生类b对象虚表是不一样的,这里我们发现f完成了重写,所以b的虚表中存的是重写的b::f,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3. 另外f1继承下来后是虚函数,所以放进了虚表,f2也继承下来了,但是不是虚函数,所以不会放进虚表。
4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
5. 总结一下派生类的虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中
b.如果派生 类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己 新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
用上面的代码指向的对象是a就在A的虚表里面找函数,指向b就在B的虚表里面找函数:
我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调 用虚函数。
所以为什么是这两个条件?
可以通过汇编来看看。
可以看到首先将引用a的前四个字节(x86环境下)给了eax
然后相当于把对象的前四个字节(虚表指针)移到edx里面
call eax里面的指针
总结:编译器并不是在编译的时候决定调用哪个虚函数的,而是在运行的时候,去取虚表指针指向的函数,最后call这个函数。这样构成多态的。
4.2动态绑定与静态绑定
1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态。