【C++核心编程】多态&虚函数指针&虚函数表

 🔥博客主页: 我要成为C++领域大神

🎥系列专栏【C++核心编程】 【计算机网络】 【Linux编程】 【操作系统】

❤️感谢大家点赞👍收藏⭐评论✍️

本博客致力于分享知识,欢迎大家共同学习和交流。

多态的概念

多态,通俗来讲就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。比如,在买票这一行为,普通人买票是全价买票,学生买票是半价买票,而军人买票是优先买票;再比如动物园的动物叫这个行为,不同的动物叫声是不一样的。这些都是生活中多态的例子。

C++中多态的分类:

静态多态:静态多态是指在编译时实现的多态,比如说函数重载

int Add(int left, int right)
{
    return left + right;
}
double Add(double left, int right)
{
    return left + right;
}

int main()
{
    Add(10, 20);
    //Add(10.0, 20.0);  //这是一个问题代码
    Add(10.0,20);  //正常代码
    return 0;
}

根据函数传入参数类型的不同调用不同的函数。

动态多态:动态也就是我们常说的多态,动态多态是在运行中实现的。根据父类的指针或引用接收不同对象,来确定自己会调用哪个类的虚函数。

C++多态,父类的指针指向继承该类的所有子类对象,多个子类具有多种形态由父类的指针进行统一管理,父类具有多态性

虚函数:被关键字virtual修饰的类成员函数。

重写:在子类中定义和父类中一模一样的虚函数,函数名、参数列表相同。只要对父类中的函数进行了重写,就会自动添加virtual

讲到重写,那么我们就得弄清楚3个概念,即重载,重写(覆盖),重定义(隐藏)。这是说三个定义的区别:1.重载:重载函数处在同一作用域。函数名相同,函数列表必须不同。2.重写(覆盖):必须是虚函数,且处在父类和子类中。返回值,参数列表,函数名必须完全相同(协变除外)。3.重定义:子类和父类的成员变量相同或者函数名相同,子类隐藏父类的对应成员。子类和父类的同名函数不是重定义就是重写。

多态产生的条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。那么在继承中要构成多态还有两个条件:

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

虚函数指针

__vfptr:virtial function pointer 虚函数指针当在类中添加虚函数时,编译器会自动在类中添加一个虚函数指针,类型为 void** 二级指针。 指向了 void* 数组 (void * [n]),真实类型:函数指针数组属于对象的,在定义对象时,每个对象都会存在一个。由编译器默认初始化,指向了虚函数列表,每个对象的虚函数指针指向同一个虚函数列表

__vftable:virtual function table虚函数列表本质上为 函数指针数组 ,数组的每一个元素为当前类的 虚函数 的地址。属于类的,在编译期就存在了,一个类中只存在了一份

class CTest {
public:
virtual void fun1() { cout << __FUNCTION__ << endl; }
virtual void fun2() { cout << __FUNCTION__ << endl; }
virtual void fun3() { cout << __FUNCTION__ << endl; }
void fun4(){ cout << __FUNCTION__ << endl; }
CTest(){
    cout << "CTest()" << endl;
}
}; 
cout << sizeof(CTest) << endl;//当类中没有虚函数时,对象大小为1。有虚函数时,大小为4

我们可以观察到,当类中没有虚函数时,类的大小为1个字节,有了虚函数后,编译器自动在类中添加了一个虚函数指针,在x86下,为4个字节。

下面这段代码模仿了虚函数指针的工作原理

int main() {

    CTest tst1;
    CTest tst2;
    //--------------------------------------------
    CTest* ptst=nullptr;
    ptst->fun4();
    //ptst->fun1(); //error 无法获取到虚函数指针

    //模拟虚函数指针的调用过程:
    CTest tst;
    int** __vfptr = (int**)*(int*)&tst; 
    void(*m_pfun1)() = (void (*) ())__vfptr[0];
    void(*m_pfun2)() = (void (*) ())__vfptr[1];
    void(*m_pfun3)() = (void (*) ())__vfptr[2];
    (*m_pfun1)();
    (*m_pfun2)();
    (*m_pfun3)();
    return 0;
}

虚函数列表

单继承下的虚函数表

那么我们如何查看虚函数表呢?使用下面的demo程序可以查看虚函数表

思路:取出tst,son对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数

指针的指针数组,这个数组最后面放了一个nullptr

1.先取b的地址,强转成一个int*的指针

2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针

3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。

4.虚表指针传递给PrintVTable进行打印虚表

#include <iostream>  // 包含头文件。
using namespace std; // 指定缺省的命名空间。

class CTest
{
public:
    virtual void fun1() { cout << __FUNCTION__ << endl; }
    virtual void fun2() { cout << __FUNCTION__ << endl; }
    virtual void fun3() { cout << __FUNCTION__ << endl; }
    void fun4() { cout << __FUNCTION__ << endl; }
private:
    int m_a;
};
class CSon :public CTest
{
public:
    void fun1() { cout << __FUNCTION__ << endl; }
    virtual void fun5() { cout << __FUNCTION__ << endl; }
    virtual void fun4() { cout << __FUNCTION__ << endl; }
    void fun2() { cout << __FUNCTION__ << endl; }
private:
    int m_b;
};
typedef void (*VFPTR)();
void printVtable(VFPTR vtable[])
{
    cout << "虚表地址为:" << vtable << endl;
    for (int i = 0; vtable[i] != nullptr; ++i)
    {
        cout << "第" << i + 1 << "个虚函数地址为" << vtable[i] << endl;
        VFPTR f = vtable[i];
        f();
    }
    cout << endl;
}
int main()
{
    CTest tst;
    CSon son;
    VFPTR* vtable = (VFPTR*)(*(int*)&tst);
    printVtable(vtable);
    VFPTR* vtable2 = (VFPTR*)(*(int*)&son);
    printVtable(vtable2);
}

运行程序可以看出,无论子类的函数体先后顺序怎么样,虚函数表内都是优先存放继承自父类的虚函数,然后才是子类的虚函数按先后顺序进行排列。

多继承下的虚函数表

#include <iostream>  // 包含头文件。
using namespace std; // 指定缺省的命名空间。

class CTest1
{
public:
    virtual void fun1() { cout << __FUNCTION__ << endl; }
    virtual void fun2() { cout << __FUNCTION__ << endl; }
    virtual void fun3() { cout << __FUNCTION__ << endl; }
    void fun4() { cout << __FUNCTION__ << endl; }
private:
    int m_a;
};
class CTest2
{
public:
    virtual void fun1() { cout << __FUNCTION__ << endl; }
    virtual void fun2() { cout << __FUNCTION__ << endl; }
    virtual void fun3() { cout << __FUNCTION__ << endl; }
    void fun4() { cout << __FUNCTION__ << endl; }
private:
    int m_a2;
};
class CSon :public CTest1,public CTest2
{
public:
    void fun1() { cout << __FUNCTION__ << endl; }
    virtual void fun5() { cout << __FUNCTION__ << endl; }
    virtual void fun4() { cout << __FUNCTION__ << endl; }
    void fun2() { cout << __FUNCTION__ << endl; }
private:
    int m_b;
};
typedef void (*VFPTR)();
void printVtable(VFPTR vtable[])
{
    cout << "虚表地址为:" << vtable << endl;
    for (int i = 0; vtable[i] != nullptr; ++i)
    {
        cout << "第" << i + 1 << "个虚函数地址为" << vtable[i] << endl;
        VFPTR f = vtable[i];
        f();
    }
    cout << endl;
}
int main()
{
    CSon son;
    VFPTR* vtable1 = (VFPTR*)(*(int*)&son);
    printVtable(vtable1);
    VFPTR* vtable2 = (VFPTR*)(*(int*)((char*)&son+sizeof(CTest1)));
    printVtable(vtable2);
}

观察下图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

虚函数重写的特殊情况

协变:

正常情况下,如果我们将父类和子类中的虚函数的返回值设为不同,可能会发生报错;

协变指的是:派生类重写基类虚函数时,与基类虚函数返回值类型不同,且基类的虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。简单来说就是两者的返回值必须是父子关系的指针或者引用。举个例子:

class A{};
class B:public A
{};
class Person
{
public: 
virtual A* BuyTicket()
{
    A a;
    cout << "全价买票" << endl;
    return &a;
}
};
class Student :public Person
{
public:
virtual B* BuyTicket()
{
    B b;
    cout << "半价买票" << endl;
    return &b;
}

虚析构:在父类析构函数前加上virtual关键字。

首先我们先回顾一下没有构成多态的析构函数调用:只需要子类对象销毁时无需手动销毁父类对象,会自动调用父类对象的析构函数。1.如果父类的析构函数为虚函数,此时子类的析构函数无论加不加virtual,都是对父类的析构函数的重写。2.虽然子类和父类的析构函数的函数名不同,但其实编译器对析构函数的名称进行了特殊的处理。(name-mangle 不同类的析构函数肉眼看到的函数名不同,但是编译器会进行 name-mangle,在编译器看来,不同类的析构函数名相同)

class CFather {
public:
CFather() { cout << "CFather" << endl; } 
~CFather() { cout << "~CFather" << endl; }
};
class CSon :public CFather{
public:
CSon() { cout << "CSon" << endl; }
~CSon() { cout << "~CSon" << endl; } 
};
int main() {
    CSon son; //顺序:父子 子父

当我们定义子类对象时,构造函数调用顺序为父->子,析构函数调用顺序为子->父

若定义了父类指针申请子类空间,释放此空间,仅仅会调用父类析构函数,不会调用子类析构函数

#include <iostream>  // 包含头文件。
using namespace std; // 指定缺省的命名空间。

class CFather
{
public:
    CFather() { cout << "CFather" << endl; }
    ~CFather() { cout << "~CFather" << endl; }
};
class CSon : public CFather
{
public:
    CSon() { cout << "CSon" << endl; }
    ~CSon() { cout << "~CSon" << endl; }
};
int main()
{

    CFather *pfa = new CSon;
    delete pfa;
}

解决问题:虚析构。在父类的析构函数前加上virtual,子类析构函数进行重写父类析构,发生多态,最终调用子类析构 pfa->~CFather() ---> (*(pfa->__vfptr[0]))();

#include <iostream>  // 包含头文件。
using namespace std; // 指定缺省的命名空间。

class CFather
{
public:
    CFather() { cout << "CFather" << endl; }
    virtual ~CFather() { cout << "~CFather" << endl; }
};
class CSon : public CFather
{
public:
    CSon() { cout << "CSon" << endl; }
    ~CSon() { cout << "~CSon" << endl; }
};
int main()
{

    CFather *pfa = new CSon;
    delete pfa;
}

纯虚函数

纯虚函数也可以叫抽象函数,一般来说它只有函数名、参数和返回值类型,不需要函数体。纯虚函数是一种特殊的虚函数,它的一般格式如下(C++格式):

如果一个类中拥有 纯虚函数 那么这个类 就是抽象类,抽象类 不能实例化对象

class <类名>
{
virtual <类型><函数名>(<参数表>)=0;
…
};

首先:强调一个概念

定义一个函数为虚函数,不代表函数为不被实现的函数。

定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。

定义一个函数为纯虚函数,才代表函数没有被实现。

定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。

例如:每种动物进食的方式不同,但是我们的动物基类必须要有进食的行为,因此将进食的函数定义为纯虚函数,在后续的Dog子类中必须去实现eat函数的功能

#include<iostream>
using namespace std;

//抽象类:含有纯虚函数的类,无法具体化,不允许定义对象
class CAnimal {
public:
virtual void eat() = 0;//纯虚函数:在父类中只需要声明,可以不用定义
};
//具体类:
class CDog:public CAnimal {
public:
void eat() {
    cout << "吃骨头" << endl;
}
};
int main() {
    //CAnimal ani; //无法定义对象
    CAnimal* pani = new CDog;
    pani->eat();
    return 0;
}

当子类中未实现父类纯虚函数功能时:

纯虚函数的定义和调用

//纯虚函数可以在类外定义
void CAnimal::eat() {
    cout << "CAnimal::eat" << endl;
}

int main() {
    CAnimal* pani = new CDog;
    pani->CAnimal::eat();//编译期绑定:CAnimal::eat,静态多态
    return 0;
}


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值