目录
前言
众所周知,面向对象语言的三大特性就是封装、继承和多态,在之前的文章中我们就介绍了继承的相关内容。
这次我们一起来学习一下多态的相关内容。
什么是多态
🧊多态,顾名思义就是多种形态,换言之就是不同对象做同一件事有不同的结果。
🧊在我们的日常生活中就常有这种情景的出现。
🧊例如: 买票时特殊人群有不同的价格,有的视频只有开通 vip 才能观看。
🧊带入上文中便是,普通人和特殊人群的这两个对象同样进行买票这个操作但花费的金钱不同。
多态的实现
虚函数
🧊我们在类的成员函数前加上 virtual 便可将其定义成虚函数。

虚函数的重写
🧊当父类与子类拥有同一虚函数 (返回值和参数都相同) 此时就叫作虚函数的重写或叫作覆盖。

多态的条件
- 虚函数的重写
- 父类的指针或引用进行函数的调用
🧊只有同时满足上述的两种条件才能构成多态。
🧊通过继承部分的知识,我们知道父类的指针和引用都能够指向子类对象,此时对重写的虚函数进行调用,便会根据原对象的类型调用对应的函数。
class A
{
public:
virtual void who()
{
cout << "is a" << endl;
}
};
class B : public A
{
public:
virtual void who()
{
cout << "is b" << endl;
}
};
int main()
{
A a;
B b;
A* pa = &a;
A* pb = &b;
pa->who();
pb->who();
return 0;
}
🧊运行后的结果便是指向 a 对象的指针调用了 a 中的函数,而指向 b 对象的指针则调用了 b 中的指针。

🧊再带入到定义中来看,A 和 B 两个对象同样执行 who 函数这个事件,但最后输出的结果不同。
特例
- 父类函数写 virtual,而子类不写也能够构成重写。
class A
{
public:
virtual void who()
{
cout << "is a" << endl;
}
};
class B : public A
{
public:
void who()
{
cout << "is b" << endl;
}
};
![]()
🧊虚函数本身就是为了多态,即为了被重写而存在的,因此既然父类中有同样的虚函数,编译器便认为你需要进行重写。
🧊某种程度上也是简化了操作,但也减少了可读性。
- 若虚函数的返回值分别为父子关系的引用或指针,则返回值允许不同,这个特性被称为协变。
[注意]: 只有基类返回基类引用/指针,派生类返回派生类的引用/指针,这个条件才能成立。
class person
{};
class student : public person
{};
class A
{
public:
virtual person* func()
{
std::cout << "return person" << std::endl;
return new person;
}
};
class B : public A
{
public:
virtual student* func()
{
std::cout << "return student" << std::endl;
return new student;
}
};
int main()
{
A a;
B b;
A* pa = &a;
A* pb = &b;
pa->func();
pb->func();
return 0;
}

使用场景
🧊在平时使用时,我们有时可能会使用父类的指针指向一个子类对象,若这个时候使用 delete 进行空间释放,便会根据数据类型进行函数调用,即两个空间都使用父类的析构函数进行释放,这显然是不合理的。

🧊若我们想在析构函数调用时根据的不是数据类型,而是根据对象的话,便可以使用多态的方法。
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A
{
public:
virtual ~B()
{
cout << "~B()" << endl;
}
};
int main()
{
A* pa = new A;
A* pb = new B;
delete pa;
delete pb;
return 0;
}
🧊由此使二者的析构函数分开了,而至于为什么还会调用一次 A 的析构,这就是继承的知识了,不记得了记得去复习[doge]。
常用字段
final
🧊final 字段可以加到类的两个地方,其一加在类名后,表示这个类为最终类,不允许其他类继承。

🧊其二则是加在虚函数后,这个虚函数便不能被重写,否则直接报错。

override
🧊用于检查子类虚函数是否重写了父类某个虚函数,若未重写则编译报错。

=0
🧊在虚函数后加上 =0 便会将其定义成纯虚函数,而包含纯虚函数的类叫作抽象类,抽象类不能实例化出对象。
🧊若继承的子类没有重写纯虚函数,那么这个子类仍是抽象类。

多态原理
虚函数表
class Base
{
virtual void func()
{
}
};
🧊由此可见,我们定义了一个空类,其中的成员只有一个虚函数,我们都知道类的成员函数并不直接存在类对象中。
🧊此时打印这个类的大小,我们会看到其竟然占了 4 个字节,而切换成 64 位的情况下该大小变换成了 8 个字节。
🧊实例化一个对象后,打开监视窗口,我们看到其中有一个指针,这便是传说中的虚函数表指针。

🧊通常我们直接称虚函数表为虚表,而虚表的本质其实就是一个虚函数指针数组,只要是虚函数就会被存放到这里,换言之若类中没有虚函数就没有虚表。
🧊虚表在编译阶段生成,而对象的虚表指针则在构造函数的初始化列表中生成。
🧊那么虚表又是如何做到根据对象的决定函数的调用呢?接下来使用这个结构进行演示。
class A
{
public:
virtual void print()
{
cout << "a" << endl;
}
virtual void who()
{
cout << "is a" << endl;
}
protected:
int _a = 3;
};
class B : public A
{
public:
virtual void print()
{
cout << "b" << endl;
}
protected:
int _b = 5;
};
🧊与父类对象中的虚表进行对比后,我们可以看到在虚表中有重写的 print 函数覆盖了父类对应函数的指针,而未重写的 who 函数在虚表中仍是父类函数的指针。

🧊这时,我们便想起来函数重写的另一个名字,覆盖。我们需要记住多态中重写的是实现,而覆盖的是虚表中的函数指针。
🧊因此编译器在实际调用时调用虚函数表里的指针,从而达到多态的效果。
🧊由于一个类的各个对象的结构并不相同,因此同一类的各个对象都共用一张虚表,且虚表无法被更改。

🧊根据这个性质,我们便猜测虚表可能是存在代码段之中,现在写一个代码来验证一下。
int main()
{
int i = 5;
int* p = new int;
static int s = 6;
const char* pc = "alpaca";
printf("栈的地址 %p\n", &i);
printf("堆的地址 %p\n", p);
printf("静态区的地址 %p\n", &s);
printf("常量区的地址 %p\n", pc);
A a;
printf("对象虚表的地址 %p\n", *(VF_PTR**)&a);
}
🧊显然,虚表的地址与常量区的地址最为接近,验证了我们上面的猜想。
多态的调用路径


🧊可以看到,虽然父类和子类都进行了 call 的操作,但最终跳转的函数却不相同。
🧊正是因为,通过虚函数的重写将虚表中的指针替换掉,当编译器去虚表中查找时便直接调用了该指针,从而达到了多态的效果。
多继承与多态
class A
{
public:
virtual void func()
{
cout << "this is A" << endl;
}
protected:
int _a = 3;
};
class B
{
public:
virtual void func()
{
cout << "this is B" << endl;
}
protected:
int _b = 4;
};
class C : public A, public B
{
public:
virtual void func()
{
cout << "this is C" << endl;
}
protected:
int _c = 5;
};
🧊当多态遇上多继承,打开内存窗口,根据结构可以分出几个部分。
🧊首先前两行就是 C 中的 A 类数据,接下来两行便是 C 中的 B 类数据,最后便是 C 的成员变量。
🧊而 A 和 B 类数据中各有一个虚表指针,不难猜出有多继承了几个类那么类中就有几个虚表。


🧊在之前单继承的情况下我们没有注意到,我们都说只要是虚函数就会将其加入到虚表中,那现在我们有两个虚表该加到哪个虚表中呢?


🧊可以看到第一个虚表中明显增加了一个指针,而第二个虚表中并未增加,由此我们便可得知子类增加虚函数之后默认加入到第一个虚表中。
🧊这时候,细心的我们又注意到了,明明调用的是同一个函数,而虚表中的地址却不同。

🧊这个操作我们只能再次使用查看汇编的方法进行解答。

🧊很明显,调用第二个函数时比起第一个函数多进行了几次的跳转,而唯一的区别就是在这个过程中进行了 sub 操作。
🧊sub 表示减,而 ecx 一般用于存储 this 指针,再结合对象的内存空间进行解析

🧊不难看出,this 指针从指向第二个虚表进行 8 字节的修正,便能够指向第一个虚表。
🧊因此,这个调用过程本质上是对 this 指针进行修正最后调用的还是第一个虚表中的函数指针。
🧊好了,今天 【C++】多态 的相关内容到这里就结束了,如果这篇文章对你有用的话还请留下你的三连加关注。

601





