当前位置: 首页 > article >正文

【C++】多态详解

📢博客主页:https://blog.csdn.net/2301_779549673
📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!
📢本文由 JohnKi 原创,首发于 CSDN🙉
📢未来很长,值得我们全力奔赴更美好的生活✨

在这里插入图片描述

在这里插入图片描述

文章目录

  • 🏳️‍🌈1. 多态的概念
  • 🏳️‍🌈2.多态的定义及实现
    • ❤️2.1 多态的构成条件
      • 2.1.1 实现多态还有两个必须重要条件!
      • 2.1.2 虚函数
      • 2.1.3虚函数的重写/覆盖
      • 2.1.4 多态场景的一个选择题
      • 2.1.6 override 和 final关键字
  • 🏳️‍🌈3.纯虚函数和抽象类
  • 🏳️‍🌈4.多态的原理
  • 🏳️‍🌈4.2 多态的原理
  • 👥总结


🏳️‍🌈1. 多态的概念

多态(polymorphism)的概念:通俗来说,就是多种形态。多态分为编译时多态(静态多态)和运行时多态(动态多态),这里我们重点讲运行时多态,编译时多态(静态多态)和运行时多态(动态多态)。编译时多态(静态多态)主要就是我们前面讲的函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时一般归为静态,运行时归为动态。

运行时多态,具体点就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达到多种形态。比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是优惠买票(5折或75折);军人买票时是优先买票。再比如,同样是动物叫的一个行为(函数),传猫对象过去,就是喵,传狗对象过去,就是狗

🏳️‍🌈2.多态的定义及实现

❤️2.1 多态的构成条件

多态是一个继承关系的下的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象优惠买票。

2.1.1 实现多态还有两个必须重要条件!

  • 必须指针或者引用调用虚函数
  • 被调用的函数必须是虚函数。

**说明:**要实现多态效果,第一必须是基类的指针或引用,因为只有基类的指针或引用才能既指向派生类对象:第二派牛类必须对基类的虑函数重写/覆盖,重写或者覆盖了,派生类才能有不同的函数,多态的不同形态效果才能达到。
在这里插入图片描述

2.1.2 虚函数

类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。注意非成员函数不能加virtual修饰。

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

2.1.3虚函数的重写/覆盖

虚函数的重写/覆盖:派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。

**注意:**在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用,不过在考试选择题中,经常会故意买这个坑,让你判断是否构成多态。

class Person {
  public:
    virtual void BuyTicket() {
        cout << "买票-全价" << endl;
    }
};
class Student : public Person {
  public:
    virtual void BuyTicket() {
        cout << "买票-打折" << endl;
    }
};
void Func(Person* ptr) {
// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
    ptr->BuyTicket();
} 
int main() {
    Person ps;
    Student st;
    Func(&ps);
    Func(&st);
    return 0;
}

2.1.4 多态场景的一个选择题

在这里插入图片描述

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;
}
  1. 首先分析代码中的类继承关系和虚函数重写情况:
    • 在这段代码中,类B继承自类A
    • A中有虚函数functest,其中test函数内部调用了func函数。
    • B重写了func函数。
  2. 然后看函数调用情况:
    • main函数中,创建了一个B类的对象指针p,并调用p->test()
    • 由于test函数在A类中定义,并且func函数是虚函数,虽然在B类中重写了func函数,但是虚函数的默认参数是在编译时确定的。
    • B类中的test函数调用func时,它会使用A类中定义的默认参数1(因为默认参数不是多态的一部分,编译时绑定)。
  3. 最后得出结果:
    • 程序的输出结果是B->1

2.1.6 override 和 final关键字

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由干疏忽,比如函数名写错参数写错等导致无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此C++11提供了override,可以帮助用户检测是否重写。如果我们不想让派生类重写这个虚函数,那么可以用final去修饰。

// error C3668: “Benz::Drive”: 包含重写说明符“override”的⽅法没有重写任何基类⽅法
class Car {
  public:
    virtual void Dirve()
    {}
};
class Benz : public Car {
  public:
    virtual void Drive() override {
        cout << "Benz-舒适" << endl;
    }
};
int main() {
    return 0;
}
// error C3248: “Car::Drive”: 声明为“final”的函数⽆法被“Benz::Drive”重写
class Car {
    public:
    virtual void Drive() final {}
};
class Benz : public Car {
    public:
    virtual void Drive() {
        cout << "Benz-舒适" << endl;
    }
};
int main() {
    return 0;
}

🏳️‍🌈3.纯虚函数和抽象类

在虚函数的后面写上 =0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派生类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。

class Car {
    public:
    virtual void Drive() = 0;
};
class Benz : public Car {
    public:
    virtual void Drive() {
        cout << "Benz-舒适" << endl;
    }
}
class BMW : public Car {
    public:
    virtual void Drive() {
        cout << "BMW-操控" << endl;
    }
};
int main() {
// 编译报错:error C2259: “Car”: ⽆法实例化抽象类
    Car car;
    Car* pBenz = new Benz;
    pBenz->Drive();
    Car* pBMW = new BMW;
    pBMW->Drive();
    return 0;
}

🏳️‍🌈4.多态的原理

下面编译为32位程序的运行结果是什么()
A.编译报错 B.运行报错 C.8 D. 12

class Base {
  public:
    virtual void Func1() {
        cout << "Func1()" << endl;
    }
  protected:
    int _b = 1;
    char _ch = 'x';
};
int main() {
    Base b;
    cout << sizeof(b) << endl;
    return 0;
}
  1. 首先分析Base类的结构:
    • Base类中有一个虚函数Func1。在C++中,当一个类包含虚函数时,类对象中会包含一个虚函数指针(通常是一个指针的大小,在32位系统中为4字节,在64位系统中为8字节)。
    • Base还有一个int类型的成员变量_b(通常为4字节)和一个char类型的成员变量_ch(1字节)。
    • 由于内存对齐的要求,为了提高内存访问效率,编译器会在成员变量的布局上进行调整。对于一个包含虚函数指针和intchar类型成员变量的类,在大多数编译器下,内存布局会先放置虚函数指针,然后按照内存对齐规则放置intchar类型的变量。
  2. 然后计算sizeof(b)
    • 在32位系统中:
      • 虚函数指针占4字节,int类型的_b占4字节(按照4字节对齐),char类型的_ch占1字节(总共占4字节,因为要满足4字节对齐)。
      • 所以sizeof(b)为12字节。
    • 在64位系统中:
      • 虚函数指针占8字节,int类型的_b占4字节(按照8字节对齐,这里会填充4字节),char类型的_ch占1字节(总共占8字节,因为要满足8字节对齐)。
      • 所以sizeof(b)为16字节。

所以运行结果取决于运行程序的系统是32位还是64位。如果是32位系统,结果为12;如果是64位系统,结果为16。
在这里插入图片描述

🏳️‍🌈4.2 多态的原理

从底层的角度Func函数中ptr->BuyTicket(),是如何作为ptr指向Person对象调用Person::BuyTicket,ptr指向Student对象调用Student::BuyTicket的呢?通过下图我们可以看到,满足多态条件后,底层不再是编译时通过调用对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。第一张图,ptr指向的Person对象,调用的是Person的虚函数;第二张图,ptr指向的Student对象,调用的是Student的虚函数。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

class Person {
  public:
    virtual void BuyTicket() {
        cout << "买票-全价" << endl;
    }
};
class Student : public Person {
  public:
    virtual void BuyTicket() {
        cout << "买票-打折" << endl;
    }
};
class Soldier: public Person {
  public:
    virtual void BuyTicket() {
        cout << "买票-优先" << endl;
    }
};
void Func(Person* ptr) {
// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
    ptr->BuyTicket();
}
int main() {
// 其次多态不仅仅发⽣在派⽣类对象之间,多个派⽣类继承基类,重写虚函数后
// 多态也会发⽣在多个派⽣类之间。
    Person ps;
    Student st;
    Soldier sr;
    Func(&ps);
    Func(&st);
    Func(&sr);
    return 0;
}

👥总结


本篇博文对 【C++】多态详解 做了一个较为详细的介绍,不知道对你有没有帮助呢

觉得博主写得还不错的三连支持下吧!会继续努力的~

请添加图片描述


http://www.kler.cn/a/308580.html

相关文章:

  • Redo与Undo的区别:数据库事务的恢复与撤销机制
  • kafka面试题解答(四)
  • Java面向对象编程进阶之包装类
  • 一文简单了解Android中的input流程
  • MacOS 本地生成SSH key并关联Github
  • 软件测试面试2024最新热点问题
  • mysql学习教程,从入门到精通,SQL IN BETWEEN 运算符(13)
  • 基于STM32F407ZGT6——看门狗
  • new/delete和malloc/free到底有什么区别
  • docker镜像结构
  • 代码随想录:动态规划4-5
  • Java技术深度探索:高并发场景下的线程安全与性能优化
  • java面试题-Sql 语句的执行顺序
  • 【SOP】使用MMDeploy将MMAction2的模型转换为TensorRT
  • 二叉树的前中后序遍历(递归法)( 含leetcode上三道【前中后序】遍历题目)
  • java-lambda-常用方法总结汇总
  • 【乐企】旅客运输发票接口实现
  • 第159天:安全开发-Python-协议库爆破FTPSSHRedisSMTPMYSQL等
  • 持续集成与持续交付CI/CD
  • TDengine 首席架构师肖波演讲整理:探索新型电力系统的五大关键场景与挑战
  • CentOS7下安装Ruby3.2.4的实施路径
  • LeetCode_sql_day26(184,1549,1532,1831)
  • ubuntu系统服务器离线安装python包
  • 力扣(leetcode)每日一题 2848 与车相交的点
  • 7天速成前端 ------学习日志 (继苍穹外卖之后)
  • Spire.PDF for .NET【页面设置】演示:为 PDF 添加背景颜色或背景图像