您的位置:首页 > 教育 > 锐评 > C++多态

C++多态

2025/2/26 6:57:45 来源:https://blog.csdn.net/2302_80245587/article/details/142218364  浏览:    关键词:C++多态

多态是什么

根据字面意思,多态是指的多种形态,多态分为编译时多态(静态多态)和运⾏时多态(动态多态),这⾥我们重点讲运⾏时多态,编译时多态(静态多态)和运⾏时多态(动态多态)。编译时多态(静态多态)主要就是函数重载和函数模板,他们传不同类型的参数就可以调⽤不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时⼀般归为静态,运⾏时归为动态。

运⾏时多态,具体点就是去完成某个⾏为(函数),可以传不同的对象就会完成不同的⾏为,就达到多种形态,
就拿我们日长生活中的坐公交车,坐公交车分为,老人票,成人票,和学生票,他们的价格是不一样的,这里我们把公交车当成一个函数,老人,儿童,学生,这些就相当于不同的对象,我们传相应的对象到这个函数,就会产生不同的行为,就达到多态的目的

多态的定义和实现

多态是⼀个继承关系的下的类对象,去调⽤同⼀函数,产⽣了不同的⾏为,我们把坐公交车的通常为person,student是由person继承下来的 ,person坐车全票,student坐车半价,old man坐车免费

实现多态的必要条件

必须指针或者引⽤调⽤虚函数 •
被调⽤的函数必须是虚函数。

说明:要实现多态效果,第⼀必须是⽗类的指针或引⽤,因为只有⽗类的指针或引⽤才能既指向⽗⼦类对象;第⼆⼦类必须对⽗类的虚函数重写/覆盖,重写或者覆盖了,⽗⼦类才能有不同的函数,多态的不同形态效果才能达到。
为什么只有⽗类的指针或引⽤才能既能指向⽗类对象,又能指向⼦类对象?

这是因为,这里从子类赋值给父类,有一个切片的过程会把父类没有的给去掉就比如父类是person
子类是penson+student,这里就会把student给切掉,这里其实是一个特殊处理,它中间是没有产生临时变量的,也不会发生类型转换,它只是把这个地址给给了你的父类的指针。子类的属性大于父类的属性,这里和多态的原理有关,在构成多态的两个基类和派生类中,基类和派生类都有一个虚函数表指针,这个表里面存放的是两个类重写的虚函数,基类里面存放基类的,派生类存放派生类重写基类的虚函数地址(派生类里面还包含基类的虚函数地址,自己的虚函数地址),把派生类赋值给基类会发生切片,把单独属于派生类的那一部分去掉,其实这里子类就相当于一个特殊的基类,在构成多态的前提下,在把基类或者派生类创建的对象赋值给一个基类的指针时,程序在运行时会根据虚函数表里面的指针,基类找基类的,派生类找派生类的,指向找到对应的重写函数,这也是为什么只有⽗类的指针或引⽤才能既能指向⽗类对象,又能指向⼦类对象下面说多态的时候会具体说明。
在这里插入图片描述

要了解怎么构成多态必须知道一个东西叫做虚函数

虚函数

类成员函数前⾯加virtual修饰,那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修 饰。

虚函数的重写覆盖

虚函数的重写/覆盖:⼦类中有⼀个跟⽗类完全相同的虚函数(即⼦类虚函数与⽗类虚函数的返回值类
型、函数名字、参数列表完全相同),称⼦类的虚函数重写了⽗类的虚函数。重写指重写虚函数里面的内容!!!

注意:在重写⽗类虚函数时,⼦类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后
⽗类的虚函数被继承下来了在⼦类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使
⽤。
下面我们用一个代码看看,是怎样实现虚函数的

# include<iostream>
using namespace std;
class person
{
public:virtual void take_bus(){cout << "全票" << endl;}
};
class student : public person
{
public:virtual void take_bus(){cout << "半价" << endl;}
};
void func(person* who)
{
// 这⾥可以看到虽然都是Person指针who在调⽤take bus
// 但是跟who没关系,⽽是由who指向的对象决定的。who->take_bus();
}
int main()
{person p1;student p2;func(&p1);func(&p2);return 0;
}

这里我们的基类里面的函数就定义为虚函数,子类里面有一个同样的函数(参数,名字,返回类型都一样)他们就构成多态

在这里插入图片描述

协变

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

# include<iostream>
using namespace std;
class person
{
public:virtual person* take_bus(){cout << "全票" << endl;}
};
class student : public person
{
public:virtual student* take_bus(){cout << "半价" << endl;}
};

析构函数的重写

⽗类的析构函数为虚函数,此时⼦类析构函数只要定义,⽆论是否加virtual关键字,都与⽗类的析构
函数构成重写,虽然⽗类与⼦类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函
数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以⽗类的析构函数加了
vialtual修饰,⼦类的析构函数就构成重写。
为什么析构函数要重写?
我们来看看下面的代码

class A
{
public:virtual ~A(){cout << "~A()" << endl;}
};
class B : public A {
public:virtual~B(){cout << "~B()->delete:" << _p << endl;delete _p;}
protected:int* _p = new int[10];
};int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}

这里,首先这里的析构函数是肯定构成重新的因为在析构函数会被全部处理成destructor
如果我们把A的虚函数关键字给去掉了,不构成重写,那么这时就会内存泄漏,这时就只会去调用A类的析构函数,不会去调用B类的析构函数,程序就会异常退出
在这里插入图片描述
我们如果加了,程序就正常释放内存
在这里插入图片描述
那么为什么要释放两次A呢,这是因为B对象是A的派生类,B里面包含A,所以在释放完B之后回去释放A,在构成多态时,这也是为什么基类的析构函数建议写成虚函数。

override 和 final关键字

从上⾯可以看出,C++对函数重写的要求⽐较严格,但是有些情况下由于疏忽,⽐如函数名写错参数写
错等导致⽆法构成重载,⽽这种错误在编译期间是不会报出的,只有在程序运⾏时没有得到预期结果
才来debug会得不偿失,因此C++11提供了override,可以帮助⽤⼾检测是否重写。如果我们不想让⼦
类重写这个虚函数,那么可以⽤final去修饰。这样就像assert断言一样,可以为我们尽量避免不必要的问题发生,增加程序的健壮性。

抽象类

在虚函数的后⾯写上 =0 ,则这个函数为纯虚函数,纯虚函数不需要定义实现,只要声明即可 。包含纯
虚函数的类叫做抽象类,抽象类不能实例化出对象,如果⼦类继承后不重写纯虚函数,那么⼦类也是
抽象类。纯虚函数某种程度上强制了⼦类重写虚函数,因为不重写实例化不出对象。
就像下面这个例子

class Computer
{
public:virtual void laptop() = 0;
};
class thinkpad : public Computer
{
public:virtual void laptop(){cout << "thinkpad X61s" << endl;}};
class thinkbook :public Computer
{
public:virtual void laptop(){cout << "thinkbook16p" << endl;}
};
int main()
{Computer* p1 = new thinkpad;p1->laptop();Computer* p2 = new thinkbook;p2->laptop();return 0;
}

多态的原理

说到多态的原理,我们先来看看这道题目

class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
protected:
int _b = 1;
char _ch = 'x';
};
int main()
{
Base b;
cout << sizeof(b) << endl;
return 0;
}

这道题目是求这个类对象的大小,我们知道类的大小和结构体对齐一样,这里是32位在vs下默认对齐数是四字节,我们知道在类对象中,只存放成员变量,不存放成员函数,(成员函数是一个函数指针是不需要存放的)在类中定义的成员函数,其代码是存储在程序的可执行映像的代码段中,而不是对象的内存中。每个对象共享这些函数的代码。
所以这里我们只需要计算成员变量,首先int b 占4个字节,char占一个字节,最终是占5个字节,由于需要对齐到4字节整数倍,所以是8字节
那我们运行一下这个程序
在这里插入图片描述
这里发现是12字节,这是为什么?这里就需要知道一个东西叫虚函数表指针了

虚函数表指针

上⾯题⽬运⾏结果12bytes,除了_b和_ch成员,还多⼀个__vfptr放在对象的前⾯(注意有些平台可能
会放到对象的最后⾯,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代
表function)。⼀个含有虚函数的类中都⾄少都有⼀个虚函数表指针,因为⼀个类所有虚函数的地址要
被放到这个类对象的虚函数表中,虚函数表也简称虚表。
我们把监视器打开,就能看到在我们创建的类对象的最前面,有一个虚表指针,这个虚表指针其实是一个函数指针数组,一个数组里面存放指针,一个指针指向一个对应的函数
在这里插入图片描述

多态的原理

多态是如何实现的?
我们还是以这个代码为例子

# include<iostream>
using namespace std;
class person
{
public:virtual void take_bus(){cout << "全票" << endl;}
protected:int b = 0;
};
class student : public person
{
public:virtual void take_bus(){cout << "半价" << endl;}
protected:int a = 0;
};
class old_man :public person
{
public:virtual void take_bus(){cout << "免费" << endl;}
protected:int c = 0;
};
void func(person* who)
{who->take_bus();
}
int main()
{person p1;student p2;old_man p3;func(&p1);func(&p2);func(&p3);return 0;
}

从底层的⻆度Func函数中who->take_bus(),是如何作为who指向Person对象调⽤Person::take_bus,
who指向Student对象调⽤Student::take_bus的呢?通过下图我们可以看到,满⾜多态条件后,底层
不再是编译时通过调⽤对象确定函数的地址,⽽是运⾏时到指向的对象的虚表中确定对应的虚函数的
地址,这样就实现了指针或引⽤指向⽗类就调⽤⽗类的虚函数,指向⼦类就调⽤⼦类对应的虚函数。
这是运行结束时监视窗口的地址
在这里插入图片描述

这是运行开始时的地址
这也可以证明满足多态的函数是动态绑定的,而不是静态绑定,是在程序运行的时候确立了函数的地址,而不是在编译的时候就确定了
在这里插入图片描述
我们在来看一个普通函数的地址变化
在这里插入图片描述
从调试开始到结束地址一直没变化,说明是静态绑定的,在编译的时候就确定了函数的地址
我们可以来画图具体看看运行时,虚表函数是怎么确定所调用的函数地址的,构成多态时,我们发现在运行时,虚表函数会存放属于此类的基于基类重写的函数地址,当运行到此函数时,编译器会自动到虚表函数中去找到对应的函数地址,在去call这个函数地址,完成调用
在这里插入图片描述
我们可以具体画图看看调用的过程

在这里插入图片描述
指向谁调用谁,根据指向对象的虚函数表的地址,找到对应的函数,进行调用,这就是多态的原理

动态绑定与静态绑定

对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤
函数的地址,叫做静态绑定。

• 满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数
的地址,也就做动态绑定。
我们先来看看满足多态情况下面的汇编
在这里插入图片描述
在来看看不满足多态情况下的汇编
在这里插入图片描述
我们发现在不满足多态的情况下,程序在编译时就确定了地址,而不是运行时,而满足多态的情况下是在运行时确定地址的,而不是在编译时

虚函数表

⽗类对象的虚函数表中存放⽗类所有虚函数的地址。

⼦类由两部分构成,继承下来的⽗类和⾃⼰的成员,⼀般情况下,继承下来的⽗类中有虚函数表指

针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的⽗类部分虚函数表指针和⽗类

对象的虚函数表指针不是同⼀个,就像⽗类对象的成员和⼦类对象中的⽗类对象成员也独⽴的。 •

⼦类中重写的⽗类的虚函数,⼦类的虚函数表中对应的虚函数就会被覆盖成⼦类重写的虚函数地址

⼦类的虚函数表中包含,⽗类的虚函数地址,⼦类重写的虚函数地址,⼦类⾃⼰的虚函数地址三个 部分。

同类型的对象虚表共用,不同类的对象虚表各自独立
我们以下面的代码作为示例

class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
};
class Derive : public Base
{
public:
// 重写⽗类的func1
virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func1" << endl; }void func4() { cout << "Derive::func4" << endl; }
protected:
int b = 2;
};
int main()
{
Base b;
Base g;
Derive d;
return 0;
}

我们打开监视窗口发现,他们的地址是一样的,这样做会更节省空间,几个相同的对象都指向一个共用的虚函数表
在这里插入图片描述

虚函数表里面的内容

我们可以用调试窗口看看,虚函数表里面的函数究竟是怎么存放的,我们还是根据下面的函数看看

class person
{
public:virtual void take_bus(){cout << "全票" << endl;}virtual void func(){;}
protected:int b = 0;
};
class student : public person
{
public:virtual void take_bus(){cout << "半价" << endl;}
protected:int a = 0;
};
void func(person* who)
{who->take_bus();
}
int main()
{person p1;student p2;func(&p1);func(&p2);return 0;
}

在这里插入图片描述
同样p2也是这样的道理,我们也可以来试试
在这里插入图片描述
在这里插入图片描述

引用和指针是如何实现多态的

根据原则多态必须是基类的引用或者指针来接受派生类对象才行,这是因为用一个基类的指针来接受派生类的对象,会发生切片,如下图所示
在这里插入图片描述
这时用基类的一个指针变量接受派生类,基类指针变量里面的虚函数表就是派生类里面的虚函数表 一样会有fvptr,但这里是子类的fvptr,再去找这个虚函数表里面的函数指针,这时函数指针就是派生类的虚函数指针了,有了这个地址,便可以去call这个函数的地址,来执行这个函数
这样就完成了多态
这就是多态的原理

接下来我们看道题目

以下程序输出结果是什么()
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;
}

这道题选B,为什么,这个答案b->1,可以这样理解,首先用一个b类型的指针p来接收创建一个b类型的对象,然后在去调用test()函数,因为B是a的派生类,由于test是虚函数,b类里面没有就去调a的,在a类的test函数中,func是虚函数func()的调用将被动态绑定到B类中的func()函数(因为这里是B类型的指针p去调用的),但由参数于在编译的时候就确定了(这里是静态绑定), 所以在调用test的时候参数是a里面的参数,但函数内容是b里面的内容,所以选B。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com