1.多态的概念
1.1概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
举个例子吧,在购买火车票的时候,学生和普通人购买同一张票的票价可能不一样,学生是可以享受学生优惠的,这就是多态的例子,购买同一张票却是不同的价格,包括景区购票、少数民族高考加分等等,都是多态的实例。
2.多态的定义和实现
2.1多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。
将购买火车票看成一个函数,学生和普通人看成两个不同的对象,调用同一个函数,会有两种结果,那么这个函数绝非普通函数,绝对有要满足的条件才能达到此种效果,需要满足什么条件呢?
2.1.1. 必须通过基类的指针或者引用调用虚函数
class person
{
public:
void func() { cout << "func()"; }
virtual void buyticket()
{
cout << "买票,全价" << endl;
}
private:
int age;
string name;
char gender;
};
class stu:public person
{
private:
long num;
public:
long getnum()
{
return num;
}
virtual void buyticket()
{
cout << "买票,半价" << endl;
}
};
void func(person* a)
{
a->buyticket();
}
int main()
{
person* p = new person;
stu* s = new stu;
func(p);
func(s);
return 0;
}
左图便是代码的运行结果,在以上代码中,我们可以看到,buyticket函数都是有Person的对象调用的,只有通过基类调用才能实现多态。
2.1.2 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
首先,基类和子类是要用虚函数的,虚函数用virtual修饰即可实现。
其次,派生类还要对基类的虚函数进行重写,在继承中,我们了解了重写的概念:当派生类中包含一个与基类虚函数同名、同参数列表、同返回类型的成员函数时,我们说派生类重写了基类的虚函数。
如果没有虚函数:
由此可见,虚函数和重写是必须满足的。
有一点事值得一提的,在虚函数使用virtual修饰的时候,子类中被调用的函数前可以不写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。
2.2 C++11 override 和 final
override
检查派生类是否重写了某个基类的虚函数,如果没有的话,就会使编译器报错。
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;}
};
三个概念的对比:
3. 抽象类
概念
在虚函数的后面写上 =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;
}
};
void Test()
{
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
}
4.多态的原理
class animal
{
public:
virtual void speak()
{
cout << "animal language" << endl;
}
};
class cat:public animal
{
public:
cat(const string& text)
:name(text)
{}
virtual void speak()
{
cout << "Miao Miao" << endl;
}
private:
string name;
};
void func(animal& ani)
{
ani.speak();
}
int main()
{
cat a("Blue");
cout<<sizeof("a")<<endl;
return 0;
}
思考一下这段代码的结果是多少?
不假思索的话,我们一定会认为是40,因为cat里面只含有一个string类型的成员变量,但是我们打开监视窗口看一下就会发现并非如此。
我们发现在继承的animal中还包含了一个_vfptr的成员,那么vfptr是什么呢?其实它叫做虚函数表,里面存放的是虚函数的地址。
4.1虚函数表
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;
};
打开监视窗口我们看见了,d的虚函数表中只有继承来的Base中的fuc1和fuc2,没有自己的fuc3和fuc4,为什么呢?因为被编译器隐藏了,这里可以认为是一个小bug。
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
// 依次取虚表中的虚函数指针打印并调用。调用可以看出存的是哪个函数
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Base b;
Derive d;
VFPTR* vTableb = (VFPTR*)(*(int*)&b);
PrintVTable(vTableb);
VFPTR* vTabled = (VFPTR*)(*(int*)&d);
PrintVTable(vTabled);
return 0;
}
可以利用函数指针打印出许表中的函数地址。
4.2多态的实现原理
多态是怎么实现的?
原理大概如图,子类会从父类中继承一个vfptr,起初虚函数表中的函数也是父类的函数,但是,如果对象是子类的话,函数就会发生重写,声明是父类函数的声明,但是调用的内容就会按照子类的内容来走。