C++(初阶)(十四)——多态
多态
面向对象的其中一大特征。
- 多态
- 多态的定义及构成
- 多态的构成条件
- 多态的实现条件
- 多态的分类
- 编译时多态性
- 运行时的多态性
- 虚函数
- 定义
- 不能成为虚函数的函数
- 虚函数重写(覆盖)
- 选择题
- 虚函数重写的其他问题
- 析构函数的重写
- override 和final关键字
- 重载/重写(覆盖)/重定义(隐藏)的对比
- 纯虚函数和抽象类
- 虚函数表
多态的定义及构成
多态的构成条件
多态是⼀个继承关系的下的类对象,去调⽤同⼀函数,产⽣了不同的⾏为。⽐如Student继承了 Person。Person对象买票全价,Student对象优惠买票。
多态的实现条件
1,必须是基类的指针或者引用用调用虚函数。
2,被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖。
说明:要实现多态效果,第⼀必须是基类的指针或引⽤,因为只有基类的指针或引⽤才能既指向基类 对象⼜指向派⽣类对象;第⼆派⽣类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派⽣类之间才能有不同的函数,多态的不同形态效果才能达到。
多态的分类
在C++中,多态性是指对象在不同情况下表现出不同行为的能力。多态性分为两类:编译时的多态性(静态多态性)和运行时的多态性(动态多态性)
编译时多态性
编译时多态性是通过函数重载和模板来实现的。编译时多态性也称为静态多态性或早绑定,因为它在编译时就决定了函数的调用
运行时的多态性
1,当基类指针或引用指向派生类对象时,编译器会根据指针或引用的静态类型确定函数调用。
2,然后,在运行时根据对象的实际类型通过虚函数表找到正确的函数进行调用,实现了动态绑定,从而实现了运行时多态性。
虚函数
定义
类成员函数前⾯加virtual修饰,那么这个成员函数被称为虚函数。
注意⾮成员函数不能加virtual修饰。
以下就是一个虚函数:
class Person
{
public:virtual void BuyTicket(){cout << "买票->全价" << endl;}
};
要成为虚函数必须满足两点,一就是这个函数依赖于对象调用,因为虚函数就是依赖于对象调用,因为虚函数是存在于虚函数表中,有一个虚函数指针指向这个虚表,所以要调用虚函数,必须通过虚函数指针,而虚函数指针是存在于对象中的。二就是这个函数必须可以取地址,因为我们的虚函数表中存放的是虚函数函数入口地址,如果函数不能寻址,就不能成为虚函数。
不能成为虚函数的函数
1,内联函数:函数调用时自动展开,不能往虚表中存放,自然就不能成为虚函数。
2,静态函数:定义为静态函数的函数,这个函数只和类有关系,它不完全依赖于对象调用,所以也不能成为虚函数。
3,构造函数:构造函数的对象在调用时才会生成,如果写为虚函数,对象无法产生,自然也无法调用。
4,友元函数:友元函数不能为虚函数,因为友元函数虽然出现在类中,但是不是类成员。
因为只有类成员才能是虚函数。
5,普通函数:普通函数不属于成员函数,是不能被继承的。普通函数只能被重载,不能被重写,因此声明为虚函数没有意义。
虚函数重写(覆盖)
派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派⽣类的虚函数重写了基类的虚函数。
注意:在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承 后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样 使⽤,不过在考试选择题中,经常会故意买这个坑,让你判断是否构成多态。
class Person
{
public:virtual void BuyTicket(){cout << "买票->全价" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket(){cout << "买票->半价" << endl;}
};void Func(Person* ptr)
{//这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket//但是跟ptr没关系,⽽是由ptr指向的对象决定的。ptr->BuyTicket();
}int main()
{Person ps;Student st;Func(&ps);Func(&st);return 0;
}
选择题
以下程序输出结果是什么() A: A->0 B:B->1 C:A->1 D:B->0 E:编译出错 F:以上都不正确
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(int argc, char* argv[])
{B* p = new B;p->test();return 0;
}
虚函数重写的其他问题
协变
析构函数的重写
基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析 构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析 构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了 vialtual修饰,派⽣类的析构函数就构成重写。
override 和final关键字
C++对虚函数重写的要求⽐较严格,但是有些情况下由于疏忽,⽐如函数名写错参数 写错等导致⽆法构成重写,⽽这种错误在编译期间是不会报出的。
C++11提供了override,可以帮助⽤⼾检测是否重写。
如果我们不想让派⽣类重写这个虚函数,那么可以⽤final去修饰。
重载/重写(覆盖)/重定义(隐藏)的对比
重载:函数名相同,函数的参数类型或者个数不同。对函数返回值不做要求。发生在一个类内部,不能跨作用域。
重写(覆盖):派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派⽣类的虚函数重写了基类的虚函数。
重定义(隐藏):子类重新定义父类中有相同名称的非虚函数 ( 参数列表可以不同 ) ,指派生类的函数屏蔽了与其同名的基类函数。可以理解成发生在继承中的重载。
纯虚函数和抽象类
在虚函数的后⾯写上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被 派⽣类重写,但是语法上可以实现),只要声明即可。
包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象。
如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了 派⽣类重写虚函数,因为不重写实例化不出对象。
虚函数表
基类和派生类各自有独立的虚表,派生类之间各自有独立的虚表。