C++ 超全面讲解多态

目录
  • 多态的概念
  • 多态的定义及实现
    • 构成条件
    • 虚函数
      • 虚函数的重写
      • 虚函数重写的两个例外
  • 抽象类
    • 抽象类的概念
    • 接口继承和实现继承
  • 多态的原理
    • 虚函数表
    • 多态的原理

多态的概念

概念:通俗的来说就是多种形态,具体就是去完成某个行为,当不同类型的对象去完成同一件事时,产生的动作是不一样的,结果也是不一样的。

举一个现实中的例子:买票这个行为,当普通人买票时是全价;学生是半价;军人是不需要排队。

多态也分为两种:

  • 静态的多态:函数调用
  • 动态的多态:父类指针或引用调用重写虚函数。

这里的静态是指在编译时实现多态的,而动态是在运行时完成的。

多态的定义及实现

构成条件

多态一定是建立在继承上的,那么除了继承还要两个条件:

  • 必须通过基类(父类)的指针或引用调用函数
  • 被调用的函数必须是虚函数,且派生类(子类)必须对积累的虚函数进行重写。

虚函数

概念:被virtual修饰的类成员函数称为虚函数

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

注意:

  • 只有类的非静态成员函数可以是虚函数
  • 虚函数这里virtual和虚继承中用的是同一个关键字,但是他们之间没有关系;虚函数这里是为了实现多态;虚继承是为了解决菱形继承的数据冗余和二义性,它们没有关联

虚函数的重写

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

例:

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 f(Person& p)//这块的参数必须是引用或者指针
{
    p.BuyTicket();
}
​
int main()
{
    Person p;
    Student st;
    Soldier so;

    f(p);
    f(st);
    f(so);

    return 0;
}

注意:这里子函数的虚函数可以不加virtual,也算完成了重写,但是父类的虚函数必须要加,因为子类是先继承父类的虚函数,继承下来后就有了virtual属性了,子类只是重写这个virtual函数;除了这个原因之外,还有一个原因,如果父类的析构函数加了virtual,子类加不加都一定完成了重写,就保证了delete时一定能实现多态的正确调用析构函数。

虚函数重写的两个例外

1、协变

概念:派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变

例:

class A{};
class B : public A{};
​
class Person
{
public:
    virtual A* f()
    {
        return new A;
    }
};
​
class Student : public Person
{
public:
    virtual B* f()           //返回值不同但是构成虚函数重写
    {
        return new B;
    }
};

2、析构函数的重写

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

例:

class Person {
public:
    //建议把父类析构函数定义为虚函数,这样方便子类的虚函数重写父类的虚函数
    virtual ~Person() {cout << "~Person()" << endl;}
};
​
class Student : public Person {
public:
    virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
    Person* p1 = new Person;
   //这里p2指向的子类对象,应该调用子类析构函数,如果没有调用的话,就可能内存泄漏
    Person* p2 = new Student;
    //多态行为
    delete p1;
    delete p2;
    //只有析构函数重写了那么这里delete父类指针调用析构函数才能实现多态。
    return 0;
}

C++11 override和finel

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写

final:修饰虚函数,表示该虚函数不能再被重写

class Car
{
public:
    virtual void Drive() final {}
};
class Benz :public Car
{
public:
    //会在这块报错,因为基类的虚函数已经被final修饰,不能被重写了
    virtual void Drive() {cout << "Benz-舒适" << endl;}
};  

override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

class Car{
public:
    virtual void Drive(){}
};
class Benz :public Car {
public:
    virtual void Drive() override {cout << "Benz-舒适" << endl;}
};  

重载、覆盖(重写)、隐藏(重定义)的对比

抽象类

抽象类的概念

纯虚函数:在虚函数的后面加上=0就是纯虚函数,有纯虚函数的类就是抽象类,也叫接口类,抽象类无法实例化对象。抽象类的子类不重写父类的虚函数的话,也是一个抽象类。

//抽象类的定义
class Car
{
public:
    virtual void run()=0;   //不用实现只写接口就行。
};

纯虚函数不写函数体,并不意味着不能实现,只是我们不写。因为写出来也没有人用。

虚函数的作用

  • 强制子类重写虚函数,完成多态。
  • 表示抽象类。

接口继承和实现继承

普通函数的继承就是实现继承,虚函数的继承就是接口继承。子类继承了函数的实现,可以直接使用。虚函数重写后只会继承接口,重写实现。所以如果不用多态,就不要把函数写为虚函数。

纯虚函数就体现了接口函数。下面我们来实现一道题,展现一下接口继承。

class A
{
public:
    virtual void fun(int val=0)
    {
        cout<<"A->val = "<<val <<endl;
    }
    void Fun()
    {
        fun();
    }
};
​
class B:public A
{
public:
    virtual void fun(int val=1)
    {
        cout<<"B->val"<<val<<endl;
    }
};
​
int main()
{
    B b;
    A* a=&b;
    a->Fun();
    return 0;
}

结果打印为 :B->val=0

子类对象切片给父类指针,传给Fun函数,满足多态,会去调用子类的fun函数,但是子类的虚函数继承了父类的接口,所以val是父类的0。

多态的原理

虚函数表

class A
{
public:
    virtual void fun()
    {

    }
    protected:
    int _a;
};

sizeof(A)是多少?

打印出来是8。

我们定义了一个A类型的对象a,打开调试窗口,发现a的内容如下

我们发现出了成员变量_a以外,还多了一个指针,这个指针是不准确的,实际上应该是 _vftptr(virtual function table pointer),虚函数表指针。在计算类大小的时候要加上这个指针的大小。虚表就是存放虚函数的地址地方,当我们去调用虚函数,编译器就会通过虚表指针去虚表里查找。

class A
{
public:
    void fun1()
    {

    }
    virtual void fun2()
    {}
};
​
int main()
{
    A* a=nullptr;
    a->fun1();//调用函数,因为这是普通函数的调用
    a->fun2();//调用失败,虚函数需要对指针操作,无法操作空指针。
    return 0;
}

实现一个继承

class A
{
    public:
    virtual void fun1()
    {}
    virtual void fun2()
    {}
};
class B : public A
{
    public:
    virtual void fun1()
    {}
    virtual void fun2()
    {}
};
​
int main()
{
    A a;
    B b;
    return 0;
}

子类与父类一样有一个虚表指针。

子类的虚函数表一部分继承自父类。如果重写了虚函数,那么子类的虚函数会在虚表上覆盖父类的虚函数。

本质上虚函数表是一个虚函数指针数组,最后一个元素是nullptr,代表虚表的结束。所以,如果继承了虚函数,那么

  • 子类先拷贝一份父类虚表,然后用一个虚表指针指向这个虚表。
  • 如果有虚函数重写,那么在子类的虚表上用子类的虚函数覆盖。
  • 子类新增的虚函数按其在子类中的声明次序增加到子类虚表的最后。

虚函数表放在内存的那个区,虚函数又放在哪?

虚函数与虚函数表都放在代码段。

多态的原理

我们现在来看多态的原理

class person
{
public:
    virtual void fun()
    {
        cout<<"全价票"<<endl;
    }
};
class student : public person
{
public:
    virtual void fun()
    {
        cout<<"半价票"<<endl;
    }
};
void buyticket(person* p)
{
    p->fun();
}

这样就实现了不同对象去调用同一函数,展现出不同的形态。 满足多态的函数调用是程序运行是去对象的虚表查找的,而虚表是在编译时确定的。 普通函数的调用是编译时就确定的。

动态绑定与静态绑定

1.静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载

2.动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。我们说的多态一般是指动态多态。

这里我附上一个有意思的问题:

就是在子类已经覆盖了父类的虚函数的情况下,为什么子类还是可以调用“被覆盖”的父类的虚函数呢?

#include <iostream>
using namespace std;
​
class Base {
public:
    virtual void func() {
        cout << "Base func\n";
    }
};
​
class Son : public Base {
public:
    void func() {
        Base::func();
        cout << "Son func\n";
    }
};
​
int main()
{
    Son b;
    b.func();
    return 0;
}

输出:

Base func

Son func

这是C++提供的一个回避虚函数的机制

通过加作用域(正如你所尝试的),使得函数在编译时就绑定。

到此这篇关于C++ 超全面讲解多态的文章就介绍到这了,更多相关C++ 多态内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++ 超详细分析多态的原理与实现

    目录 多态的定义及实现 多态的构成条件 虚函数重写 C++11的override和final 抽象类 多态的原理 虚函数表 动态绑定与静态绑定 单继承和多继承关系的虚函数表 单继承中的虚函数表 多继承中的虚函数表 常见问题 多态的定义及实现 多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态. 比如买票这个行为,当普通人买票时,是全价买票:学生买票时,是半价买票:军人买票时是优先买票. 多态的构成条件 多态是在不同继承关系的类对象,去调用同一函数

  • C/C++使用C语言实现多态

    目录 1.多态的概念 1.1什么是多态? 1.2为什么要用多态呢? 1.3多态有什么好处? 2.多态的定义及实现 2.1继承中构成多态的条件 2.2虚函数 2.3虚函数的重写 2.4C++11 override 和 final 2.5 重载.覆盖(重写).隐藏(重定义)的对比 3.抽象类 3.1概念 3.2实现继承和接口继承 4.多态的原理 4.1虚函数表 4.2多态的原理 4.3 动态绑定与静态绑定 5.单继承和多继承关系的虚函数表 5.1 单继承中的虚函数表 5.2 多继承中的虚函数表 总结

  • C/C++中多态性详解及其作用介绍

    目录 概述 静态多态 函数重载 运算符重载 动态多态 非动态 动态 概述 多态性 (polymorphism) 是面向对象程序设计的一个重要特征. 利用多态性扩展设计和实现一个易于扩展的系统. C++ 中多态性: 同一函数名可以实现不同的功能 用一个函数名调用不同内容的函数完成不同的工作 静态多态 静态多态 (static polymorphism) 是通过函数的重载实现的, 包括函数的重载和运算符重载. 在程序编译时系统就能觉得调用哪个函数. 函数重载 int main() { cout <<

  • C++类和对象之多态详解

    目录 多态基本概念和原理剖析 多态案例1 计算器类 纯虚函数和抽象类 多态案例2 制作饮品 虚析构和纯虚析构 多态案例3 电脑组装 多态基本概念和原理剖析 多态:多态是C++面向对象的三大特性之一.多态分为静态多态和动态多态. 静态多态:函数重载和运算符重载属于静态多态,复用函数名. 动态多态:派生类和虚函数实现运行时多态. 区别: 静态多态的函数地址早绑定,编译阶段确定函数地址. 动态多态的函数地址晚绑定,运行阶段确定函数地址. #include <iostream> using names

  • C++的多态与虚函数你了解吗

    目录 多态性 虚函数 总结 多态性 多态性是面向对象程序设计的关键技术之一,若程序设计语言不支持多态性,不能称为面向对象的语言,利用多态性技术,可以调用同一个函数名的函数,实现完全不同的功能 在C++中有两种多态性: 编译时的多态 通过函数的重载和运算符的重载来实现的 运行时的多态性 运行时的多态性是指在程序执行前,无法根据函数名和参数来确定该调用哪一个函数,必须在程序执行过程中,根据执行的具体情况来动态地确定:它是通过类继承关系public和虚函数来实现的,目的也是建立一种通用的程序:通用性是

  • C++关于指针,继承和多态介绍

    目录 指针 指针和继承 指针.继承和多态 指针 我们在书本上学到的指针基本上都是:首先,指针是一个变量:其次,这个变量存储的值是一个地址.这个是对指针的一个基本理解,最近在编程中发现了一些新的东西. 首先,指针不仅仅是一个地址,还存在一个和所指向内容大小相关的值,如下代码: #include<iostream> using namespace std; int main() { int a = 10; int *pa = &a; cout << "pa: &quo

  • C++多态实现方式详情

    注:文章转自公众号:Coder梁(ID:Coder_LT) 在我们之前介绍的继承的情况当中,派生类调用基类的方法都是不做任何改动的调用. 但有的时候会有一些特殊的情况,我们会希望同一个方法在不同的派生类当中的行为是不同的.举个简单的例子,比如speak方法,在不同的类当中的实现肯定是不同的.如果是Human类,就是正常的说话,如果是Dog类可能是汪汪,而Cat类则是喵喵. 在这种情况下只是简单地使用继承是无法满足我们的要求的,最好能够有一个机制可以让方法有多种形态,不同的对象去调用的逻辑不同.这

  • C++数据结构分析多态的实现与原理及抽象类

    目录 多态的 概念 虚函数 多态构成的条件 C++11override和final 重载.重写和重定义(隐藏) 抽象类 多态的原理

  • C++ 超全面讲解多态

    目录 多态的概念 多态的定义及实现 构成条件 虚函数 虚函数的重写 虚函数重写的两个例外 抽象类 抽象类的概念 接口继承和实现继承 多态的原理 虚函数表 多态的原理 多态的概念 概念:通俗的来说就是多种形态,具体就是去完成某个行为,当不同类型的对象去完成同一件事时,产生的动作是不一样的,结果也是不一样的. 举一个现实中的例子:买票这个行为,当普通人买票时是全价:学生是半价:军人是不需要排队. 多态也分为两种: 静态的多态:函数调用 动态的多态:父类指针或引用调用重写虚函数. 这里的静态是指在编译

  • Java超详细讲解多态的调用

    概念:多态是什么它就相当于区别对待,比如买票这个行为,当普通人买票时,是全价买票:学生买票时,是半价买票:军人买票时是优 先买票.再者就是再举个详细的例子: 最近为了争夺在线支付市场,支付宝年底经常会做诱人的扫红包-支付-给奖励金的活动.那么 大家想想为什么有人扫的红包又大又新鲜8块.10块…,而有人扫的红包都是1毛,5毛….其实这背后也是 一个多态行为.支付宝首先会分析你的账户数据,比如你是新用户.比如你没有经常支付宝支付等等,那么 你需要被鼓励使用支付宝,那么就你扫码金额 = random(

  • Java超详细讲解三大特性之一的多态

    目录 多态性 instanceof 关键字的使用 ==和equals()区别 object类中toString()的使用 static关键字的使用 总结 多态性 1理解多态性:可以理解为一个事物的多种形态. 2何为多态性:对象的多态性:父类的引用指向子类的对象(或子类的对象赋给父类的引用) 3多态的使用:虚拟方法调用,有了对象的多态性以后,我们在编译期,只能调用父类中声明的方法,但在运行期,我们实际执行的是子类重写父类的方法,多态性的使用前提:类的继承关系,方法的重写,总结:编译,看左边,运行,

  • Java 类与对象超基础讲解

    目录 什么是面向对象 面向过程与面向对象 类与对象的使用 类与对象的使用与说明 对象的初始化 this的使用 构造方法 this可以调用本类的构造方法 什么是面向对象 Java语言是一个纯面向对象的语言,面向对象的语言不仅只有Java,包括C++,PHP等 面向对象的编程思想简称 OOP(Object Oriented Programming),其基本特点就是封装,继承和多态. 面向过程与面向对象 想要弄清楚什么是面向对象,首先需要知道两者的区别 面向过程更注重程序的每一个步骤,用相应的函数来实

  • Java超详细讲解类的继承

    目录 写在前面 1.子类的创建 1.1子类的创建方法 1.2调用父类中特定的构造方法 2.在子类中访问父类成员 3.覆盖 3.1覆盖父类中的方法 3.2用父类的对象访问子类的成员 4.不可被继承的成员和最终类 实例java代码 写在前面 类的继承可以在已有类的基础上派生出来新的类,不需要编写重复的代码,提高了代码的复用性,是面向对象程序设计的一个重要的特点,被继承的类叫做父类,由继承产生的新的类叫做子类,一个父类可以通过继承产生多个子类,但是与C++不同的是Java语言不支持多重继承,即不能由多

  • C++超详细讲解隐藏私有属性和方法的两种实现方式

    目录 例子 用抽象类解决问题 用Pimpl风格解决问题 总结 参考 在我们编写程序的时候,会将程序模块化,常见的就是用动态链接库的方式,然后导出函数接口或者类.而对于导出类的方式,作为模块的实现者,不论是给第三方使用或者自己的项目使用,应该都不太愿意暴露自己的私有属性和方法,个人碰到的主要有以下两个常见原因: 通过隐藏私有属性和方法,让被调用者猜不到其实现方式 私有方法中或者属性中,可能会存在一些第三方的头文件或者库的依赖,而对于被调用方来说不应该直接依赖 本文将介绍两种方式来满足以上的需求,一

  • Java超详细讲解三大特性之一的封装

    目录 封装 封装的概念 Java中的包 java中类的成员-构造器 java中的this关键字 总结 说到面向对象则不得不提面向对象的三大特征:封装,继承,多态.那么今天就和大家先来介绍什么是封装. 封装 封装的概念 将类的某些信息隐藏在类的内部,不允许外部程序直接访问,而是通过该类提供的方法来对隐藏的信息进行操作和访问. 为什么需要封装? 当我们创建一个类的对象后,我们可以通过“对象.属性”的方式,对对象的属性进行赋值.这里赋值操作要受到 属性的数据类型和存储范围的制约.除此之外,没有其他制约

  • C++超详细讲解强制类型转换的用法

    目录 static_cast dynamic_cast const_cast reinterpret_cast static_cast static_cast<type-id>(expression) 将 expression 转换为 type-id 类型.static_cast 是静态类型转换,发生在编译期.这种转换不会进行运行时的动态检查(RTTI),因而这种转换可能是不安全的.static_cast 典型应用场景如下: 1. 类的层级结构中,基类和子类之间指针或者引用的转换. 上行转换(

  • java反射超详细讲解

    目录 Java反射超详解✌ 1.反射基础 1.1Class类 1.2类加载 2.反射的使用 2.1Class对象的获取 2.2Constructor类及其用法 2.4Method类及其用法 Java反射超详解✌ 1.反射基础 Java反射机制是在程序的运行过程中,对于任何一个类,都能够知道它的所有属性和方法:对于任意一个对象,都能够知道它的任意属性和方法,这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制. Java反射机制主要提供以下这几个功能: 在运行时判断任意一个对象所属

  • 超详细讲解Linux C++多线程同步的方式

    目录 一.互斥锁 1.互斥锁的初始化 2.互斥锁的相关属性及分类 3,测试加锁函数 二.条件变量 1.条件变量的相关函数 1)初始化的销毁读写锁 2)以写的方式获取锁,以读的方式获取锁,释放读写锁 四.信号量 1)信号量初始化 2)信号量值的加减 3)对信号量进行清理 背景问题:在特定的应用场景下,多线程不进行同步会造成什么问题? 通过多线程模拟多窗口售票为例: #include <iostream> #include<pthread.h> #include<stdio.h&

随机推荐