目录
一、什么是多态:
多态,通俗来说就是多种形态,分为编译时多态(静态多态)和运行时多态(动态多态)
编译时多态主要就是前面讲的函数多态和函数模版,通过参数不同达到多种形态,
将它叫做编译时多态,是因为他们的实参传给形参的过程中实现的
一般将编译时成为静态,将运行时成为动态
运行时多态,具体点就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,
就达到多种形态
class A
{
public:
virtual ~A()
{
cout << "~A" << endl;
}
virtual void print() final//final修饰的虚函数无法被重写
{
cout << ' ' << endl;
}
};
class B : public A
{
public:
构成虚函数重写
~B()//在编译器处理后都会被处理成的destructor
{
cout << "~B" << endl;
delete _p;
A::~A();
}
protected:
int* _p = new int[10];
};
二、多态的条件
class Person
{
1、必须基类的指针或者引用调用虚函数;2、被调用的函数必须是虚函数,
派生类必须对基类的虚函数重写或者覆盖
public:
派生类的可以去掉,但是基类的不可以去掉
virtual void BuyTicket()//用virtual修饰的成员函数叫做虚函数
{
cout << "买票-全价" << endl;
}
virtual A* BuyTicket()//协变
{
cout << " " << endl;
return nullptr;
}
};
class Student : public Person//多个子类之间也可以实现多态,一句代码实现不同的行为
{
public:
派生、覆盖
三个相同:返回值类型、参数列表、函数名(缺省值不一样也算重写)
但是内部实现不同
virtual void BuyTicket() override
{
cout << "买票-打折" << endl;//重写的是实现
}
派生类虚函数可以不加virtual,但是并不规范
};
void Func(Person* ptr)//基类的指针
{
通过Person指针调用BuyTicket,满足多态,指向谁,调用谁
不满足多态看传的指针的类型,是什么类型调用谁
ptr->BuyTicket();//调用虚函数,并且完成了覆盖或者重写
}
void Func(Person& ptr)//基类的引用
{
ptr.BuyTicket();
}
int main()
{
实现了与类型无关,只与指向有关,如果基类的析构函数不设计为虚构函数
这里就会因为不够成多态不看指向看类型导致析构B的时候调用的是A的析构函数
A* p1 = new A;
A* p2 = new B;
先调用析构函数,再调用operator delete
调用A的析构函数
delete p1;
调用B的析构函数,但他有两部分组成父类和子类自己的部分,派生类析构函数会在结束后自动调用
父类的析构函数,以保证先子后父的析构顺序,所以会有~A ~B ~A
delete p2;//构成多态,一个是父类指针——p1和p2的类型都是A,另一个是调用重写的虚函数
return 0;
}
三、理解虚函数的重写行为
判断以下程序结果:
输出结果是B->1
在重写的情况下,重写的是虚函数实现,可以理解成是用派生类的函数体加上基类的函数声明实现
绝不重新定义继承而来的缺省值原因如上
因此B中的缺省值无效
effective C++
class A
{
public:
virtual void func(int val = 1)
{
std::cout << "A->" << val << std::endl;
}
virtual void test()//这里的this指针是A*
{
func();//这里的func满足多态,一个是有基类的指针(this),另一个是调用重写过的虚函数
}
};
class B : public A
{
public:
void func(int val = 0)
{
std::cout << "B->" << val << std::endl;
}
};
int main()
{
B* p = new B;
p->test();//这里通过继承可以找到test函数
return 0;
}
int main()
{
B* p = new B;
p->func();这里不构成多态,因为是派生类的指针,只是一个普通调用
return 0;
}
四、虚函数重写的其他问题
协变:虚函数返回值可以不同,要求必须返回基类或者派生类的指针或者引用
析构函数的重写:基类的析构函数为虚函数,此时派生类只要定义就行, 就可以构成重写override 和 final 关键字:override可以检查是否构成重写(编译的时候检查不出来),而如果不想让虚函数被重写,就加上final
五、 纯虚函数和抽象类
在虚函数后面写上=0,即为纯虚函数,纯虚函数不需要定义实现(语法上也可以定义)
包含纯虚函数的类是抽象类,抽象类不能实例化出对象
如果派生类没有重写纯虚函数,那么派生类也是抽象类
class car//抽象类——不能实例化出对象
{
public:
virtual void Drive() = 0;//纯虚函数
};
class Benz : public car//纯虚函数间接在语法上强制派生了重写纯虚函数
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW : public car//纯虚函数间接在语法上强制派生了重写纯虚函数
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
int main()
{
car car;
Benz car_Benz;
BMW car_BMW;
car* pBenz = new Benz;
pBenz->Drive();
car* pBMW = new BMW;
pBMW->Drive();
return 0;
}
六、多态的底层原理
class base
{
public:
virtual void Func()//这里有一个虚函数表指针_vfptr,大小4字节——virtual function table
{
vfptr是一个函数指针数组,每个虚函数的地址都会被放进去
cout << "Func()" << endl;
}
virtual void Func2()
{
cout << "Func2()" << endl;
}
private:
int _b = 1;
char _ch = 'x';
};
class upper_base : public base
{
public:
指向谁调用谁,指向那个对象,就到指向函数的虚函数表中找到对应的虚函数地址,进行调用,
也就实现了运行时多态
void Func1()//vfptr中的函数指针指向upper_base的func
{
cout << "upper_base" << endl;
}
private:
int _a = 1;
};
int main()
{
base b;
upper_base u;
cout << sizeof(b) << endl;
cout << sizeof(u) << endl;
return 0;
}
七、动态绑定和静态绑定
静态绑定:函数调用是在编译时确定调用函数的地址
动态绑定:运行到指向对象的虚函数表中找到调用函数的地址
从效率来说静态绑定效率更高,因为不需要查找
八、虚函数表
·基类虚函数表中存放基类所有虚函数地址,同一种类型的基类对象的虚函数表是一样的,共用,
不同对象的虚表各自独立
·派生类分为基类和字节的成员,派生类的虚表是继承来的,但里面放的东西不一样,会将派生类中已经重写的虚函数
在继承来的虚表里的原地址改为新的虚函数的地址
·基类的虚函数表总包含基类的虚函数地址、派生类重写的虚函数地址,派生类自己的虚函数地址
(现将父类的虚函数表拷贝过来,再用重写的虚函数的地址去完成相应部分的覆盖,没有完成的部分不管
还要放进去派生类自己的虚函数地址)
·虚函数表存在没有标准答案,vs编译器将这个函数指针数组存在常量区
虚函数和普通函数也存在一起,只不过虚函数地址会被放在虚表