【C++】——多态(上)
1 多态的概念2 多态的定义及实现2.1 多态的构成条件2.1.1 实现多态的必要条件2.1.2 虚函数2.1.3 感受多态2.1.4 判断是否满足多态2.1.5 多态场景的一道选择题2.1.6 虚函数重写的一些其他问题2.1.6.1 协变2.1.6.2 析构函数的重写 2.1.7 override 和 final 关键字2.1.8 重载/重写/隐藏的对比重载 3 纯虚函数和抽象类
1 多态的概念
多态的概念:通俗来说,多态就是多种形态。多态分为静态多态(编译时的多态)
和动态多态(运行时多态)
。本章我们重点介绍运行时多态。
编译时多态(静态多态) 主要就是我们前面学习的函数重载
和函数模板
,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态。之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的
,我们把编译时一般归为静态,运行时归为动态
比如:
int i;double d;cout << i;cout << d;
上面,好像我们调用 cout
好像是同一个函数,但实际上真的是同一个函数吗?
并不是,这里运用了函数重载。在编译时,编译器通过一些规则去进行对应的匹配,调用对应的流插入,是 i n t int int 就调用 i n t int int 的流插入; d o u b l e double double 就调用 d o u b l e double double 的流插入。这里对一个函数名实现出多重形态,实际上底层还是去调用不同的函数
。
函数模板也是类似的道理。
我们今天主要学习运行时多态(动态多态):当我们具体完成某个行为时,不同的对象去做这个行为时,结果是不一样的
。
比如:
现实中的买票行为。普通人买票时,买的是全票;学生买票时,买的是优惠票;军人买票时,可以优先买票。
再比如:
同样是动物叫的一个行为(函数),传猫过去是喵喵喵,传狗过去是汪汪汪。
2 多态的定义及实现
2.1 多态的构成条件
多态必须是在继承的条件下面,去调用同一个函数(返回值,函数名,参数都相同),产生了不同的行为
2.1.1 实现多态的必要条件
实现多态必须要满足以下两个条件:
必须是指针或引用调用虚函数被调用的函数必须是虚函数,派⽣类必须完成对基类的虚函数重写/覆盖说明:要实现多态的效果:第一必须是基类的指针或引用,因为只有基类的指针或引用才能既指向基类又指向派生类对象;第二派生类必须对基类的虚函数进行重写/覆盖,如此派生类才能有不同的函数,多态的不同形态效果才能达到。
多态的现本质就是调用虚函数
那什么又是虚函数呢?我们一起来看看
2.1.2 虚函数
类成员函数前面加 v i r t u a l virtual virtual(返回值前面),那么这个成员函数被称为 虚函数。
注:非成员函数不能加 virtual 修饰
注:在继承
章节,我们曾讲过虚继承也是用 v i r t u a l virtual virtual 关键字,但这里的 v i r t u a l virtual virtual 和虚继承中的 v i r t u a l virtual virtual 功能上没有任何关系
。这里属于一鱼两吃。
那么说明中虚函数的重写
和覆盖
又是什么意思呢?我们一起来看看
虚函数的重写/覆盖:
子类中有一个跟父类完全相同的虚函数(即子类虚函数与父类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了父类的虚函数。
我们再来理清楚各个条件之间的关系:
多态有两个条件:必须是指针或引用调用虚函数;调用虚函数必须完成重写/覆盖。
而虚函数重新/覆盖又要满足:必须是基类和派生类中的两个虚函数,两个虚函数必须完全相同(只有函数体可以不同)
2.1.3 感受多态
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;}
为什么void Func(Person* ptr)
的参数要基类的指针或引用呢?在继承章节,我们曾讲过切片,只有参数是基类,才能既可以传父类又可以传子类对象(详情请看:【C++】—— 继承(上))
这时就达到了一个效果:指向谁调用谁。传递 p s ps ps,指向的是基类,就调用基类的虚函数;传递 s t st st,指向派生类,调用派生类的虚函数。传递不同的对象,调用不同的函数。
运行结果:
如果没有学习多态,实际调用的类型是根据表达式的类型来确定的。 f u n c func func 函数参数类型是 P e r s o n Person Person*,因此不论传递父类还是子类指针,传递给形参 p t r ptr ptr 都会强转成 P e r s o n Person Person*,最终调用的都是父类的函数。
现在有了多态,实际调用的函数实在运行时根据对象的实际类型来确定的。指向谁调用谁,指向父类调父类,指向子类调子类我们传递不同的对象,实现调用不同的函数。
当然,引用也是可以的
class Animal{public:virtual void talk() const{}};class Dog : public Animal{public:virtual void talk() const{std::cout << "汪汪" << std::endl;}};class Cat : public Animal{public:virtual void talk() const{std::cout << "(>^ω^<)喵" << std::endl;}};void letsHear(const Animal& animal){animal.talk();}int main(){Cat cat;Dog dog;letsHear(cat);letsHear(dog);return 0;}
运行结果:
2.1.4 判断是否满足多态
class Person {public:void BuyTicket() { cout << "买票-全价" << endl; }};class Student : public Person {public:virtual void BuyTicket() { cout << "买票-打折" << endl; }};
不满足第二个条件,基类BuyTicket()
不是虚函数
运行结果:
class Person {public: virtual void BuyTicket() { cout << "买票-全价" << endl; }};class Student : public Person {public: virtual void BuyTicket() { cout << "买票-打折" << endl; }};void Func(Person ptr){ ptr->BuyTicket()}
不满足第一个条件,不是基类的指针或应用调用
运行结果:
不满足多态编译器看调用的参数类型void Func(Person ptr)
,类型父类是父类,类型子类是子类
满足多态则看起指向的对象
这种情况下满足多态吗
class Animal{public:virtual void talk() const{}};class Dog : public Animal{public:virtual void talk() const{std::cout << "汪汪" << std::endl;}};class Cat : public Animal{public:virtual void talk() const{std::cout << "(>^ω^<)喵" << std::endl;}};int main(){Cat* pcat = new Cat;Dog* pdog = new Dog;pcat->talk();pdog->talk();return 0;}
不满足,因为多态要求是基类的指针/引用来调用
,这里 p c a t pcat pcat 和 p d o g pdog pdog 都是派生类的指针来调用。但基类和派生类是相对的,如果有 A A A 类 B B B 类继承了 p c a t pcat pcat 和 p d o g pdog pdog,那么他们同时还是基类,这样调用就构成多态了。
那可不可以是子类的指针或引用呢
class Person {public:virtual void BuyTicket() { cout << "买票-全价" << endl; }};class Student : public Person {public:virtual void BuyTicket() { cout << "买票-打折" << endl; }};void Func(Student* ptr){ptr->BuyTicket();}
也是不行的,必须是父类的指针或调用
。因为如果是子类的,那就不能传递父类的对象,也就没有多态的概念了
那这样满足多态的条件吗
class Person {public: virtual void BuyTicket() { cout << "买票-全价" << endl; }};class Student : public Person {public: void BuyTicket() { cout << "买票-打折" << endl; }};void Func(Person* ptr){// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。ptr->BuyTicket();}
是满足的
注意:在重写基类虚函数时,派生类的虚函数在不加 v i r t u a l virtual virtual 关键字时,虽然也可以构成重写
(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该钟写法不是很规范,不建议这样使用
,不过在考试选择题中,经常会故意挖这个坑,让你判断是否构成多态。
2.1.5 多态场景的一道选择题
以下程序输出结果是什么()A : A A: A A:A -> 0 B : B B: B B:B -> 1 C : A C: A C:A -> 1 D : B D: B D:B ->0 E E E: 编译出错 F F 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;}
首先 f u n c func func函数是虚函数
,并完成了重写
接着 n e w new new 了一个子类对象 B B B,并通过 p p p 来指向 B B B
再接着, p p p 调用 子类 B B B 从父类 A A A 继承过来的 t e s t ( ) test() test() 函数,这是个普通调用
t e s t ( ) test() test() 函数中去调用 f u n c ( ) func() func() 函数。这里 t e s t ( ) test() test() 中的 this指针
是 A A A* 还是 B B B* 呢;如果是 A A A*, f u n c func func函数是通过父类的指针调用,满足多态的条件,如果是 B B B* 不满足多态的条件
这里 t e s t test test函数中的 t h i s this this是 A A A*,为什么呢? B B B 不是继承了 A A A 吗,现在是 B B B 调用 t e s t test test,不应该是 B B B*吗?其实继承是一个形象的说法,继承的意思是我可以用,不会将函数中的参数给改了。编译器不会真的将 A A A 中的成员变量和不是重写成员函数都拷贝一份到 B B B 中。
因此,是满足多态的条件
的,调用 B B B 类中的 f u n c func func 函数
那答案是 B->0
了吗?还没完
虚函数重写,只重写函数体的实现,不重写声明! 也就是说继承而来的缺省值是不会重写的,可以认为重写后的虚函数 = 基类的函数声明 + 派生类的函数体
。也就是说 B B B 类中的虚函数本质是:virtual void func(int val = 0) { std::cout << "A->" << val << std::endl; }
所以正确答案是 B->1
那如果是这样调用,答案又选什么呢
int main(int argc, char* argv[]){B* p = new B;p->func();return 0;}
这里是子类 B B B 去调用,不满足多态的条件,因此 B B B 中的 f u n c func func函数 也不用去组合。这里是普通调用,直接调用 B B B 类原本的 f u n c func func 函数。答案: B->0
2.1.6 虚函数重写的一些其他问题
2.1.6.1 协变
上述说到完成虚函数的重新必须满足三同(返回参数、函数名、参数类型),但是也有例外,那就是协变
的情况。
协变的概念
派生类重写基类虚函数时,可以与基类虚函数返回值类型不同。但要求基类虚函数返回基类对象的指针或引用
,派生类虚函数返回派生类对象的指针或引用
,称为协变。 注:返回的基类和派生类可以不是自己本身
,但他们必须是一对基类和派生类
协变的实际意义并不大,我们了解一下即可
class A {};class B : public A {};class Person {public:virtual A* BuyTicket(){cout << "买票-全价" << endl;return nullptr;}};class Student : public Person {public:virtual B* BuyTicket(){cout << "买票-打折" << endl;return nullptr;}};void Func(Person* ptr){ptr->BuyTicket();}int main(){Person ps;Student st;Func(&ps);Func(&st);return 0;}
运行结果:
2.1.6.2 析构函数的重写
当基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加 v i r t u a l virtual virtual 关键字,都与基类的析构函数构成重写。
虽然基类与派生类析构函数名字看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成 d e s t r u c t o r ( ) destructor() destructor(),所以基类的析构函数加了 v i r t u a l virtual virtual 修饰,派生类的析构函数就构成重写
class A{public :virtual ~A(){cout << "~A()" << endl;}};class B : public A {public:~B(){cout << "~B()->delete:" << _p << endl;delete _p;}protected:int* _p = new int[10];};
传统的意义上,哪怕 ~B() 加上 v i r t u a l virtual virtual 也不会构成重写(不符合三同中的函数名相同),但经过编译器将析构函数函数名处理成 d e s t r u c t o r ( ) destructor() destructor() 后,再加上 v i r t u a l virtual virtual 就构成了重写了。
但 C++ 为什么设计成这样呢?
我们实践中会遇到这样的问题
int main(){A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0}
现在我有一个父类对象的指针,他可以指向父类对象也可以指向子类对象。当我们进行 d e l e t e delete delete 时,delete p1;
没问题,但delete p2;
是调用父类对象的析构还是子类对象的析构呢?
我们期望的是delete p2;
调用的是子类的析构函数,但是正常来说(不构成多态情况下)delete p2;
是调用父类的析构函数,因为 p 2 p2 p2 是 A A A* 类型。
这种情况只有构成多态才能解决问题。构成多态,指针调用时不是跟 p1 和 p2 的类型有关,而是跟他们指向的对象有关,指向父类调父类;指向子类调子类。
编译器将析构函数统一处理为 d e s t r u c t destruct destruct,就是为了能够实现析构函数的重写,这样只要在父类析构函数前加 v i r t u a l virtual virtual 就构成多态。只有这样才能正确释放资源
运行结果:
为什么最后会多调用一次父类的?
这一点我们在【C++】—— 继承(上)中提到过:这是为了保证析构的顺序是先子后父
不构成多态的情况:
class A{public : ~A(){cout << "~A()" << endl;}};class B : public A {public:~B(){cout << "~B()->delete:" << _p << endl;delete _p;}protected:int* _p = new int[10];};int main(){A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;}
运行结果:
p 1 p1 p1 和 p 2 p2 p2 都是调用父类的析构函数
, p 2 p2 p2 无法调到子类的析构函数
2.1.7 override 和 final 关键字
从上面可以看出,C++ 对函数重写的要求比较严格
,但是有些情况下由于疏忽,比如函数名写错参数写错等导致无法构成重写,而这种错误在编译期间时不会报出的,只有在程序运行时没有得到预期结果才来 d e b u g debug debug 会得不偿失,因此 C++11 提供了 o v e r r i d e override override关键字,可以帮助用户检测是否重写,如果没有完成重写,在编译时会报错
。
class Car {public:virtual void Dirve() {}};class Benz :public Car {public:virtual void Drive() override{ cout << "Benz-舒适" << endl; }};int main(){return 0;}
函数名不同( D i r v e Dirve Dirve 和 D r i v e Drive Drive),没有构成重写,编译报错。不加 o v e r r i d e override override,编译时是检查不出来的。
如果我们不想让派生类重写这个虚函数,那么可以用 f i n a l final final 去修饰
在继承章节中我们曾提过:如果一个类我们不想让他被继承,我们可以用 f i n a l final final 去修饰他;被 final 修饰了他就是最终类。
多态这里是不能被重写:如果一个虚函数不想被重写 就可以用 f i n a l final final 去修饰
class Car {public:virtual void Dirve() final{}};class Benz :public Car {public:virtual void Dirve() { cout << "Benz-舒适" << endl; }};int main(){return 0;}
同时,既然析构函数函数名都被编译器处理成 d e s t r u c t o r destructor destructor,那也意味着他们构成隐藏,如果想要显式调用父类的析构函数需指定类域。详情请看:【C++】—— 继承(上)
2.1.8 重载/重写/隐藏的对比重载
重载
两个函数在同一作用域函数名相同,但参数类型或个数不同,返回类型可同、可不同重写/覆盖
两个函数分别在继承体系的父类和子类不同作用域函数名、参数、返回值都必须相同
。协变除外两个函数都必须是虚函数 隐藏
两个函数分别在继承体系的父类和子类不同作用域函数名相同两函数只要不构成重写就是隐藏父子类的成员变量相同也叫隐藏
重载/重写/隐藏都有一个共同点:函数名相同
3 纯虚函数和抽象类
在虚函数的后面写上 =0,则这个函数为纯虚函数,纯虚函数不需要定义实现
(实现没啥意义,因为要被派生类重写,但是语法上可以实现),只要声明即可。
包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类
。
纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。
注:纯虚函数不需要定义实现,但是语法上可以实现的
,选择题爱考
什么时候我们可以定义抽象类呢。比如现实世界中定义了某个类,这个类不够具体或现实中不存在这个类,也不需要将这个类实例化出对象,这时就可以用抽象类。
很多时候,我们将基类定义为抽象类。比如动物的叫声:我们将基类 a n i m a l animal animal 定义成抽象类,因为动物是一个统称,不存在具体的叫声;我们可以在子类 C a t Cat Cat 和 D o g Dog Dog 中进行重写, C a t Cat Cat 和 D o g Dog Dog 是一具体的类,有其叫声,可以实例化出来
class Animal{public:virtual void talk() const = 0;};class Cat : public Animal{public:virtual void talk() const{std::cout << "(>^ω^<)喵" << std::endl;}};class Dog : public Animal{public:virtual void talk() const{std::cout << "汪汪" << std::endl;}};
int main(){Animal* pCat = new Cat;Animal* pDog = new Dog;pCat->talk();pDog->talk();return 0;}
运行结果:
虽然父类 A n i m a l Animal Animal 不能实例化出对象,但是父类的指针或引用是可以定义的,依然可以实现多态。
int main(){Animal animal;return 0;}
class Car{public :virtual void Drive() = 0;};class Benz :public Car{};
问: B e n z Benz Benz类是抽象类吗?
是的, B e n z Benz Benz 继承了 C a r Car Car,但是并没有重写虚函数Drive()
。虽然 B e n z Benz Benz 不直接包含纯虚函数,但它继承了 C a r Car Car,将其纯虚函数继承了下来,因此 B e n z Benz Benz 也有纯虚函数
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;}};
所以纯虚函数某种程度上强制了子类必须重写虚函数
好啦,本期关于 多态 的知识就介绍到这里啦,希望本期博客能对你有所帮助。同时,如果有错误的地方请多多指正,让我们在 C++ 的学习路上一起进步!