当前位置: 首页 > news >正文

多态的学习与了解

目录

1.多态的概念

2.多态的实现

3.虚函数

虚函数的重写

题目练习

协变

析构函数的重写

重载重写隐藏

3.纯虚函数和抽象类

4.多态的原理

1.虚表

2.虚函数指针

总结:


1.多态的概念

   说人话就是在继承的基础上传不同的对象实现不同的功能。

多态分为静态多态和动态多态,静态多态又称编译时多态,就是编译时就确定调用哪个函数,例如函数重载传不同的参数调用不同的函数这在编译时就是确定的,动态多态又称运行时多态,就是在运行时根据实际对象才决定调用哪个函数。

2.多态的实现

1.必须是基类的指针或引用调用的虚函数。

2.被调用的虚函数完成了重写。

下面代码打印的是B的fun()。就是满足了多态的两个实现条件。

那么我们要反问一句为什么要必须调用基类的指针或引用呢?赋值不行吗?答案是不行,因为前面继承中我们学习过派生类给基类赋值会发生切片,直接把派生类部分给切掉然后进行拷贝构造进行赋值,无法调用派生类f()。

我们现在记住一句话叫做是根据对象的实际类型进行多态函数的调用。我们传引用它的类型还是派生类只不过我们不能访问派生类的派生类新增部分,但是如果我们去传值进行拷贝构造的话,它的类型就是基类而不再是派生类。

 

class A {
public:virtual void f() {cout << "A::f()" << endl;}
};class B : public A 
{
private:virtual void f() override {cout << "B::f()" << endl;}
};int main() {A* a = new B;a->f();return 0;
}

3.虚函数

下面我们来了解多态的实现的重要工具虚函数,在函数前面加virtual就是虚函数,它的作用就是来实现多态。

虚函数的重写

当派生类与父类的成员函数有同名时,该成员函数构成隐藏,当父类和父类的成员函数,它们的函数名相同,并且参数类型相同,返回值相同时,并且它们都是虚函数时啊,这时我们就构成了虚函数的重写。父类的虚函数是一个实现方式,子类的虚函数是一个实现方式,虚函数的复写是我们实现传不同对象调用不同函数的关键。

注意:正常情况下,我们父类和子类的虚函数都要加virtual,但是实际上,如果我们父类加虚函数关键字,子类不加,它仍然能够复写,但是如果父类没加虚函数关键字,但是子类加了,它就不能构成复写,这里是一个比较特殊的点。

题目练习

这个题目我们可以看出它是通过B类型的指针去调用test,B的test是从A中继承的,这时调用test又会调用func,但是它构成了多态所以我们调用B类的func,但是,这里重写仅仅是实现的重写,但是val仍然是父类的val。所以这道题选B,非常的坑。

以下程序输出结果是什么()
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
class A
{
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func();}
};
class B : public A
{
public:
void func(int val = 0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;p->test();
return 0;
}

协变

派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或引
⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。协变的实际意义并不⼤,所以们
了解⼀下即可。

析构函数的重写

在继承中虚构函数要进行重写,我们要对析构函数进行谨慎处理,核心问题在于:

当通过 基类指针删除派生类对象 时,如果基类的析构函数 不是虚函数,会导致:

  • 派生类的析构函数不被调用,仅调用基类的析构函数。

  • 派生类独有的资源(如动态内存、文件句柄)泄漏

下面我们用代码来解释一下。

如果我们不写析构函数的虚函数的话,它析构只会调用A的析构。核心问题再次强调一下:基类指针删除派生类对象,不写析构不会调派生类的析构。

using namespace std;class A {
public:virtual void f() {cout << "A::f()" << endl;}virtual ~A(){cout << "~A";}
};class B : public A 
{
private:virtual void f() override {cout << "B::f()" << endl;}virtual ~B(){cout << "~B";}
};int main() {A* a = new A;A* b = new B;delete a;delete b;return 0;
}

了解完析构函数继承要写多态后,有的人就要问了,主播主播构造函数和静态函数需不需要写呢?

这个问题我们下面来解答会更清晰一点!

重载重写隐藏

重载是在同一作用域内,函数名相同参数不同的函数构成重载,根据不同的参数进行调用。

重写是在不同作用域父类和子类同名,同参数,同返回值的虚函数(函数名前加virtual)进行传不同的对象调用不同的函数实现多态。

隐藏是在不同作用域,父类和子类成员同名父类成员在派生类中会被隐藏。

3.纯虚函数和抽象类

在虚函数后加=0构成纯虚函数,它没有实际的定义。包含纯虚函数的类叫抽象类。抽象类不能实例化出对象,派生类如果继承后不重写纯虚函数也会是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写的话还是抽象类,无法实例化出对象。

了解完多态的定义和概念后,我们下来了解多态的原理。

4.多态的原理

多态是依赖虚表和虚指针来实现的。

1.虚表

每个包含虚函数的类都含有一个虚表,存储虚函数的地址。

当派生类继承含义虚函数的父类时,如果派生类不重写虚函数,它的虚表的内容和父类的虚表内容一致,只不过会新增派生类中新增的虚函数。如果派生类重写父类的虚函数,重写的虚函数就会覆盖原有的父类的虚函数。

注意:即使派生类中不重写父类的虚函数,它的vptr指向的物理地址也不一样。一个类共享一个vptr。

虚函数表细则:

基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共⽤同⼀张虚表,不同类型的对
象各⾃有独⽴的虚表,所以基类和派⽣类有各⾃独⽴的虚表。
派⽣类由两部分构成,继承下来的基类和⾃⼰的成员,⼀般情况下,继承下来的基类中有虚函数表
指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基
类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴
的。
派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函
数地址。
派⽣类的虚函数表中包含,(1)基类的虚函数地址,(2)派⽣类重写的虚函数地址完成覆盖,派⽣类
⾃⼰的虚函数地址三个部分。
虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标
记。(这个C++并没有进⾏规定,各个编译器⾃⾏定义的,vs系列编译器会再后⾯放个0x00000000
标记,g++系列编译不会放)
虚函数存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函
数的地址⼜存到了虚表中。
虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,我们写下⾯的代码可以
对⽐验证⼀下。vs下是存在代码段(常量区)

2.虚函数指针

每个含有虚函数的类中有虚函数指针,指向虚函数表,我们通过这个指针来调用虚函数。

结合下图我们再结合我们继承的知识,我们来复盘一下虚函数调用的具体过程,__vfptr是指向虚表的指针,虚表里存储的虚函数的地址,如果我们父类含有虚函数,子类不对它进行重写,子类依旧会继承父类的虚函数并有自己独特的虚函数表,只不过它的定义和父类相同,如果我们对父类的虚函数进行重写,那么我们重写的虚函数会覆盖原有的虚函数。这样我们就可以调用重写的虚函数了。

当我们把派生类对象的引用或地址传给父类的引用或地址时,它不会发生切片,仅仅是我们不能通过它访问派生类新增的对象,但是__vfptr是可以访问的。且我们访问的是修改之后的__vfptr,我们借助代码再来理解一下。

“派生类对象会拥有自己的 __vfptr,它默认指向派生类的虚表。如果派生类重写了基类的虚函数,则虚表中对应条目会更新为派生类的函数地址;否则保留基类的实现。

注意:这里的虚表都是独立的并不是基类的虚表和派生类的虚表共享一份,他们之间是独立的独立的独立的!!!

再借鉴下面这句话来理解一下,

派⽣类由两部分构成,继承下来的基类和⾃⼰的成员,⼀般情况下,继承下来的基类中有虚函数表
指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基
类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴
的。

这里就是虚函数的调用是根据其实际类型进行调用的。

class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:
string _name;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-打折" << endl; }
private:
string _id;
};
class Soldier: public Person {
public:
virtual void BuyTicket() { cout << "买票-优先" << endl; }
private:
string _codename;
};
void Func(Person* ptr)
{
// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
ptr->BuyTicket();
}
int main(){
// 其次多态不仅仅发⽣在派⽣类对象之间,多个派⽣类继承基类,重写虚函数后
// 多态也会发⽣在多个派⽣类之间。
Person ps;
Student st;
Soldier sr;
Func(&ps);
Func(&st);
Func(&sr);
return 0;
}

总结:

相信到了这里大家就该大致了解多态和继承了,继承的基础上我们定义了多态来实现编程的方便,继承是父类的成员继承给派生类,但是它们之间的成员并没有关联,父类有int a,派生类继承int a,派生类中的a和父类的a实际是没有关系的,因为本质上它们是属于两个不同的类,多态我们引入了虚函数指针和虚函数表,虚函数表中的虚函数继承了父类的虚函数,如果我们再派生类中对他们进行重写,重写的虚函数就会覆盖原有的虚函数表中对应的虚函数。

附言:上文中我们提到了构造函数和静态函数不可以虚函数化,现在我们可以给出解释,构造函数不可以虚函数化是因为函数前面加virtual会调用虚函数指针,但是虚函数指针是构造中生成的,这会发生矛盾,编译无法通过,至于静态函数不可以虚函数化,静态函数不属于某个类的对象,它是属于整个类,它不通过对象调用,没有this指针,前面我们说过多态就是根据不同的对象调用来实现多态,但是静态函数不通过对象调用,所以我们的静态函数不可以虚函数化!!!

如有错误,请指正,博主多有感谢!!!

相关文章:

  • Java【网络原理】(4)HTTP协议
  • 5.1 掌握函数定义与参数传递的奥秘
  • RNN的理解
  • 小刚说C语言刷题——1049 汉译英
  • 1222222
  • Linux 动、静态库的实现
  • 滑动时间窗口实现重试限流
  • 解决模拟器打开小红书设备异常问题
  • SpringBoot入门实战(第一篇:环境准备和项目初始化)
  • 深度可分离卷积与普通卷积的区别及原理
  • 【数据库】事务
  • 深入剖析Java并发编程原理:从底层到实践
  • C++move的作用和原理
  • LeetCode每日一题4.20
  • 模拟实现memmove,memcpy,memset
  • iPhone 13P 换超容电池,一年实记的“电池循环次数-容量“柱状图
  • ebpf: CO-RE, BTF, and Libbpf(三)
  • 标准的JNI (Java Native Interface) 加载函数 JNI_OnLoad
  • VLC搭建本机的rtsp直播推流和拉流
  • 如何成为Agent工程师:学习路径、核心技能与职业规划
  • A股低开高走,震荡收涨:两市成交10414亿元,4360股收涨
  • 延安市委副书记马月逢已任榆林市委副书记、市政府党组书记
  • 牛市早报|国常会:要持续稳定股市,4月LPR今日公布
  • 海南医科大学继续开展部门正职竞聘上岗,致力营造“谁有本事谁来”
  • 人大书报资料中心与中科院文献中心共筑学科融合创新平台
  • 世遗X时尚,七匹狼这场大秀秀出中国文化独特魅力