C++【多态】

简介: C++【多态】

🌇前言

多态 是面向对象三大基本特征中的最后一个,多态 可以实现 “一个接口,多种方法”,比如父子类中的同名方法,在增加了多态后,调用同名函数时,可以根据不同的对象(父类对象或子类对象)调用属于自己的函数,实现不同的方法,因此 多态 的实现依赖于 继承

同一个售票地点,为不同的购票方式提供了不同的取票窗口(多种状态 -> 多态


🏙️正文

1、多态基本概念

在使用多态的代码中,不同对象完成同一件事会产生不同的结果

比如在购买高铁票时,普通人原价,学生半价,而军人可以优先购票,对于 购票 这一相同的动作,需要 根据不同的对象提供不同的方法

#include <iostream>
using namespace std;
class Person
{
public:
  virtual void identity() { cout << "普通人原价" << endl; }
};
class Student : public Person
{
public:
  virtual void identity() { cout << "学生半价" << endl; }
};
class Soldier : public Person
{
public:
  virtual void identity() { cout << "军人优先购票" << endl; }
};
void BuyTickets(Person& people)
{
  //根据不同的身份,选择不同的购票方案
  people.identity();
}
int main()
{
  Person ordinary;  //普通人
  Student student;  //学生
  Soldier soldier;  //军人
  //调用同一个购票函数
  BuyTickets(ordinary);
  BuyTickets(student);
  BuyTickets(soldier);
  return 0;
}


可以看到在调用同一函数、同一方法的情况下,不同对象的执行结果不同

注:父类 Peoson 中使用的 virtual 关键字和 BuyTickets 函数中的父类引用 是实现多态的关键


2、多态的定义及实现

实现多态需要借助虚表(虚函数表),而构成虚表又需要虚函数,即 virtual 修饰的函数,除此之外还需要使用虚表指针来进行函数定位、调用

2.1、构成多态的两个必要条件

必要条件

  1. virtual 修饰后形成的虚函数,与其他类中的虚函数形成 重写(三同:返回值、函数名、参数均相同)
  2. 必须通过【父类指针】或【父类引用】进行虚函数调用

virtual 修饰后,成为 虚函数

virtual void identity() { cout << "普通人原价" << endl; }
virtual void identity() { cout << "学生半价" << endl; }
virtual void identity() { cout << "军人优先购票" << endl; }


通过 【父类指针】或【父类引用】 调用 虚函数

void BuyTickets(Person& people)
{
  //根据不同的身份,选择不同的购票方案
  people.identity();
}


除了后续两种特殊情况外,上述两个构成多态的必备条件缺一不可!

缺少条件一:没有虚函数

缺少条件二:不是【父类指针】或【父类引用】进行虚函数调用

显然,缺少其中任意一个条件,都不构成多态

当然还存在两个例外:

  1. 除父类外,其他子类中的函数不必使用 virtual 修饰,此时仍然能构成多态(注意三同,需要构成重写)
  2. 父子类中的虚函数返回值可以不相同,但此时需要返回对应的父类指针或子类指针,确保构成多态,这一现象称为 协变(了解)

例外一:子类虚函数没有使用 virtual 修饰

例外一有点违反 必要条件一 的意思,不过在某些场景中,这个例外很实用,比如:给父类的析构函数加上 virtual 修饰,这样在进行析构函数调用时,得益于 多态,父类指针可以针对不同对象调用不同的析构函数释放资源

  • 无论是谁的析构函数,最终函数名都为 destructor,可能存在析构错误调用的问题,因此可以利用 virtual 修饰父类的析构函数,这样子类在继承时,自动形成多态
#include <iostream>
using namespace std;
class Person
{
public:
  //此时未构成多态
  ~Person() { cout << "~Person" << endl; }
};
class Student : public Person
{
public:
  ~Student() { cout << "~Student" << endl; }
};
class Soldier : public Person
{
public:
  ~Soldier() { cout << "~Soldier" << endl; }
};
int main()
{
  //父类指针 指向子类对象
  Person* p1 = new Person();  //普通人
  Person* p2 = new Student(); //学生
  Person* p3 = new Soldier(); //军人
  //释放空间
  delete p1;
  delete p2;
  delete p3;
  return 0;
}


假若不使用 virtual 修饰父类析构函数,直接运行代码,结果如下:

显然此时并未释放两个子类的资源,导致内存泄漏,可以给父类析构函数加上 virtual,构成多态

//此时构成多态(利用例外一)
virtual ~Person() { cout << "~Person" << endl; }


面试题:为什么要在 父类/基类 的析构函数中加上 virtual 修饰?

  • 为了构成多态,确保不同对象的析构函数能被成功调用,避免内存泄漏

建议:例外一会破坏代码的可阅读性,可能无法让别人一眼看出多态,因此除了析构函数外,不推荐在子类虚函数中省略 virtual

例外二:协变

如何快速判断是否构成多态?

  • 首先观察父类的函数中是否出现了 virtual 关键字
  • 其次观察是否出现虚函数重写现象,三同:返回值、函数名、参数(协变例外)
  • 最后再看调用虚函数时,是否为【父类指针】或【父类引用】

父类指针或引用调用函数时,如何判断函数调用关系?

  • 若满足多态:看其指向对象的类型,调用这个类型的成员函数
  • 不满足多态:看具体调用者的类型,进行对应的成员函数调用

2.2、虚函数及重写

所以什么是虚函数?为什么类中被 virtual 修饰的函数能变成虚函数?

虚函数的作用是在目标函数(想要构成多态的函数)之间构成 重写(覆盖),一旦构成了 重写(覆盖),那么子类对象在实现此虚函数时,会 继承父类中的虚函数接口(返回值、函数名、参数列表),然后覆盖至子类对应的虚函数处,因此 重写又叫做覆盖

#include <iostream>
using namespace std;
class Person
{
public:
  virtual void func(int a = 10) { cout << "Person a: " << a << endl; }
};
class Student : public Person
{
public:
  virtual void func(int a = 20) { cout << "Student a: " << a << endl; }
};
int main()
{
  Person* p = new Student();
  p->func();
  return 0;
}


预想结果:输出 Student a: 20,但实际情况不是如此

不难看出,子类 Student 中虚函数 func 实际上是 Personfunc 的返回值、函数名、参数列表 + Studentfunc 的函数体 组合而成

所以虚函数就是 虚拟 的函数,可以被覆盖的、实际形态未确定的函数,使用 virtual 修饰后,就是在告诉编译器:标记此函数,调用时要触发 覆盖 行为,同时虚表指针需要找到正确的函数进行调用

注意:

  1. 除了类中的成员函数外,普通函数不能添加 virtual 关键字进行修饰,因为虚函数、虚函数表、虚表指针是一体的,普通函数没有
  2. 此处的 virtual 修饰函数为虚函数,与 virtual 修饰类继承为虚继承没有关系:一个是实现多态的基础,而另一个是解决菱形继承的问题
  3. 同样的,假设不是父类指针或引用进行调用,不会构成多态,也不会发生重写(覆盖)行为

2.3、final 与 override

C++11 中,新增了两个多态相关的关键字:finaloverride

final修饰父类的虚函数,不让子类的虚函数与其构成重写,即不构成多态

override修饰子类的虚函数,检查是否构成重写(是否满足重写的必要条件),若不满足,则报错

显然一个是 避免被重写 --> 不实现多态,而另一个是 检查是否完成重写 --> 后续实现多态

对父类的虚函数加上 final无法构成重写

对子类的虚函数加上 override 进行 重写检查

新标准中的小工具,在某些场景下很实用

final 还可以修饰父类,修饰后,父类不可被继承

注:final 可以修饰子类的虚函数,因为子类也有可能成为父类;但 override 无法修饰父类的虚函数,因为父类之上没有父类了,自然无法构成重写

2.4、重载、重写、重定义

截至目前为止,我们已经学习了三个 “重” 相关函数知识:重载、重写、重定义

这三兄弟不止名字很像,而是功能也都差不多,很多面试题中也喜欢考这三者的区别

重载:即函数重载,函数参数 不同而触发,不同的 函数参数 最终修饰结果不同,确保链接时不会出错,构成重载

重写(覆盖):发生在类中,当出现虚函数且符合重写的三同原则时,则会发生重写(覆盖)行为,具体表现为 父类虚函数接口 + 子类虚函数体,是实现多态的基础

重定义(隐藏):发生在类中,当子类中的函数名与父类中的函数名起冲突时,会隐藏父类同名函数,默认调用子类的函数,可以通过 :: 指定调用

重写和重定义比较容易记混,简言之 先看看是否为虚函数,如果是虚函数且三同,则为重写;若不是虚函数且函数名相同,则为重定义

注:在类中,仅仅是函数名相同(未构成重写的情况下),就能触发 重定义(隐藏)


3、抽象类

什么是抽象?难道是 围棋大师柯洁直播 “云顶之弈” 下电子围棋 吗?

当然不是,抽象类是一种极其特殊的类:不允许实例化对象

什么年代了还下传统围棋~

3.1、定义与特点

如何实现一个抽象类:在虚函数之后加上 =0,此时虚函数升级为 纯虚函数

纯虚函数也可以与普通虚函数构成重写,也能实现多态,不过包含纯虚函数的类不能实例化对象,因此也被称为抽象类

注意:只要类中有一个函数被修饰为纯虚函数,那么这个类就会变成抽象类

纯虚函数:

virtual void func(int a = 10) = 0 { cout << "Person a: " << a << endl; 


抽象的线条画无法直接看出作者的意图,抽象类也是如此,无法实例化出具体对象,你只知道这个类存在

出自著名画家 彼埃·蒙德里安

尝试使用 纯虚函数 构成的 抽象类 实例化对象

#include <iostream>
using namespace std;
//抽象类
class Person
{
public:
  //纯虚函数
  virtual void func(int a = 10) = 0 { cout << "Person a: " << a << endl; }
};
int main()
{
  //纯虚函数无法实例化对象
  Person p;
  Person pp = new Person(); //也不能new出对象
  return 0;
}


3.2、抽象类的用途

抽象类适合用于描述无法拥有实体的类,比如 人、动物、植物,毕竟这些都是不能直接使用的,需要经过 继承 赋予特殊属性后,才能作为一个独立存在的个体(对象)

#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
  Person(const string& name = string())
    :_name(name)
  {}
  virtual void func() = 0 {};
protected:
  string _name;
};
class Student : public Person
{
public:
  Student(const string& name = string())
    :Person(name)
  {}
  //子类继承抽象类后,需要重写纯虚函数,否则仍然是抽象类
  virtual void func() {};
};
int main()
{
  //抽象类无法直接实例化对象
  //Person p("newnew");
  Student s("newnew");
  return 0;
}


抽象类的继承很好的体现了函数重写时,继承的是父类虚函数接口的事实,这正是实现多态的基础

普通继承:子类可以直接使用父类中的函数

接口继承:子类虚函数继承父类虚函数的接口,进行重写,构成多态

建议:假如不是为了多态,那么最好不要使用 virtual 修饰函数,更不要尝试定义纯虚函数

注意: 若父类中为抽象类,那么子类在继承后,必须对其中的纯虚函数进行重写,否则无法实例化出对象


4、多态实现原理

所以如此神奇的多态究竟是如何实现的?先来看一段简单的代码

#include <iostream>
using namespace std;
class Test
{
  virtual void func() {};
};
int main()
{
  Test t; //创建一个对象
  cout << "Test sizeof(): " << sizeof(t) << endl;
  return 0;
}


这是一个空类,其中什么成员都没有,但有一个虚函数

所以一个对象的大小为多少?

0 吗 ?

答案是 4,当前是 32 位平台下,如果是在 64 位平台,大小会变为 8

大小随平台而变的只能是指针了,因此可以推测当前类中藏着一个 虚表指针

就是依靠这个 虚表指针+虚表 实现了多态

4.1、虚表与虚表指针

虚函数表(虚表)即 virtual function table -> vft,指向虚表的指针称为 虚表指针 virtual function pointer -> vfptr,在 vs 的监视窗口中,可以看到涉及虚函数类的对象中都有属性 __vfptr(虚表指针),可以通过虚表指针所指向的地址,找到对应的虚表

  • 虚函数表中存储的是虚函数指针,可以在调用函数时根据不同的地址调用不同的方法

在下面这段代码中,父类 Person 有两个虚函数(func3 不是虚函数),子类 Student 重写了 func1 这个虚函数,同时新增了一个 func4 虚函数

#include <iostream>
using namespace std;
class Person
{
public:
  virtual void func1() { cout << "Person::fun1()" << endl; };
  virtual void func2() { cout << "Person::fun2()" << endl; };
  void func3() { cout << "Person::fun3()" << endl; }; //fun3 不是虚函数
};
class Student : public Person
{
public:
  virtual void func1() { cout << "Student::fun1()" << endl; };
  virtual void func4() { cout << "Student::fun4()" << endl; };
};
int main()
{
  Person p;
  Student s;
    return 0;
}


如何通过程序验证虚表的真实性?

  • 虚表指针指向虚表,虚表中存储的是虚函数地址,而 32 位平台中指针大小为 4 字节
  • 因此可以先将虚表指针强转为 指向首个虚函数 的指针,然后遍历虚表打印各个虚函数地址验证即可
  • vs 中对虚表做了特殊处理:在虚表的结尾处放了一个 nullptr,因此下面这段代码可能在其他平台中跑不了
//打印虚表
typedef void(*VF_T)();
void PrintVFTable(VF_T table[]) //也可以将参数类型设为 VF_T*
{
  //vs中在虚表的结尾处添加了 nullptr
  //如果运行失败,可以尝试清理解决方案重新编译
  int i = 0;
  while (table[i])
  {
    printf("[%d]:%p->", i, table[i]);
    VF_T f = table[i];
    f();  //调用函数,相当于 func()
    i++;
  }
  cout << endl;
}
int main()
{
  //提取出虚表指针,传递给打印函数
  Person p;
  Student s;
  //第一种方式:强转为虚函数地址(4字节)
  PrintVFTable((VF_T*)(*(int*)&p));
  PrintVFTable((VF_T*)(*(int*)&s));
  return 0;
}


子类重写后的虚函数地址与父类不同

因为平台不同指针大小不同,因此上述传递参数的方式(VF_T*)(*(int*)&p 具有一定的局限性
假设在 64 位平台下,需要更改为 (VF_T*)(*(long long*)&p

//64 位平台下指针大小为 8字节
PrintVFTable((VF_T*)(*(long long*)&p));
PrintVFTable((VF_T*)(*(long long*)&s));


除此之外还可以间接将虚表指针转为 VF_T* 类型进行参数传递

//同时适用于 32位 和 64位 平台
PrintVFTable(*(VF_T**)&p);
PrintVFTable(*(VF_T**)&s);


传递参数时的类型转换路径

不能直接写成 PrintVFTable((VF_T*)&p);,因为此时取的是整个虚表区域的首地址地址,无法定位我们所需要虚表的首地址,打印时会出错

  • 类似于 int* arr[]int* 是第一个指针数组的首地址,遍历的是第一个指针数组;而 int** 是整个指针数组的首地址,遍历的是整个指针数组,+1 会直接跳过一个指针数组

错误写法:

//错误写法
PrintVFTable((VF_T*)&p);
PrintVFTable((VF_T*)&s);


综上所述,虚表是真实存在的,只要当前类中涉及了虚函数,那么编译器就会为其构建相应的虚表体系

虚表相关知识补充:

  • 虚表是在 编译 阶段生成的
  • 虚表指针是在构造函数的 初始化列表 中初始化的
  • 虚表一般存储在 常量区(代码段),有的平台中可能存储在 静态区(数据段)

通过一段简单的代码验证 虚表的存储位置

int main()
{
  //验证虚表的存储位置
  Person p;
  Student s;
  int a = 10; //栈
  int* b = new int; //堆
  static int c = 0; //静态区(数据段)
  const char* d = "xxx";  //常量区(代码段)
  printf("a-栈地址:%p\n", &a);
  printf("b-堆地址:%p\n", b);
  printf("c-静态区地址:%p\n", &c);
  printf("d-常量区地址:%p\n", d);
  printf("p 对象虚表地址:%p\n", *(VF_T**)&p);
  printf("s 对象虚表地址:%p\n", *(VF_T**)&s);
  return 0;
}


显然,虚表地址与常量区的地址十分接近,因此可以推测 虚表位于常量区中,因为它需要被同一类中的不同对象共享,同时不能被修改(如同代码一样)

函数代码也是位于 常量区(代码段),可以在监视窗口中观察两者的差异

4.2、虚函数调用过程

现在来看,虚函数的调用过程就非常简单了

  • 首先确保存在虚函数且构成重写
  • 其次使用【父类指针】或【父类引用】指向对象,其中包含切片行为
  • 切片后,将子类中不属于父类的切掉,只保留父类指针可调用到的部分函数
  • 实际调用时,父类指针的调用逻辑是一致的:比如虚表第一个位置调用第一个函数,虚表第二个位置调用第二个函数,但是因为此时的虚表是切片得到的,所以 同一位置可以调用到不同的函数,这就是多态
int main()
{
  Person* p1 = new Person();
  Person* p2 = new Student();
  p1->func1();
  p2->func1();
  delete p1;
  delete p2;
  return 0;
}


通过汇编代码观察:

注:下图中的函数地址仅供参考,与上图中的调用演示并不是同一次运行

4.3、动态绑定与静态绑定

静态绑定(前期绑定/早绑定)

  • 在编译时确定程序的行为,也称为静态多态

动态绑定(后期绑定/晚绑定)

  • 在程序运行期间调用具体的函数,也称为动态多态
p1->func1();
p2->func1();
add(1, 2);
add(1.1, 2.2);


简单来说,静态绑定就像函数重载,在编译阶段就确定了不同函数的调用;而动态绑定是虚函数的调用过程,需要 虚表指针+虚表,在程序运行时,根据不同的对象调用不同的函数


5、单继承与多继承中的虚表

5.1、单继承中的虚表

单继承中的虚表比较简单,无非就是 子类中的虚函数对父类中相应的虚函数进行覆盖

  • 单继承不会出现虚函数冗余的情况,顶多就是子类与父类构成重写

向父类中新增虚函数:父类的虚表中会新增,同时子类会继承,并纳入自己的虚表之中

向子类中新增虚函数:只有子类能看到,因此只会纳入子类的虚表中,父类是看不到并且无法调用的

向父类/子类中添加非虚函数时:不属于虚函数,不进入虚表,仅当作普通的类成员函数处理

5.2、多继承中的虚表

C++ 中支持多继承,这也就意味着可能出现 多个虚函数重写 的情况,当父类指针面临 不同虚表中的相同虚函数重写 时,该如何处理呢?

#include <iostream>
using namespace std;
//父类1
class Base1
{
public:
  virtual void func1() { cout << "Base1::func1()" << endl; }
  virtual void func2() { cout << "Base1::func2()" << endl; }
};
//父类2
class Base2
{
public:
  virtual void func1() { cout << "Base2::func1()" << endl; }
  virtual void func2() { cout << "Base2::func2()" << endl; }
};
//多继承子类
class Derive : public Base1, public Base2
{
public:
  virtual void func1() { cout << "Derive::func1()" << endl; }
  virtual void func3() { cout << "Derive::func3()" << endl; } //子类新增虚函数
};
int main()
{
  Derive d;
  return 0;
}


此时的子类 Derive 中拥有两张虚表,分别为 Base1 + Derive::func1 构成的虚表Base2 + Derive::func1 构成的虚表

此时出现了两个问题:

  1. 子类 Derive 中新增的虚函数 func3 位于哪张虚表中?
  2. 为什么重写的同一个 func1 函数,在两张虚表中的地址不相同?

这两个问题是多继承多态中的主要问题

5.2.1、子类新增虚函数的归属问题

在单继承中,子类中新增的虚函数会放到子类的虚表中,但这里是多继承,子类有两张虚表,所以按照常理来说,应该在两张虚表中都新增虚函数才对

但实际情况是 子类中新增的虚函数默认添加至第一张虚表中

通过 PrintVFTable 函数打印虚表进行验证

因此此时有两张虚表,所以需要分别打印

  • 第一张虚表简单,直接取地址+类型强转,如法炮制即可
  • 第二张虚表就比较麻烦了,需要在第一张虚表的起始地址处,跳过第一张虚表的大小,然后才能获取第二张虚表的起始地址

//打印虚表
typedef void(*VF_T)();
void PrintVFTable(VF_T table[])
{
  //vs中在虚表的结尾处添加了 nullptr
  int i = 0;
  while (table[i])
  {
    printf("[%d]:%p->", i, table[i]);
    VF_T f = table[i];
    f();  //调用函数,相当于 func()
    i++;
  }
  cout << endl;
}
int main()
{
  Derive d;
  PrintVFTable(*(VF_T**)&d);  //第一张虚表
  PrintVFTable(*(VF_T**)((char*)&d + sizeof(Base1))); //第二张虚表
  return 0;
}


可以看出新增的 func3 函数确实在第一张虚表中

可能有的人觉得取第二张虚表的起始地址很麻烦,那么可以试试利用 切片 机制,天然的取出第二张虚表的地址

切片行为是天然的,可以完美取到目标地址

Base2* table2 = &d; //切片
PrintVFTable(*(VF_T**)table2);  //第二张虚表


此时已经解决问题一:子类新增虚函数的归属问题 —> 添加至第一张虚表中

5.2.2、冗余虚函数的调用问题

在上面的多继承多态代码中,子类分别重写了两个父类中的 func1 函数,但最终通过监视窗口发现:同一个函数在两张虚表中的地址不相同

因此可以推测:编译器在调用时,根据不同的地址寻找到同一函数,解决冗余虚函数的调用问题

至于实际调用链路,还得通过汇编代码展现:

ptr2 在调用时的关键语句 sub ecx 4

  • sub 表示减法,ecx 通常存储 this 指针,4 表示 Base1 的大小
  • 这条语句表示将当前的 this 指针向前偏移 sizeof(Base1),后续再 jmp 时,调用的就是同一个 func1

这一过程称为 this 指针修正,用于解决冗余虚函数的调用问题

为什么是 Base2 修正?

  • 因为先继承了 Base1,后继承了 Base2,假设先继承的是 Base2,那么修正的就是 Base1

这种设计很大胆也很巧妙,完美解决了多继承多态带来的问题

因此回答问题二:两张虚表中同一个函数的地址不同,是因为调用方式不同,后继承类中的虚表需要通过 this 指针修正的方式调用虚函数

5.3、菱形继承多态与菱形虚拟继承多态(了解)

菱形继承问题是 C++ 多继承中的大坑,为了解决菱形继承问题,提出了 虚继承 + 虚基表 的相关概念,那么在多态的加持之下,菱形继承多态变得更加复杂:需要函数调用链路设计的更加复杂

菱形虚拟继承多态就更不得了:需要同时考虑两张表:虚表、虚基表

  • 虚基表中空余出来的那一行是用来存储偏移量的:表示当前虚基表距离虚表有多远

因为这种写法过于复杂,所以在实际中一般不会使用,更不会去考

如果感兴趣的同学可以看看下面这两篇相关文章:

C++虚函数表解析

C++对象的内存布局


6、多态相关面试题

一些简单的概念题,主要是为了回顾面向对象特性

6.1、基本概念(选择)

1.下面哪种面向对象的方法可以让你变得富有( )

A: 继承
B: 封装
C: 多态
D: 抽象

2.以下关于纯虚函数的说法,正确的是( )

A:声明纯虚函数的类不能实例化对象
B:声明纯虚函数的类是虚基类
C:子类必须实现基类的纯虚函数
D:纯虚函数必须是空函数

3.关于虚表说法正确的是( )

A:一个类只能有一张虚表
B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C:虚表是在运行期间动态生成的
D:一个类的不同对象共享该类的虚表

4.下面程序输出结果是什么? ()

#include<iostream>
using namespace std;
class A
{
public:
  A(const char* s) { cout << s << endl; }
  ~A() {}
};
class B :virtual public A
{
public:
  B(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:
  C(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public B, public C
{
public:
  D(const char* s1, const char* s2, const char* s3, const char* s4) :B(s1, s2), C(s1, s3), A(s1)
  {
    cout << s4 << endl;
  }
};
int main() {
  D* p = new D("class A", "class B", "class C", "class D");
  delete p;
  return 0;
}


A:class A class B class C class D
B:class D class B class C class A
C:class D class C class B class A
D:class A class C class B class D

5.多继承中指针偏移问题,下面说法正确的是( )

class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main()
{
  Derive d;
  Base1* p1 = &d;
  Base2* p2 = &d;
  Derive* p3 = &d;
  return 0;
}


A:p1 == p2 == p3
B:p1 < p2 < p3
C:p1 == p3 != p2
D:p1 != p2 != p3

答案:

  1. A
  2. A
  3. D
  4. A
  5. C

6.2、综合问答(简答)

1.什么是多态?

多态是指不同继承关系的类对象,去调用同一函数,产生了不同的行为。多态又分为静态的多态和动态的多态

2.为什么要在父类析构函数前加上 virtual 修饰?

与子类析构函数构成多态,确保析构函数能被成功调用

3.什么是重载、重写、重定义?三者区别是什么?

重载:同名函数因参数不同而形成不同的函数修饰名,因此同名函数可以存在,并且能被正确匹配调用
重写:父子类中的函数被 virtual 修饰为虚函数,并且符合 “三同” 原则,构成重写
重定义:父子类中的同名函数,在不被重写的情况下,构成重定义,父类同名函数被隐藏

重载可以出现任何位置,只要函数在同一作用域中,而重定义是重写的基础,或者是重写包含重定义,假设因为没有 virtual 修饰不构成重写,那么必然构成重定义,重写和重定义只能发生在继承关系中

4.为什么内联修饰可以构成多态?

不同环境下结果可能不同

内联对编译器只是建议,当编译器识别为虚函数时,会忽略 inline

5.静态成员函数为什么不能构成多态?

没有 this 指针,不进虚表,构造函数也不能构成多态

6.普通函数与虚函数的访问速度?

没有实现多态时,两者一样快

实现多态后,普通函数速度快,因为虚函数还需要去虚表中调用


🌆总结

以上就是本次关于 C++【多态】的全部内容了,在本篇文章中,我们重点介绍了多态的相关知识,如什么是多态、如何使用多态、构成多态的两个必要条件及两个例外该,最后还学习了多继承模式下多态引发的相关问题,探究了其原理。本文中最重要的莫过于 虚表 的相关概念,只有自己多测试、多调试、多画图 才能加深对虚表的理解



相关文章推荐


C++ 进阶知识


C++【继承】


STL 之 泛型思想


C++【模板进阶】


C++【模板初阶】


STL 之 适配器


C++ STL学习之【优先级队列】


C++ STL学习之【反向迭代器】


C++ STL学习之【容器适配器】
目录
相关文章
|
1月前
|
C++
C++多态实现计算器
C++多态实现计算器
|
2月前
|
C++
9. C++虚函数与多态
9. C++虚函数与多态
32 0
|
2月前
|
存储 编译器 C++
C++:多态究竟是什么?为何能成为面向对象的重要手段之一?
C++:多态究竟是什么?为何能成为面向对象的重要手段之一?
51 0
|
2月前
|
存储 安全 算法
【C/C++ 数据发送结构设计】C++中的高效数据发送:多态、类型擦除与更多解决方案
【C/C++ 数据发送结构设计】C++中的高效数据发送:多态、类型擦除与更多解决方案
79 0
|
2月前
|
算法 Java 编译器
【C++ 关键字 virtual 】C++ virtual 关键字(将成员函数声明为虚函数实现多态
【C++ 关键字 virtual 】C++ virtual 关键字(将成员函数声明为虚函数实现多态
25 0
|
9天前
|
编译器 C++
c++的学习之路:22、多态(1)
c++的学习之路:22、多态(1)
21 0
c++的学习之路:22、多态(1)
|
2月前
|
存储 编译器 C++
【C++练级之路】【Lv.13】多态(你真的了解虚函数和虚函数表吗?)
【C++练级之路】【Lv.13】多态(你真的了解虚函数和虚函数表吗?)
|
2天前
|
C++
深入理解 C++ 中的多态与文件操作
C++中的多态是OOP核心概念,通过继承和虚函数实现。虚函数允许对象在相同操作下表现不同行为,提高代码可重用性、灵活性和可维护性。例如,基类`Animal`声明`makeSound()`虚函数,派生类如`Cat`、`Dog`和`Bird`可重写该函数实现各自叫声。C++也提供多种文件操作,如`fstream`库的`ofstream`、`ifstream`用于读写文件,C++17引入的`&lt;filesystem&gt;`库提供更现代的文件操作接口。
9 0
|
8天前
|
存储 C++
【C++进阶(九)】C++多态深度剖析
【C++进阶(九)】C++多态深度剖析
|
9天前
|
编译器 C++
c++的学习之路:23、多态(2)
c++的学习之路:23、多态(2)
21 0