【导读】《21天学通C++》这本书通过大量精小短悍的程序详细而全面的阐述了C++的基本概念和技术,包括管理输入/输出、循环和数组、面向对象编程、模板、使用标准模板库以及创建C++应用程序等。这些内容被组织成结构合理、联系紧密的章节,每章都可在1小时内阅读完毕,都提供了示例程序清单,并辅以示例输出和代码分析,以阐述该章介绍的主题。本文是系列笔记的第五篇,欢迎各位阅读指正!
多态
多态(Polymorphism)是面向对象语言的一种特征,让你能够以类似的方式处理不同类型的对象。
使用虚函数实现多态行为
可通过Fish指针或Fish引用访问Fish对象,这种指针或引用可指向Fish、Carp等对象。但你不需要知道也不关心它们指向的是哪种对象。可以用下面代码所示:
pFish->Swim(); myFish.Swim();
你希望通过这种指针或引用调用Swim()时,如果它们指向的是Tuna对象,则可像Tuna那样游泳,若指向的是Carp对象,则可以像Carp那样游泳,若指向的是Fish,则可像Fish那样游泳。为此,可在基类Fish中将Swim声明为虚函数;
classBase{ virtualReturnTypeFunctionName (parameterList); }; classDerived{ FunctionName (parameterList); };
通过使用关键字virtual,可确保编译器调用覆盖版本。也就是说,如果Swim()被声明为虚函数,则将参数myFish(其类型为Fish&)设置为一个Tuna对象时,myFish.Swim()将执行Tuna::Swim(),程序如下所示:
usingnamespacestd; classFish{ public: virtualvoidSwim() { cout<<"Fish swims!"<<endl; } }; classTuna :publicFish{ public: voidSwim() { cout<<"Tuna swims!"<<endl; } }; classCarp :publicFish{ public: voidSwim() { cout<<"Carp swims!"<<endl; } }; voidMakeFishSwim(Fish&InputFish) { InputFish.Swim(); } intmain() { TunamyDinner; CarpmyLunch; MakeFishSwim(myDinner); MakeFishSwim(myLunch); return0; }
输出为:
Tuna swims!
Carp swims!
首先,根本没有调用Fish::Swim() ,因为存在覆盖版本 Tuna::Swim()和 Carp::Swim() ,它们优先于被声明为虚函数的Fish::Swim()。这很重要,它意味着在MakeFishSwim()中,可通过Fish&参数调用派生类定义的Swim(),而无需知道该参数指向的是哪种类型的对象。这就是多态:将派生类对象视为基类对象,并执行派生类的Swim()实现。
为什么需要虚构函数
上面的代码如果加入析构函数释放内存,对于使用new在自由储存区中实例化的派生类对象,如果将其赋值给基类指针,并通过该指针调用delete,将不会调用派生类的析构函数,这可能导致资源未释放、内存泄露等问题,必须引起重视。要避免这种问题,可将基类析构函数声明为虚函数。如下面代码所示:
classFish{ public: Fish() { cout<<"construct Fish"<<endl; } virtual~Fish() { cout<<"Destroy Fish"<<endl; } };
输出还表明,无论Tuna对象是使用new在自由存储区中实例化的,还是以局部变量的方式在栈中实例化的,构造函数和析构函数的调用顺序都相同。
抽象基类和纯虚函数
不能实例化的基类被称为抽象基类,这样的基类只有一个用途,那就是从它派生出其他类。在 C++中,要创建抽象基类,可声明纯虚函数。以下述方式声明的虚函数被称为纯虚函数:
classAbstractBase{ public: virtualvoidDoSomething()=0; //纯虚函数}; 该声明告诉编译器,AbstractBase的派生类必须实现方法DoSomething(); classDerived : publicAbstractBase{ public: voidDoSomething() { cout<<"Implemented virtual function"<<endl; } };
AbstractBase类要求Derived类必须提供虚方法DoSomething()的实现。这让基类可指定派生类中方法的名称和特征(Signature),即指定派生类的接口。虽然不能实例化抽象基类,但可将指针或引用的类型指定为抽象基类。抽象基类提供了一种非常好的机制,让您能够声明所有派生类都必须实现的函数。抽象基类常被简称为ABC。ABC有助于约束程序的设计。
使用虚继承解决菱形问题
如图程序所示,如果没有使用虚继承,则会输出:
AnimalconstructorAnimalconstructorAnimalconstructorPlatypusconstructor
输出表明,由于采用了多继承,且 Platypus 的全部三个基类都是从 Animal 类派生而来的,因此第38行创建Platypus实例时,自动创建了三个Animal实例。Animal 有一个整型成员——Animal::Age,为方便说明问题,将其声明成了公有的。如果您试图通过Platypus 实例访问 Animal::Age(如第 42 行所示),将导致编译错误,因为编译器不知道您要设置Mammal::Animal::Age、Bird::Animal::Age还是Reptile::Animal::Age。更可笑的是,如果您愿意,可以分别设置这三个属性:
duckBilledp.Mamal::Animal::Age=25; duckBilledp.Bird::Animal::Age=25; duckBilledp.Reptile::Animal::Age=25;
显然,鸭嘴兽应该只有一个Age属性,但您希望Platypus类以公有方式继承 Mammal、Bird 和Reptile。解决方案是使用虚继承。如果派生类可能被用作基类,派生它是最好使用关键字virtual:
classDerived1 : publicvirtualBase{ //members and funnctions}; classDerived2 : publicvirtualBase{ //members and funnctions};
在继承层次结构中,继承多个从同一个类派生而来的基类时,如果这些基类没有采用虚继承,将导致二义性。这种二义性被称为菱形问题(Diamond Problem)。其中的“菱形”可能源自类图的形状。
注意:C++关键字virtual的含义随上下文而异(我想这样做的目的很可能是为了省事),对其含义总结如下:
在函数声明中,virtual意味着当基类指针指向派生对象时,通过它可调用派生类的相应函数。从Base类派生出Derived1和Derived2类时,如果使用了关键字virtual,则意味着再从Derived1和Derived2派生出Derived3时,每个Derived3实例只包含一个Base实例。也就是说,关键字virtual被用于实现两个不同的概念。
可将复制构造函数声明为虚函数吗
根本不可能实现虚复制构造函数,因为在基类方法声明中使用关键字virtual时,表示它将被派生类的实现覆盖,这种多态行为是在运行阶段实现的。而构造函数只能创建固定类型的对象,不具备多态性,因此C++不允许使用虚复制构造函数。虽然如此,存在一种不错的解决方案,就是定义自己的克隆函数来实现上述目的:
虚函数Clone模拟了虚复制构造函数,但需要显式地调用,如下面程序所示:
输出为:
TunaswimsfastintheseaCarpswimsslowinthelakeTunaswimsfastintheseaCarpswimsslowinthelake
在main()中,第 40~44行声明了一个静态基类指针(Fish*')数组,并各个元素分别设置为新创建的Tuna、Carp、Tuna和Carp对象。注意到myFishes数组能够存储不同类型的对象,这些对象都是从Fish派生而来的。这太酷了,因为为本书前面的大部分数组包含的都是相同类型的数据,如int。如果这还不够酷,您还可以在循环中使用虚函数Fish::Clone将其复制到另一个Fish*'数组(myNewFishes)中,如第48行所示。注意到这里的数组很小,只有4个元素,但即便数组长得多,复制逻辑也差别不大,只需调整循环结束条件即可。第 52 行进行了核实,它通过新数组的每个元素调用虚函数 Swim(),以验证Clone()复制了整个派生类对象。
PS:我的c++系列全部代码还有笔记都上传到github上了,欢迎star和fork。
github链接:https://github.com/xwr96/21-Day-grasped-Cpp