C++多态与多态原理

虚函数

虚函数是为了实现多态而设定的。而且只能在继承体系中成员函数可以设置为虚函数,虚函数是什么样子的呢?

在这里插入图片描述

如上图,我们fun()函数即为虚函数。同时只有我们的成员数为虚函数才能够形成多态,在继承章节我们提到一个隐藏的概念,那么在多态中我们还有一个重写(重写虚函数的实现)的概念。

隐藏只要求我们基类和派生的函数名相同即可。

重写则需要函数返回值,函数名,函数参数类型(只是类型相同即可都要相同才可以构成多态。而重写有什么用呢?也是为了形成多态

多态

什么是多态呢?本质就是:不同人干同一件事,具有不同的效果。如下段代码:

class Person
{
public:
	virtual void fun()
	{
		cout << "买票-全价" << endl;
	}
};
class Adult : public Person
{
public:
	virtual void fun()
	{
		cout << "买票-全价" << endl;
	}
};
class Child : public Person
{
public:
	virtual void fun()
	{
		cout << "买票-半价" << endl;
	}
};
class Soldier : public Person
{
public:
	virtual void fun()
	{
		cout << "买票-优先" << endl;
	}
};

void Buy_tickets(Person& p)
{
	p.fun();
}
int main()
{
	Adult _adu;
	Child _chl;
	Soldier _sol;

    //调用买票这个函数
	Buy_tickets(_adu);
	Buy_tickets(_chl);
	Buy_tickets(_sol);
	return 0;
}

上段代码都是成员函数为虚函数的情况下,先来看一下我们成员函数不是虚函数的运行结果:

在这里插入图片描述

再来看一下类成员函数为虚函数的运行结果:

在这里插入图片描述

如上图我们可以总结多态的形成条件:

  1. 虚函数重写
  2. 父类的指针或引用调用

这就是多态,根据指针指向的内容的类型来决定调用谁的函数,就是不同人干一件事有不同的效果。同时上面两个条件必须严格遵守,缺一个都不能构成多态。

协变

上文提到,我们的重写必须返回值,函数名,参数类型都要相等,但是这里有一个例外,就是我们的返回值如果是继承关系的就可以不一样也可以形成多态。如下图:

在这里插入图片描述

这里的返回值是自己继承体系的也可以。

多态原理

那么上述指向谁调用谁是如何实现的呢来看一下代码:

class Base
{
public:
	virtual void fun1() { cout << "virtual void fun1() - Base" << endl; }
	virtual void fun2() { cout << "virtual void fun2() - Base" << endl; }
	void fun3() { cout << "void fun3() - Base" << endl; }
};
class Derive : public Base
{
public:
	virtual void fun1() { cout << "virtual void fun1() - Derive" << endl; }
	virtual void fun2() { cout << "virtual void fun2() - Derive" << endl; }
	void fun3() { cout << "void fun3() - Derive" << endl; }
};
void Func(Base& b)
{
	b.fun1();
	b.fun2();
	b.fun3();
}
int main()
{
	Base b;
	Derive d;

	Func(b);
	cout << endl << endl;
	Func(d);
	return 0;
}

虚表

当我们有定义虚函数的时候,我们在基类或者派生类对象中会默认生成一个虚表。它存储着我们虚函数编译后的指令,当我们使用对应的虚函数的时候回去虚表中调用对应的虚函数。

在这里插入图片描述

如上图,我们虚表存储在对象的头四个节点上,它是一个函数指针数组的地址,所以我们的虚表本质是一个函数指针数组。而我们的虚函数和成员函数在同一个地方(代码段),编译器会把函数编译好之后的指令放入虚表中以供调用。

在这里插入图片描述

如上图,当我们派生类中重写的对应的虚函数的时候,派生类当中的虚存储的是派生类自己的虚函数,如果没有重写时则是基类的虚函数所以重写其实是一种特殊的隐藏,只不过重写的要求更加严格了。

动态绑定和静态绑定

函数调用的时候分为两种绑定方式:

  1. 静态绑定(编译时绑定)

普通成员函数

  1. 动态绑定(运行时绑定)

虚函数

在这里插入图片描述

如上图,对于我们的fun2()是我们实现的虚函数,我们的fun3()则不是虚函数,可以看到,fun2()的调用是在运行时才确定的,而我们fun3()则是直接就是call对应的地址。所以这也就是为什么我们多态可以实现指向谁调用谁的原因,因为虚函数的调用是运行时才确定的。

在这里插入图片描述

那么虚函数和成员函数是在代码段存着的,那虚表是在哪存储的呢?

在这里插入图片描述

如上图,我们虚表其实是存在代码段的。

析构函数

在我们继承章节的时候,我们在派生类构造的时候需要手动调用父类的构造函数,而析构的时候并不让我们去调用父类的析构,因为我们要先析构子类,最后子类自动调用父类的析构函数,而析构函数会被编译器默认统一命名为destructor()

class Person
{
public:
	Person()
	{
		cout << "Person()" << endl;
	}
	~Person()
	{
		cout << "~Person()" << endl;
	}
};
class Student : public Person
{
public:
	Student()
	{
		cout << "Student()" << endl;
	}
	~Student()
	{
		cout << "~Student()" << endl;
	}
};
int main()
{
	Student st;
	return 0;
}

如上段代码,先来看一下直接创建对象的时候的运行结果:

在这里插入图片描述

如上图,我们析构和构造的调用次数是正确的,来看如下情况:

在这里插入图片描述

看如上图的情况,发生了经典的内存泄漏问题。

为什么会发生这样的问题呢?

我们的p是一个子类对象的指针,那么析构的时候应该把子类也析构了,但是此时我们的析构函数并不是虚函数,那么delete p的本质是:析构 + operator delete。前面说了,析构函数会被默认转化为destructor(),但是由于这里析构函数并不是虚函数,所以调用析构的时候就是**什么类型的指针调用对应的析构函数,所以这里我们调用的是基类的析构函数。**所以这里我们只需要把析构函数加上我们的virtual即可。

在这里插入图片描述

所以这就是多态的好处,由于我们析构函数会被转化为destructor(),所以天生就是重写,当我们使用父类指针调用的时候会去执行指针指向对象的析构函数。

虚函数重写

如下段代码,它的运行结果是什么呢?

在这里插入图片描述

在这里插入图片描述

如上图,我们发现输出结果为: D e r i v e : − > 1 Derive:->1 Derive:>1所以虚函数重写只是重写我们函数的实现。而函数的声明使用的是父类的,这里要跟直接使用Derive对象调用fun()函数区分开,当我们直接是用Derive对象调用的时候输出的是: D e r i v e : − > 0 Derive:->0 Derive:>0,他直接去对象的虚表中查找调用的。所以可以说当我们使用父类指针或引用调用的时候,虚函数的声明始终是我们父类的,但是函数的实现是由指针指向的对象决定的。

那么再来看一下下面的代码,运行结果是什么呢?

class A
{
public:
    A() 
        :m_iVal(0) 
    { 
        cout << "A() " << endl;
        test();
    }
    virtual ~A() 
    {
        cout << "~A()" << endl; 
    }
    virtual void func() 
    {
        std::cout << m_iVal << " ";
    }
    void test()
    {
        func();
    }
public:
    int m_iVal;
};

class B : public A
{
public:
    B() 
        :A()
    {
        cout << "B() " << endl;
        test();
    } 
    ~B()
    {
        delete[] ptr;
        cout << "~B()" << endl;
    }

    virtual void func()
    {
        ++m_iVal;
        std::cout << m_iVal << " ";
    }
private:
    int* ptr = new int[2];
};
int main()
{
    A* p = new B; 
    p->test();
    return 0;
}

在这里插入图片描述

这里需要注意的是,在我们B对象构造的过程会先去调用A的构造,那么在A构造的过程中,编译器会认为此时的类型是A类型,所以调用虚函数的时候,也是调用A的虚函数。

小知识

  • virtual关键字只在声明时加上,在类外实现时不能加

  • .staticvirtual是不能同时使用的

  • 23
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值