C++系列笔记(五)

简介: C++系列笔记(五)

导读】《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(),程序如下所示:

#include<iostream>#include<string>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有助于约束程序的设计。

使用虚继承解决菱形问题

71ae31441c86f49d2f2a0311a00cdb51.jpg

如图程序所示,如果没有使用虚继承,则会输出:

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++不允许使用虚复制构造函数。虽然如此,存在一种不错的解决方案,就是定义自己的克隆函数来实现上述目的:

c117b78ea046f0645517c8adbd1d8e51.jpg

虚函数Clone模拟了虚复制构造函数,但需要显式地调用,如下面程序所示:

4683e4f00648846b030e6e71374e9e02.jpg

ea1bc08de176e196a9062ac1a9421d3f.jpg

输出为:

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

8f9d93cf1e422e58870b9099516fd5aa.png

相关文章
|
6月前
|
算法 C++
算法笔记:递归(c++实现)
算法笔记:递归(c++实现)
|
6月前
|
编译器 C++
《Effective C++ 改善程序与设计的55个具体做法》 第一章 笔记
《Effective C++ 改善程序与设计的55个具体做法》 第一章 笔记
|
4月前
|
C++ 容器
【C/C++笔记】迭代器
【C/C++笔记】迭代器
33 1
|
4月前
|
存储 安全 程序员
【C/C++笔记】迭代器范围
【C/C++笔记】迭代器范围
75 0
|
5月前
|
C++ Windows
FFmpeg开发笔记(三十九)给Visual Studio的C++工程集成FFmpeg
在Windows上使用Visual Studio 2022进行FFmpeg和SDL2集成开发,首先安装FFmpeg至E:\msys64\usr\local\ffmpeg,然后新建C++控制台项目。在项目属性中,添加FFmpeg和SDL2的头文件及库文件目录。接着配置链接器的附加依赖项,包括多个FFmpeg及SDL2的lib文件。在代码中引入FFmpeg的`av_log`函数输出"Hello World",编译并运行,若看到"Hello World",即表示集成成功。详细步骤可参考《FFmpeg开发实战:从零基础到短视频上线》。
252 0
FFmpeg开发笔记(三十九)给Visual Studio的C++工程集成FFmpeg
|
7月前
|
存储 C++ 容器
黑马c++ STL部分 笔记(7) list容器
黑马c++ STL部分 笔记(7) list容器
|
7月前
|
算法 C++ 容器
黑马c++ STL常用算法 笔记(5) 常用算术生成算法
黑马c++ STL常用算法 笔记(5) 常用算术生成算法
|
7月前
|
算法 C++ 容器
黑马c++ STL常用算法 笔记(4) 常用拷贝和替换算法
黑马c++ STL常用算法 笔记(4) 常用拷贝和替换算法
|
7月前
|
存储 算法 搜索推荐
黑马c++ STL常用算法 笔记(3) 排序算法
黑马c++ STL常用算法 笔记(3) 排序算法
|
7月前
|
算法 C++
黑马c++ STL常用算法 笔记(2) 查找算法
黑马c++ STL常用算法 笔记(2) 查找算法