Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >C++多态原理揭秘

C++多态原理揭秘

作者头像
初阶牛
发布于 2023-11-19 00:23:38
发布于 2023-11-19 00:23:38
20200
代码可运行
举报
文章被收录于专栏:C语言基础C语言基础
运行总次数:0
代码可运行
🎈个人主页:🎈

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

1.重写:

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

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

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

示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
virtual void function() = 0;

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

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

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

抽象类的作用如下:

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

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

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

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

接口继承与实现继承

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

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

🍉抽象类示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
#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
代码运行次数:0
运行
AI代码解释
复制
#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
代码运行次数:0
运行
AI代码解释
复制
#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 删除。

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
C++进阶:详解多态(多态、虚函数、抽象类以及虚函数原理详解)
注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承后在派生类依旧保持虚函数属性)但是该种写法不规范,大家还是少用为好。
是Nero哦
2024/03/17
7550
C++进阶:详解多态(多态、虚函数、抽象类以及虚函数原理详解)
移情别恋c++ ദ്ദി˶ー̀֊ー́ ) ——11.多态
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态。
hope kc
2024/09/23
1930
移情别恋c++ ദ്ദി˶ー̀֊ー́ ) ——11.多态
【C++】多态——实现、重写、抽象类、多态原理
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
平凡的人1
2023/10/15
7360
【C++】多态——实现、重写、抽象类、多态原理
多态
我们就会发现,1正常释放,2只是释放了基类的,没有释放父类的,这就会造成内存泄漏。 当我们写成虚函数virtual ~teacher(),构成多态之后,就可以全部正常的对子类释放(调用子类的析构函数时,先析构子类,再析构父类):
code-child
2023/05/30
3340
多态
【C++进阶】多态的理解
        1.静态绑定,也称为静态多态,是在程序编译阶段确定的,例如:函数重载和模板;
aosei
2024/01/23
2590
【C++进阶】多态的理解
【c++】多态(多态的概念及实现、虚函数重写、纯虚函数和抽象类、虚函数表、多态的实现过程)
本篇文章是继继承之后,博主跟大家介绍面向对象三大特性的最后一个——多态。
ephemerals__
2024/12/25
7990
【c++】多态(多态的概念及实现、虚函数重写、纯虚函数和抽象类、虚函数表、多态的实现过程)
【C++】C++多态世界:从基础到前沿
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。 那么在继承中要构成多态还有两个条件:
六点半就起.
2024/10/23
2160
【C++】C++多态世界:从基础到前沿
C++从入门到精通(第九篇) :多态
持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第22天,点击查看活动详情
雪芙花
2022/10/31
5550
C++从入门到精通(第九篇) :多态
【C++】———— 多态
举个例子:就比如买票这个行为,成人买成人票,学生买学生票,军人优先买票,这就是一个简单的例子。
用户11036582
2024/07/15
2230
【C++】———— 多态
【C++】多态
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
zxctscl
2024/04/25
1840
【C++】多态
多态与虚(函数)表
续接上回(继承),我们了解了继承是如何通过虚基表,来解决派生类和父类有相同的成员变量的情况,但是类和对象中可不只有成员变量,如果成员函数也有同名,更或者如果我们想在访问不同情况(类)但是相同函数名时,根据不同类满足不同需求。
比特大冒险
2023/04/16
6760
多态与虚(函数)表
什么是多态?如何实现?只看这一篇就够了
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
海盗船长
2020/08/27
1.6K0
C++进阶-多态
C++进阶-多态 零、前言 一、多态的概念和定义 二、虚函数 1、概念和定义 2、虚函数重写的特例 3、C++11 override 和 final 4、重载/重写/重定义对比 三、抽象类 四、多态的原理 1、虚函数表 2、多态的原理 3、动态绑定与静态绑定 4、多继承虚函数表 五、继承和多态常见的面试问题 零、前言 C++有五大特性:对象,封装,继承,抽象和多态。而本章则将学习讲解C++中的多态 一、多态的概念和定义 概念: 通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会
用户9645905
2022/11/30
7210
C++进阶-多态
c++进阶(c++里的多态)
多态的概念:一般来说,就是多种状态。具体来讲就是去完成某个行为,当不同对象去完成时会产生不同的状态。 举个例子:比如买票这个行为,当普通人买票时,为全价买票;当学生买票时,为半价买票;军人买票时,为优先买票。
Yui_
2024/10/15
1860
c++进阶(c++里的多态)
【C++】三大特性之多态
多态的概念:通俗来说,就是多种形态, 具体点就是去完成某个行为,当不同的对象去完成时会
青衫哥
2023/03/31
8950
【C++】三大特性之多态
【C++修炼之路】16.C++多态
多态的概念: 通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
每天都要进步呀
2023/03/28
5760
【C++修炼之路】16.C++多态
【C++】多态(定义、虚函数、重写、隐藏)
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态。
秦jh
2024/07/03
3640
【C++】多态(定义、虚函数、重写、隐藏)
【C++】多态详细讲解
多态通俗来说就是多种形态。多态分为编译时多态(静态多态)和运⾏时多态(动态多态)。
羚羊角
2025/02/06
1960
【C++】多态详细讲解
【C++】多态
1. 多态是在继承的基础之上实现的,我们说继承是类设计层次的代码复用的一种手段,而多态则是在此基础上实现的多种形态,完成某一件事,可以由于对象的不同产生不同的完成结果,我们称这种现象为多态。
举杯邀明月
2023/04/12
6530
【C++】多态
C++:多态
编译时多态主要就是函数重载和函数模板,传递不同类型的参数调用不同的函数,通过参数不同达到多种形态。至于叫做编译时多态的原因,是它们实参传给形参的参数匹配是在编译时完成的。
HZzzzzLu
2024/11/26
2160
C++:多态
相关推荐
C++进阶:详解多态(多态、虚函数、抽象类以及虚函数原理详解)
更多 >
领券
一站式MCP教程库,解锁AI应用新玩法
涵盖代码开发、场景应用、自动测试全流程,助你从零构建专属AI助手
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档