前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++多态原理揭秘

C++多态原理揭秘

作者头像
初阶牛
发布2023-11-19 08:23:38
1450
发布2023-11-19 08:23:38
举报
文章被收录于专栏:C语言基础
🎈个人主页:🎈

一、你分的清"重写","重载"和"重定义"吗?

1.重写:

(上一篇以及详细介绍了)

条件: (1)分别在两个不同的作用域,基类和派生类. (2)三同(函数名,返回值,函数参数列表)(斜变和析构函数除外) (3)是virtual修饰的虚函数.

实现效果: 不同对象使用同一个函数名,可以实现不同的行为,也就是多态.

示例:

代码语言:javascript
复制
class Person									//基类
{
public:
	virtual void test(int a,int b)
	{
		cout << a + b << endl;
	}
};

class Student:public Person						//派生类
{
public:
	virtual void test(int a,int b)
	{
		cout << a * b << endl;
	}
};


void Test(Person& p1)
{
	p1.test(2,3);
}
int main()
{
	Person p1;
	Student s1;

	Test(p1);
	Test(s1);

	return 0;
}

2.重载:

条件: (1)在同一个作用域.(这个很重要). (2)参数列表不同,参数个数不同,也可以是参数类型不同或者参数顺序不同。 返回值可同可不同. (3)函数名相同

实现效果: 函数重载可以为不同的数据类型或参数组合提供相同的接口,使得代码更加方便调用和使用。

代码语言:javascript
复制
int Add(int a, int b)
{
	return a + b;
}

int Add(float a, float b)		//参数不同
{
	return a + b;
}
//float Add(float a, float b)		//也是ok的
//{
//	return a + b;
//}

double Add(double a, double b)
{
	return a + b;
}


int main()
{
	int a = 2;
	float b = 1.2f;
	double c = 2.2;

	cout << Add(a, a) << endl;
	cout << Add(b, b) << endl;
	cout << Add(c, c) << endl;
	return 0;
}

3.重定义

条件: (1)分别在两个不同的作用域,基类和派生类. (2)函数名相同,只要不构成重载.

实现效果: 隐藏派生类的函数.

代码语言:javascript
复制
class Person									//基类
{
public:
	 void test(int a,int b)
	{
		cout << a + b << endl;
	}
};

class Student:public Person						//派生类
{
public:
	 void test(int a)				//只要函数名相同即可
	{
		cout << a  << endl;
	}
};

二、抽象类

抽象类是一种特殊的类,它不能被实例化,只能被用作基类。抽象类通常包含一些纯虚函数,这些函数没有实现体,只有函数名。派生类必须实现这些纯虚函数,才能被实例化。 这点很重要,纯虚函数必须被重写.

🍭纯虚函数

纯虚函数是定义在抽象类中的特殊函数,它不需要具体的实现,而是由其派生类实现。 格式:函数声明的分号前加上=0

例如,下面就是一个纯虚函数的定义:

代码语言:javascript
复制
virtual void function() = 0;

抽象类: 包含纯虚函数的类就是抽象类. 抽象类不能被实例化,也就是不能创建对象但是可以定义指向抽象类的指针和引用,并通过派生类对象的地址来初始化它们。

派生类必须实现其基类中所有的纯虚函数,否则它仍然是抽象类,无法被实例化。

纯虚函数的作用是规范继承类的接口,强制派生类提供相应的实现,从而增强程序的可扩展性。同时,纯虚函数也可以作为基类中的一个默认实现,提供一些默认的行为。

抽象类的作用如下:

提供一种适合多态的机制。因为抽象类的纯虚函数只有函数名,没有实现体,所以无法被单独实例化。但是,抽象类可以被用作基类,在派生类中实现纯虚函数,从而实现不同的多态行为。

规范派生类的实现。抽象类中定义的纯虚函数,是对派生类接口的规范。派生类必须实现这些纯虚函数,否则无法被实例化。这样可以避免派生类在实现中遗漏必要的函数或参数,从而保证代码的正确性。

封装类的实现细节。抽象类中通常包含一些实现细节,这些细节对于使用派生类的代码来说并不需要知道。通过将这些细节封装在抽象类中,可以使代码更加清晰和简洁。

总之,抽象类是C++中面向对象编程的重要机制之一,它通过规范派生类的实现和封装类的实现细节,提高了代码的可读性、可维护性和可扩展性。

接口继承与实现继承

实现继承: 派生类继承了基类普通函数,可以使用函数,继承的是函数的实现。也就是实现继承.

虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

🍉抽象类示例:

代码语言:javascript
复制
#include <iostream>
#include <string>

// 定义抽象水果类
class Fruit {
public:
    // 纯虚函数,只能在子类重写才有意义
    virtual string getName() const = 0;
    virtual string getColor() const = 0;

    // 虚函数,可以在子类中被重写
    virtual void printInfo() const {
        cout << "This is a " << getName() << "." << endl;
        cout << "Color: " << getColor() << "." << endl;
    }

    // 析构函数,需要为虚函数,确保在析构父类指针时,能够正确调用其子类的析构函数
    virtual ~Fruit() {}
};

// 定义苹果类,继承自水果类
class Apple : public Fruit {
public:
    string getName() const override {
        return "Apple";
    }


    string getColor() const override {
        return "Red";
    }
};

// 定义香蕉类,继承自水果类
class Banana : public Fruit {
public:
    string getName() const override {
        return "Banana";
    }

    string getColor() const override {
        return "Yellow";
    }
};

int main() {

    // Fruit f1;       //抽象类无法实例化出对象

    // 构建苹果对象,调用printInfo()方法

    //方法1
    Fruit* f1 = new Apple;
    f1->printInfo();

    //方法2:
    Apple apple;
    apple.printInfo();

    cout << endl;

    // 构建香蕉对象,调用printInfo()方法
    Banana banana;
    banana.printInfo();

    return 0;
}

抽象类无法直接实例化出对象,只有被继承,进行函数重写才有意义.

三、解密多态原理

还记得在刚刚接触类和对象的时候,我们需要了解对象的大小如何计算. 对于函数,所有的类都可能需要使用,这可以将函数存放在公共区域,也就是不在类中,不占用类的空间. 那试着计算一下People类的大小吧!

代码语言:javascript
复制
class People
{
public:
    virtual void Have_lunch()
    {
        cout << "你需要支付10元的午餐费!" << endl;
    }
    virtual void Test()
    {
        int a = 2;
    }
protected:
    int _b=2;
};

int main()
{
    cout << sizeof(People) << endl;
    People p1;

    return 0;
}

运行结果:

8

解析:

如下图: vfptr是一个指针,占用四个字节(32位下). _b是int类型占四个字节.

vfptr是什么呢? Virtual Function Pointer即虚函数指针. 虚函数指针,顾名思义,就是用于指向虚函数的指针. 对象中的这个指针我们叫做虚函数表指针。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表.

基类中的虚表

虚表中存放着虚函数的地址.

派生类的虚表

派生类的虚表有两部分构成: 第一部分: 从基类中继承下来的虚函数(如果在派生类中也定义了,就会重写,也就实现了多态). 第二部分: 派生类自己的虚函数,放在虚函数表的下半部分.(这里在监视窗口中没看到,但是在内存窗口可以看到). 内存窗口中看到的第三个函数指针,我们猜测是派生类自己的虚函数,下面再验证.

派生类的虚表生成: 先将基类中的虚表内容拷贝一份到派生类虚表中 .(继承下来) 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 (重写) 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。(新增)

原理: 多态是因为在派生类中,对继承下来的虚函数进行了重写. 当程序调用一个虚函数时,实际上是通过对象的vptr找到相应的虚函数表,再根据函数在虚函数表中的索引找到具体的函数地址。如果对象是派生类的实例,而且派生类中重写了虚函数,那么调用该函数时就会调用派生类中的版本。这种机制在程序运行时动态决定了具体调用哪个函数,从而实现了多态特性。

注意: 多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的

简单来说就是,普通的函数调用就是 call这个函数的地址,然后执行函数的语句就行,这就是静态的调用.

而多态不同,在执行函数调用时并不知道函数地址,而是运行起来后需要通过对象去对应的虚函数表中寻找,等找到对应的函数后再确定地址.(也就是动态调用).

如何打印虚函数表?

代码语言:javascript
复制
#include <iostream>
#include <string>
using namespace std;

class People
{
public:
    virtual void Have_lunch()
    {
        cout << "People::Have_lunch" << endl;
    }
    virtual void Test()
    {
        cout << "People::Test()" << endl;
    }
    void P1_test() //会被继承下去,但是不会进虚函数表
    {
        cout << "People::P1_test()" << endl;
    }

protected:
    int _b = 2;
};

class Teacher : public People
{
public:
    virtual void Have_lunch()
    {
        cout << "Teacher::Have_lunch()" << endl;
    }
    virtual void Test1()
    {
        cout << "Teacher::Test1()" << endl;
    }
    void Test_Teacher()
    {
        cout << "Teacher::Test_Teacher" << endl;
    }

protected:
    int _c;
};


//声明一个函数指针
typedef void (*VFPtr_Table)();      //函数指针的类型重命名不一样,这里不能写成typedef void (*)() VFPtr_Table

void Print_vfptr(VFPtr_Table table[])       //参数类型是一个 函数指针 数组
{

    for (int i = 0; table[i] != nullptr; i++)       //这里循环结束的条件是遇到空指针,这是VS特有,不具备跨平台特性
    {
        printf("table[%d]--->%p:: ", i, table[i]);
        table[i]();		//通过函数指针调用相应的函数
    }
}

int main()
{
    People p1;
    Teacher t1;


    cout << "People::VFPtr_Table::" << endl;
    Print_vfptr((VFPtr_Table *)*(int *)&p1); // &p1表示取出对象的地址
                                             //(int*)&p1表示获取对象的前四个字节,也就是指向虚表的地址
                                             //*((int*)&p1)对虚表指针解引用,得到虚表地址
                                             //(VFPtr_Table*)*(int *)&p1 将得到的虚表地址,强转为函数指针传参.
                                            
    cout << "Student::VFPtr_Table::" << endl;
    Print_vfptr((VFPtr_Table *)*(int *)&t1);
    return 0;
}

对函数指针不大了解的友友们,可能理解起来就困难一些了,这里注释牛牛已经算是讲解的比较详细了.

验证猜想:

  1. 虚函数存在哪的?虚表存在哪的? 可不要说虚函数在存在虚表中,虚表是一个函数指针数组,里面存放的都是一个个函数指针. 他们只是指向虚函数的指针. 那虚函数存在哪里呢?

虚函数和普通函数一样的,都是存在代码段(常量区)的. 虚表看上去是存放在对象中,其实也不然,对象只是存放虚表指针. 那么虚表存在哪的呢?

对于这类问题,我们可以直接实践一波. 分别在栈区,堆区,常量区和静态区分别定义一个变量,打印其地址,再与虚表地址进行对比.

验证虚表位置:

代码语言:javascript
复制
#include <iostream>
#include <string>
using namespace std;

class People
{
public:
    virtual void Have_lunch()
    {
        cout << "People::Have_lunch" << endl;
    }
    virtual void Test()
    {
        cout << "People::Test()" << endl;
    }
    void P1_test() //会被继承下去,但是不会进虚函数表
    {
        cout << "People::P1_test()" << endl;
    }

protected:
    int _b = 2;
};

class Teacher : public People
{
public:
    virtual void Have_lunch()
    {
        cout << "Teacher::Have_lunch()" << endl;
    }
    virtual void Test1()
    {
        cout << "Teacher::Test1()" << endl;
    }
    void Test_Teacher()
    {
        cout << "Teacher::Test_Teacher" << endl;
    }

protected:
    int _c;
};

typedef void (*VFPtr_Table)();
void Print_vfptr(VFPtr_Table table[])
{

    for (int i = 0; table[i] != nullptr; i++)
    {
        printf("table[%d]--->%p:: ", i, table[i]);
        table[i]();
    }
}

int main()
{
    People p1;
    Teacher t1;
    int a = 0;              //栈区
    printf("栈区:%p\n", &a);



    int* p = new int;       //堆区
    printf("堆区:%p\n", p);

    static int  sa = 0;     //静态区
    printf("静态区:%p\n", &sa);

    const char* ca = "CSDN!! cjn";       //常量区(代码段)
    printf("常量区:%p\n", ca);
 
    cout << endl;
    printf("虚表1地址:%p\n",*((int*)&p1));         //&p1表示对象的地址
                                                    //(int*)&p1表示获取对象的前四个字节,也就是指向虚表的地址
                                                    //*((int*)&p1)对虚表指针解引用,得到虚表地址
    printf("虚表2地址:%p\n",*((int*)&t1));

    
    cout << "People::VFPtr_Table::" << endl;
    Print_vfptr((VFPtr_Table *)*(int *)&p1);

    cout << "Student::VFPtr_Table::" << endl;
    Print_vfptr((VFPtr_Table *)*(int *)&t1);
    return 0;
}

运行结果:

很明显,常量区的地址距离虚表最近.

四、多继承中的虚表

代码语言:javascript
复制
#include<iostream>

using namespace std;

class A
{
public:
    virtual void Fun1(){    cout << "A::Fun1()" << endl;    }
    virtual void Fun2(){    cout << "A::Fun2()" << endl;    }
};


class B 
{
public:
    virtual void Fun1() { cout << "B::Fun1()" << endl; }
    virtual void Fun3() { cout << "B::Fun3()" << endl; }

};


class C :public A ,public B
{
public:
    virtual void Fun1() { cout << "C::Fun1()" << endl; }
    virtual void Fun3() { cout << "C::Fun3()" << endl; }
    virtual void Fun4() { cout << "C::Fun4()" << endl; }
};

typedef void (*VFPtr_Table)();
void Print_vfptr(VFPtr_Table table[])
{

    for (int i = 0; table[i] != nullptr; i++)
    {
        printf("table[%d]--->%p:: ", i, table[i]);
        table[i]();
    }
}

int main()
{
    A a1;
    B b1;
    C c1;
   

    cout << "A::VFPtr_Table::" << endl;
    Print_vfptr((VFPtr_Table*)*(int*)&a1);
    cout << endl;

    cout << "B::VFPtr_Table::" << endl;
    Print_vfptr((VFPtr_Table*)*(int*)&b1);
    cout << endl;

    cout << "C::A::VFPtr_Table::" << endl;
    Print_vfptr((VFPtr_Table*)*(int*)&c1);
    cout << "C::B::VFPtr_Table::" << endl;
    Print_vfptr((VFPtr_Table*)*((int*)&c1 +1));

    return 0;
}

通过观察监视窗口,我们可以看到,派生类c中,有两个虚表.

(图片不清晰,请见谅.)

主要有两点:

  1. 基类中的虚函数,无论在派生类中是否被重写,都存放在派生类中对应的该基类虚表中. 被重写的虚函数,在虚表中被覆盖.
  2. 派生类自己的虚函数,存放在第一个基类的虚表后面,

对于菱形虚拟继承,菱形继承都不推荐设计,就别谈菱形虚拟继承了,这里也就不讨论了.

c++中有关多态的知识,到这里就结尾了,如果文章有什么错误之处,希望与牛牛私信交流,牛牛会一 一改正的.

码文不易,三连支持一下吧!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-11-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、你分的清"重写","重载"和"重定义"吗?
    • 1.重写:
      • 2.重载:
        • 3.重定义
        • 二、抽象类
          • 🍭纯虚函数
            • 🍉抽象类示例:
              • 基类中的虚表
              • 派生类的虚表
          • 三、解密多态原理
          • 四、多继承中的虚表
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档