c++基础之--- 多态

1. 多态

1.1 介绍

c++面向对象的三大特征:

  • 封装
    数据与具体操作实现的代码都放在某个对象的内部,使这些代码的具体细节不被外界发现,只留下一些接口供外部来使用,而不能一任何的形式来对象内部的实现。使用封装能够隐藏具体的实现的细节,使代码更加易于维护并且保证了系统的安全性。
  • 继承
    继承机制是面向对象程序设计使代码进行复用的最重要的手段,他允许程序员在保证类原有类特性的基础上进行扩展来增加功能。这样新产生的类就被称为派生类,继承就可以表现面向对象机制的的层次结构。
  • 多态
    • 定义:“一个接口,多种方法”,程序在运行时才决定调用的函数。
    • 实现:C++多态性主要是通过虚函数实现的,虚函数允许子类重写override(注意和overload的区别,overload是重载,是允许同名函数的表现,这些函数参数列表/类型不同)。
    • 目的:接口重用。封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。
    • 用法:声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。
    • 多态与非多态的实质区别就是函数地址是早绑定还是晚绑定。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并生产代码,是静态的,就是说地址是早绑定的。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就属于晚绑定。
    • 我的理解:类定义接口,各对象根据自己的需求重写虚函数。

1.2 虚函数

虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。

多态是由虚函数实现的,而虚函数主要是通过虚函数表(V-Table)来实现的。
如果一个类中包含虚函数(virtual修饰的函数),那么这个类就会包含一张虚函数表,虚函数表存储的每一项是一个虚函数的地址。
在这里插入图片描述
这个类的每一个对象都会包含一个虚指针(虚指针存在于对象实例地址的最前面,保证虚函数表有最高的性能),这个虚指针指向虚函数表。
注:对象不包含虚函数表,只有虚指针,类才包含虚函数表,派生类会生成一个兼容基类的虚函数表。

下图是原始基类的对象,可以看到虚指针在地址的最前面,指向基类的虚函数表(假设基类定义了3个虚函数)
  在这里插入图片描述
  假设现在派生类继承基类,并且重新定义了3个虚函数,派生类会自己产生一个兼容基类虚函数表的属于自己的虚函数表。
  在这里插入图片描述
  Derive class 继承了 Base class 中的三个虚函数,准确的说,是该函数实体的地址被拷贝到 Derive类的虚函数表,派生类新增的虚函数置于虚函数表的后面,并按声明顺序存放。

  • 代码示例
#include <iostream>
using namespace std;
class A
{
public:
    virtual void Print() { cout << "A::Print" << endl; }
};
class B : public A
{
public:
    virtual void Print() { cout << "B::Print" << endl; }
};
class D : public A
{
public:
    virtual void Print() { cout << "D::Print" << endl; }
};
class E : public B
{
    virtual void Print() { cout << "E::Print" << endl; }
};
int main()
{
    A  a; B b; D d; E e;
    A *pa = &a;  B *pb = &b;
    pa->Print();    //多态, a.Print()被调用,输出:A::Print
    pa = pb;        //基类指针pa指向派生类对象b
    pa->Print();  //b.Print()被调用,输出:B::Print
    pa = &d;       //基类指针pa指向派生类对象d
    pa->Print();  //多态, d. Print ()被调用,输出:D::Print
    pa = &e;       //基类指针pa指向派生类对象d
    pa->Print();  //多态, e.Print () 被调用,输出:E::Print
    return 0;
}

程序的输出结果是:
A::Print
B::Print
D::Print
E::Print
程序中,四个类之间的派生关系如下所示。
在这里插入图片描述
每个类都有同名、同参数表的虚函数 Print(每个 Print 函数声明时都加了 virtual 关键字)。根据多态的规则,对于语句“pa->Print()”,由于 Print 是虚函数,尽管 pa 是基类 A 的指针,编译时也不能确定调用的是哪个类的 Print 函数。当程序运行到该语句时,pa 指向的是哪个类的对象,调用的就是哪个类的 Print 函数。

例如,程序执行到第 26 行时,pa 指向的是基类对象 a,因此调用的就是类 A 的 Print 成员函数;执行到第 28 行时,pa 指向的是类 B 的对象,因此调用的是类 B 的 Print 成员函数;第 30 行也是如此;类 E 是类 A 的间接派生类,因此,执行到第 32 行时,多态规则仍然适用,此时 pa 指向派生类 E 的对象,故调用的是类 E 的 Print 成员函数。

需要强调的是,编译器不会分析程序的运行过程。编译器并没有通过分析程序上下文,得出在第 28 行 pa 指向的是类 B 的对象,因此第 28 行应该调用类 B 的 Print 成员函数这样的结论。

多态的语句调用哪个类的成员函数是在运行时才能确定的,编译时不能确定(具体原理后面会解释)。因此,多态的函数调用语句被称为是“动态联编”的,而普通的函数调用语句是“静态联编”的。

1.3 重载(overload)、重写(override)、隐藏(overwrite)

Overload(重载):在C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,但参数或返回值不同(包括类型、顺序不同),即函数重载。
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
Override(重写):是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
注:重写基类虚函数的时候,会自动转换这个函数为virtual函数,不管有没有加virtual,因此重写的时候不加virtual也是可以的,不过为了易读性,还是加上比较好。
Overwrite(隐藏):隐藏,是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

1.4 什么函数不能作为虚函数

  • 1)普通函数
    普通函数不属于成员函数,是不能被继承的。普通函数只能被重载,不能被重写,因此声明为虚函数没有意义。因为编译器会在编译时绑定函数。
    而多态体现在运行时绑定。通常通过基类指针指向子类对象实现多态。

  • 2)友元函数
    友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。

  • 3)构造函数
    首先说下什么是构造函数,构造函数是用来初始化对象的。假如子类可以继承基类构造函数,那么子类对象的构造将使用基类的构造函数,而基类构造函数并不知道子类的有什么成员,显然是不符合语义的。从另外一个角度来讲,多态是通过基类指针指向子类对象来实现多态的,在对象构造之前并没有对象产生,因此无法使用多态特性,这是矛盾的。因此构造函数不允许继承。

  • 4)内联成员函数
    我们需要知道内联函数就是为了在代码中直接展开,减少函数调用花费的代价。也就是说内联函数是在编译时展开的。而虚函数是为了实现多态,是在运行时绑定的。因此显然内联函数和多态的特性相违背。

  • 5)静态成员函数
    首先静态成员函数理论是可继承的。但是静态成员函数是编译时确定的,无法动态绑定,不支持多态,因此不能被重写,也就不能被声明为虚函数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值