一、什么是多态?
1、多态基本概念
多态就是去完成某个行为,不同的对象会产生出不同的状态。
例子:普通人去服装店买衣服是按标准价给钱的,而店内员工在该店买东西,是按照员工价付款的。
2、多态的定义
(1)在继承中构成多态的两个条件:
-必须通过基类指针或引用调用虚函数
-被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
class Ordinary
{
public:
virtue void BuyCloth()
{
cout<<"全价购买"
}
};
class sales:public Oridinary
{
virtual void BuyCloth()
{
cout<<"员工价购买";
}
};
void Func(Oridinary & people)
{
people.BuyCloth();
}
void test()
{
Oridinary a;
Func(a);
Sales b;
Func(b);
}
(2)什么是虚函数?
被virtual修饰的类成员函数称为虚函数
class Ordinary
{
public:
virtue void BuyCloth()
{
cout<<"全价购买"
}
};
(3)虚函数重写
-1、什么是重写?
派生类中有一个跟基类完全相同的虚函数(返回值类型、函数类型、参数列表完全相同),此时子类的虚函数重写了基类的虚函数。
-2、注:重写虚基类时,派生类虚函数可以不加virtual关键字也可构成重写,但是这种写法不规范,不建议使用。
- 3、虚函数重写的两个例外
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)析构函数的重写(基类与派生类析构函数名字不同)
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;
}
只有派生类的析构函数重写了基类的析构函数,才能保证delete时两个指针指向的对象正确调用析构函数
(4)c++11 overrid 和 final
-overrid
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写则报错
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
- final
修饰虚函数,表示该虚函数不能再被继承
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
(5)重载、重写、覆盖的区别
二、抽象类
1、抽象类概念
包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象。只有继承后重写纯虚函数才能实例化出对象。
- 纯虚函数:
虚函数后面写上=0,则称这个函数为纯虚函数。
class Car
{
public:
virtual void Drive() = 0;
};
2、接口继承和实现继承
(1)接口继承:
虚函数的继承是继承了一个接口,目的是为了重写,形成多态,叫做接口继承。
(2)实现继承:
普通函数的继承就是继承基类的函数实现,所以是实现继承。
三、多态原理
1、虚函数表(简称虚表)
举例:
#include<iostream>
using namespace std;
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
/*
每个类对象的成员变量的值都是不同的,但是都调用同一段和函数代码,
为了减少空间浪费所以将成员函数存储在公共代码段
*/
int main()
{
cout << sizeof(Base) << endl;//结果是8,多了一个指针
Base b;
system("pause");
return 0;
}
只有一个虚函数,所以这里vfptr只有一个。且虚函数的地址要被存放在虚函数表中,所以虚函数表是一个虚函数指针的指针数组。
- 总结:
(1)虚表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
(2)派生类虚表生成过程:
a)将基类中的虚表内容拷贝一份至派生类的虚表。
b)如果派生类重写了虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数。
c)派生类自己的虚函数,依次添加到虚函数表的最后。
(3)虚函数和普通函数一样存在代码段,虚表存的是指向虚函数的指针,vs下虚表存在代码段。
2、多态原理解析
同一段代码:
class Ordinary
{
public:
virtue void BuyCloth()
{
cout<<"全价购买"
}
};
class sales:public Oridinary
{
virtual void BuyCloth()
{
cout<<"员工价购买";
}
};
void Func(Oridinary & people)
{
people.BuyCloth();
}
void test()
{
Oridinary a;
Func(a);
Sales b;
Func(b);
}
解读:
1、 test()函数里面的a,传参至Func函数中,在其虚函数表里找到的是Ordinary类的虚函数,故调用Ordinary的虚函数Func。
2、而b传参至Func里面,b的虚函数表里村的是自己重写之后的Func函数,所以调用的是重写之后的Func。
3、**满足多态条件的函数调用,不是在编译时确定的,而是运行起来后在对象中找取的。**不满足多态的函数调用是在编译时确认好的。
2、动态与静态绑定
(1)静态绑定:
又称前期绑定,在程序编译期间确定了程序的行为,也称为静态多态。(比如函数重载)
(2)动态绑定:
又称后期绑定,在程序运行期间根据具体类型确定具体行为,调用具体函数,也叫动态多态。
四、单继承和多继承关系的虚函数表
1、单继承中的虚表
#include<iostream>
using namespace std;
class Base
{
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
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; }
private:
int b;
};
int main()
{
Base b;
Derive d;
system("pause");
return 0;
}
看不到d的虚函数func3()和func4();
利用特定函数对虚表进行打印可以发现d的虚表中为:
2、多继承中的虚表
class Base1 {
public:
virtual void func1() {cout << "Base1::func1" << endl;}
virtual void func2() {cout << "Base1::func2" << endl;}
private:
int b1;
};
class Base2 {
public:
virtual void func1() {cout << "Base2::func1" << endl;}
virtual void func2() {cout << "Base2::func2" << endl;}
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() {cout << "Derive::func1" << endl;}
virtual void func3() {cout << "Derive::func3" << endl;}
private:
int d1;
};
可以得到当一个类多继承的时候它有两个虚表,它一旦重写了两个父类都有的同名虚函数,那么其虚表里存的虚函数时自己进行重写之后的,并且多继承的派生类未重写的虚函数放在第一个虚表中。
问题:
1、inline函数可以是虚函数吗?
不能,因为inline函数没有地址,无法把地址放到虚函数表中
2、静态成员可以是虚函数吗?
不可以,因为静态成员没有this指针,直接用类调用,无法访问虚表,所以静态成员不是虚函数。
3、构造函数可以是虚函数吗?
不可以,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的,如果构造函数是虚函数,那么就得先调虚函数表,所以不可以。
4、析构函数最好定义未虚函数
5、对象访问虚函数和普通函数哪个更快?
如果是普通对象访问效率是一样的
如果是指针或引用访问,访问普通函数更快。
区别:主要在于指针或引用访问虚函数时需要在运行时通过查询虚函数表才能确定要调用的函数的真实地址,而其它形式的访问则在编译时就可以确定要调用函数的地址。
6、虚函数表是在什么时候生成的?
虚函数表是在编译阶段就生成的,一般存储在代码段的常量区;