【C++】多态相关(多态的概念、定义及原理,虚函数与虚函数表)

目录

01.多态的概念

02.多态的定义

虚函数

虚函数的重写(覆盖)

 虚函数重写的两个例外

override和final

抽象类

03.多态的原理

虚函数表


01.多态的概念

多态是面向对象编程中的一个核心概念,它允许不同类的对象通过相同的接口调用来执行不同的行为。通俗点来讲,就是多种形态,去完成某个行为,当不同的对象去完成时会产生出不同的状态。

         比如说我们的家电遥控器:一只遥控器可以控制电视、空调和音响等,按下遥控器上的电源键,每个设备的反应会不同——电视会打开或关闭屏幕,空调会启动或停止制冷,而音响会开始或停止播放音乐,这里“电源键”就是一个多态的接口,通过这个接口,每种设备执行其特定的行为。

        在比如买票这件事情,普通成年人买票时,是全价买票;学生和儿童买票时,是半价买票;军人买票时,是优先买票。“买票”这个行为也可以看成是一个多态接口。

02.多态的定义

在继承中构成多态需要有两个条件:

1.必须通过基类的指针或者引用调用虚函数

2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

 

为什么这里student类对象作为参数传入Func函数中时,被强制类型转换成了person类型,但是调用的函数还是student版本的呢?

这是因为Buyticket函数被声明为虚函数,所以在调用people.Buyticket()时,将根据对象的实际类型决定调用哪个版本的 ‘Buyticket’ 。在调用Func(Lee)时,虽然people是一个person类型的引用,但它指向的是一个student对象,由于Buyticket是虚函数,所以调用的是student类的版本,输出“student:半价”。

注:Func函数中参数person的静态类型是person,但不能是student,这是因为person是基类,而student是派生类,派生类对象可以强转成基类,但是基类对象不能强转成派生类。

虚函数

虚函数:被virtual修饰的类成员函数称为虚函数。

class person {
public:
	virtual void Buyticket() {
		cout << "person:全价" << endl;
	}
};

虚函数的重写(覆盖)

派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同)称子类的虚函数重写了基类的虚函数。

class person {
public:
	virtual void Buyticket() {
		cout << "person:全价" << endl;
	}
};

class student : public person {
public:
	virtual void Buyticket() {
		cout << "student:半价" << endl;
	}
};

注:在重写基类虚函数的时候,派生类的虚函数不加关键字virtual,也可以构成重写(覆盖),这是因为基类的虚函数在派生类中继承了下来,依旧保持虚函数的属性,但建议还是规范写法。

重载、重写(覆盖)、重定义(隐藏)

  • 重载

    • 函数名相同但参数列表不同(参数的数量、类型或顺序不同)。
    • 发生在同一个作用域内(通常是同一个类中)。
    • 编译时决定。
  • 重写(覆盖)

    • 派生类中重新定义基类中的虚函数
    • 函数名、参数列表和返回类型必须相同。
    • 支持运行时多态。
  • 重定义(隐藏)

    • 派生类中定义一个与基类中同名的非虚函数或变量。
    • 基类的成员在派生类中被隐藏。
    • 编译时决定。

下面用一个例子来展示这三种概念: 

class A{
public:
	void f1(int a) { cout << "A::f1( " << a << " )" << endl; }
	void f1(int a, int b) { cout << "A::f1( " << a << " , " << b << " )" << endl; }
	virtual void f2() { cout << "A::f2()" << endl; }
	void f3() { cout << "A::f3()" << endl; }
};

class B : public A{
public:
	virtual void f2() { cout << "B::f2()" << endl; }
	void f3() { cout << "B::f3()" << endl; }
};

int main()
{
	B b1;
	A& a1 = b1;
	a1.f1(1);
	a1.f1(1, 2);
	a1.f2();
	a1.f3();
}

运行结果如下:

 

在基类A与派生类B的定义中,f1函数被重载,f2被重写,而f3则被重定义。

此时指针a1的静态类型是A,而动态类型是B,在调用f1时,调用的是基类的重载函数,没有问题;调用f2时由于f2是个虚函数且在派生类中被重写,所以基类引用a1在运行时调用实际对象类型B中的虚函数实现;f3并不是虚函数,不支持运行时多态,在编译时调用基类A的f3方法。

 虚函数重写的两个例外

1.协变:

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,称为协变

class A{};
class B : public A {};

class Person {
public:
    virtual A* f() {return new A;}
};
class Student : public Person {
public:
    virtual B* f() {return new B;}
};

 2.析构函数的重写

基类的析构函数定义成虚函数,此时虽然基类与派生类析构函数名字不同(都是各自的类名)看起来违背了重写的规则,但是也构成重写,可以理解成,编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

class Person {
public:
    virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
    virtual ~Student() { cout << "~Student()" << endl; }
};

override和final

 在C++11中,还可以使用override和final关键字,帮助用户检测虚函数是否重写

1.override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

class Car{
public:
    virtual void Drive(){}
};
class Benz :public Car {
public:
    virtual void Drive() override {cout << "Benz-舒适" << endl;}
};

2.final:修饰虚函数,表示该虚函数不能再被重写

class Car
{
public:
    virtual void Drive() final {}
};
class Benz :public Car
{
public:
    virtual void Drive() {cout << "Benz-舒适" << endl;}
};

抽象类

在虚函数的后面写上 =0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现了接口继承。

class Car
{
public:
    virtual void Drive() = 0;
};
class Benz :public Car
{
public:
    virtual void Drive(){
        cout << "Benz-舒适" << endl;
    }
};
class BMW :public Car
{
public:
    virtual void Drive(){
        cout << "BMW-操控" << endl;
    }
};
void Test()
{
    Car* pBenz = new Benz;
    pBenz->Drive();
    Car* pBMW = new BMW;
    pBMW->Drive();
}

03.多态的原理

虚函数表

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};


int mian()
{
	Base b;
	return 0;
}

思考一下:sizeof(Base)是多少?

通过观察测试我们发现,b对象是8bytes,除了_b成员,还多一个_vfptr放在对象的前面,对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类都至少有一个虚函数表指针,因为虚函数的地址要被放在虚函数表中,虚函数表简称虚表。

那么在派生类中,虚函数是如何存放的呢?

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	cout << "Derive::Func1()" << endl;
	return 0;
}

我们增加了一个派生类Derive继承Base,其中Derive重写Base的函数Func1,Base中还有虚函数Func2和普通函数Func3。

此时基类对象 b和派生类对象 d的存储结构如下:

我们观察到:派生类对象d也有一个虚表指针,d对象由两部分构成,一部分是基类继承下来的成员(包含基类的虚表指针),另一部分是自己的成员。

        我们发现b对象和d对象虚表是不一样的,我们可以看到,第一个虚函数的地址不一样,而第二个虚函数的地址一样,这是因为在Derive中,Func1函数完成了重写(覆盖),此时基类虚函数表中对应的虚函数指针就被重写后的虚函数指针覆盖了,Func2函数没有被重写,但也作为虚函数被继承下来了,所以放在了虚函数表中,而Func3函数不是虚函数,即使被继承了也不会放进虚函数表中。

注意:要清楚一个概念:虚表中存放的是虚函数指针,而不是虚函数本身,另外对象中存的也是虚函数指针,而不是虚表,在vs下,虚表是存放在代码段中的。

以上就是多态相关的知识的整理了,欢迎在评论区留言,觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~😉

  • 13
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
C++中的多态性是通过虚函数实现的。在含有虚函数的类中,编译器会自动添加一个指向虚函数的指针,这个指针通常称为虚函数指针。虚函数是一个存储类的虚函数地址的数组,每个类有一个对应的虚函数。当一个类对象被创建时,会自动分配一个指向它的虚函数的指针。 虚函数指针的大小和虚函数的大小都与具体实现相关。在一般情况下,虚函数指针的大小为4或者8个字节,虚函数的大小取决于类中虚函数的个数。 以下是一个模拟实现: ```c++ #include <iostream> using namespace std; class A { public: virtual void func1() { cout << "A::func1" << endl; } virtual void func2() { cout << "A::func2" << endl; } }; class B : public A { public: virtual void func1() { cout << "B::func1" << endl; } }; int main() { A* a = new A(); B* b = new B(); cout << "size of A: " << sizeof(A) << endl; cout << "size of B: " << sizeof(B) << endl; cout << "size of a: " << sizeof(a) << endl; cout << "size of b: " << sizeof(b) << endl; a->func1(); a->func2(); b->func1(); b->func2(); delete a; delete b; return 0; } ``` 输出结果: ``` size of A: 8 size of B: 8 size of a: 8 size of b: 8 A::func1 A::func2 B::func1 A::func2 ``` 在上面的代码中,我们定义了两个类A和B,其中B继承自A。类A和B都含有虚函数,因此编译器会为它们添加虚函数指针。在main函数中,我们创建了一个A类对象和一个B类对象,并输出了它们的大小以及指针的大小。接着我们调用了每个对象的虚函数,可以看到B对象的func1()覆盖了A对象的func1(),而A对象的func2()没有被覆盖。最后我们删除了这两个对象,避免内存泄漏。 需要注意的是,虚函数指针的大小和虚函数的大小是不确定的,取决于具体实现。此外,虚函数指针通常被放在对象的开头,因此虚函数通常被放在内存中较靠前的位置。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谁在夜里看海.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值