【C++】多态:深度剖析(多态、虚函数、抽象类、底层原理)

温馨提示:在观看本文前确保已经了解了C++中继承的相关知识,若不了解,可以查看我的这篇文章进行学习:【C++】继承:深度剖析-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/2301_80555259/article/details/141829528?spm=1001.2014.3001.5501


目录

一.多态的概念

二.多态的定义和实现

三.虚函数

1.概念

2.重写

3.重写的两个例外

3.1 协变(返回类型不同)

3.2 析构函数的重写(函数名不同)

4.C++11:override和final

5.重载、重写、隐藏的对比

四.抽象类

五.虚函数原理

六.多态的原理


一.多态的概念

通俗来讲,多态就是拥有多种状态,父子对象完成相同的任务却会产生不同的结果;例如普通人和学生去买票(进行相同的任务),普通人(父类)是全价,而学生(子类)是半价,这就是多态的一种现实体现。

从程序角度来讲,多态是指同一个函数名可以根据调用对象的不同而具有不同的实现。多态分为两种类型:编译时多态(静态多态)和运行时多态(动态多态) 

  • 编译时多态:通过函数重载运算符重载实现,是在编译阶段确定函数调用
  • 运行时多态:通过虚函数和继承实现,是在运行阶段确定函数调用

二.多态的定义和实现

多态的实现通常依赖于虚函数。在基类(父类)中声明虚函数,然后在派生类(子类)中进行重写(覆盖)。通过基类指针或引用调用虚函数时,将根据对象的实际类型调用相应的派生类函数

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

  1. 必须通过基类的指针或者引用调用函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

那么,什么是虚函数?接下来就对虚函数进行深度解析


三.虚函数

1.概念

虚函数是在基类中使用关键字virtual进行声明的成员函数,它的存在允许在派生类中进行该函数的重写(覆盖)。

class Person
{
public:
	//虚函数
	virtual void BuyTicket() { cout << "买票-全价"; }
};

2.重写

虚函数的重写(Override)是指在派生类中重新实现上述基类中已经声明为虚函数的函数。在进行重写时,子类中的虚函数的返回类型、函数名、参数列表类型必须与基类中虚函数完全一致(参数名称、缺省值可以不相同)

注意:在重写基类虚函数时,派生类的虚函数可以不加virtual关键字,这依然构成重写,因为继承后基类的虚函数在派生类中依然保持虚函数属性,但这种写法其实不规范,不建议这样书写

3.重写的两个例外

3.1 协变(返回类型不同)

派生类重写虚函数其返回类型为派生类指针或引用,而基类虚函数其返回类型为基类指针或引用时,此时尽管两虚函数返回类型不同,也依然是虚函数重写,称为协变

class A
{
public:
    //返回类型为基类指针
	virtual A* f() 
	{ 
		return new A; 
	}
};

class B:public A
{
public:
    //返回类型为派生类指针
	virtual B* f()
	{
		return new B;
	}
};

3.2 析构函数的重写(函数名不同)

如果基类的析构函数为虚函数,那么派生类的析构函数只要定义,无论是否加virtual关键字都构成重写,尽管函数名字不同。这里其实是编译器做了特殊处理,编译后析构函数的名称统一处理为destructor

至于为什么要这样处理,可以用以下样例解释:

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

class B :public A
{
public:
	//不加virtual也依旧构成重写
	~B()
	{
		cout << "~B()" << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};

int main()
{
	A* p1 = new A;
	A* p2 = new B;
	delete p1;
	delete p2;
}

若基类A和派生类B不构成虚函数重写的话,那么此处调用delete p2时,p2是基类A的指针,只会调用A的析构,从而没有调用B的析构,此时B内申请的资源int* _p就没有释放,造成了内存泄漏,如下图,为了防止这种情况,才使用继承中的虚函数析构

4.C++11:override和final

1.override

派生类虚函数的格式有严格要求,返回类型、函数名、参数类型都必须完全相同,但这在编译时是不会报错的,哪怕函数名打错了一个字母也不会报错让你发现,这时就需要override来检查是否正确地重写了该函数了,例如:

class Car 
{
public:
    virtual void Drive() {}
};

class Benz :public Car 
{
public:
    virtual void Drive() override {}
};

此时如果函数名写错就会在编译阶段报错了

2.final

final用于在派生类中阻止对虚函数的进一步重写,或者在类定义中阻止类被继续派生

//阻止该类继续派生,该类就是"最终"(final)类
class Base final {
    // ...
};
//阻止该虚函数被继续重写
class Base {
public:
    virtual void f() final 
    {
        // ...
    }
};

5.重载、重写、隐藏的对比


四.抽象类

在虚函数的后面加上 = 0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化出对象,只有其派生类重写纯虚函数后,派生类才能实例化出对象。也就是说,纯虚函数规定了派生类必须重写。

 实际上很好理解,例如把动物当做一个抽象类,你能实例化找出一个“动物”吗?动物是个抽象的概念,无法实际存在,只有将其重写为猫、狗等实际存在的动物后,才能实例化地找到。


五.虚函数原理

在调试一个含有虚函数的类Base时,我们发现该类除了成员变量_base外,还包含一个变量_vfptr,这个变量就是虚函数表指针(virtual function pointer)

 一个含有虚函数的类中至少都有一个虚函数表指针,该指针指向的是一个数组,该数组是储存函数指针的。也就是说,该指针指向一个函数指针数组。该数组存放虚函数的地址,不同的派生类该表也不同。

 通过一下例子,我们可以再详细谈谈这整个过程

class Base
{
public:
    virtual void Func1()
    {
        cout << "Func1()" << endl;
    }

    virtual void Func2()
    {
        cout << "Func2()" << endl;
    }
private:
    int _base = 1;
};

class Derive :public Base
{
public:
    virtual void Func1()
    {
        cout << "Func111()" << endl;
    }

    //virtual void Func2()
    //{
    //    cout << "Func222()" << endl;
    //}
protected:
    int _derive = 2;
};

int main()
{
    Base b;
    Derive d;

    return 0;
}

通过该结果可以发现,派生类Derive的虚函数表与基类Base是不同的,Derive中对虚函数Func1进行了重写,但没有对Func2重写,这就导致了d类的虚表中第一个Func1的地址被覆盖了,而第二个Func2没有被覆盖,和基类相同。因此继承时,派生类是继承了基类的虚函数表的,不过当虚函数重写时就进行函数地址的覆盖而已。

顺带一提,如果在Derive中在新写一个虚函数,vs的调试窗口上d类的虚表中是看不见的,但可以通过内存窗口去观察


六.多态的原理

class Person {
public:
    virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student : public Person {
public:
    virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

void Func(Person& p)
{
    p.BuyTicket();
}

int main()
{
    Person man;
    Func(man);

    Student Jack;
    Func(Jack);

    return 0;
}

根据下图的红色箭头可以发现,p是指向man对象时,p.BuyTicket在虚表中找到的是Person::BuyTicket,而蓝色箭头,p是指向Jack对象时,p.BuyTicket在虚表中找到的就是Student::BuyTicket,这不是在编译时确定的,而是运行以后到对象中找到的。

当然这里的核心依然是切割,编译器看到的是父类,不过是指向子类时是切割过去的而已,里面的虚表也是被子类覆盖后的结果。

好了,多态的知识解析就是这些,最后再来一道很容易错的习题来结束吧

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

答对了吗?

解析:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值