C++多态

雾桩索引
• 阅读 544

什么是多态

  • 多态是指调用同一个函数时,出现不同的效果。

多态分类

  • 静态的多态:函数重载就是静态的多态
  • 动态的多态:用父类的指针或引用调用虚函数

虚函数

  • 虚函数是指:类的成员函数被 virtual 修饰的函数(普通函数不能用virtual修饰)
  • 注意:

    • 内联函数实际是没有地址的,但是如果内联函数被virtual修饰,则编译器会忽略函数的内联属性,把它当做正常函数使用。
    • 静态成员函数是不能作为虚函数的,因为静态函数调用时不会隐式传入this指针,所以静态成员函数无法放入虚函数表。
    • 构造函数不能是虚函数,因为构造函数阶段,虚表还没有初始化,所以构造函数不能是虚函数。
  • 有虚函数的类,编译器会自动增加一个成员变量(虚表指针,指向虚表(函数指针数组))

    • 例如下列类,sizeof(A) == 12
    • 实际的成员变量为:1个指针类型,一个int类型,一个char类型
    • 根据对齐规则,所以类的大小为 12 字节。
class A
{
public:
    virtual void test()   //这个就是虚函数
    {}
    int a;
    char b;
}

继承中构成多态的条件

  • 必须通过父类的指针或引用调用虚函数
  • 被调用的函数必须是虚函数,且子类必须对虚函数进行覆盖。

    • 注意:虚函数的覆盖,必须是返回类型、函数名、参数列表都相同才行。
    • 如果返回值是父类、子类的指针或引用时,返回值不同也可以构成虚函数覆盖(协变)
    • 如果父类的析构函数为虚函数,则子类的析构函数只要定义(无论是否virtual修饰),都与父类的虚析构函数构成覆盖。(析构函数本身函数名就不相同)
class A
{
public:
    virtual void test()   //这个就是虚函数
    {}
}

class B
{
  public:
    virtual void test()   //覆盖父类的虚函数
    {}  
}

虚函数的重写(覆盖)

  • 子类会继承父类的虚函数,如果子类有父类虚函数的重写时,就会将重写的虚函数地址覆盖父类虚函数的地址。

C++多态

  • 注意:虚函数的重写,是重新实现父类的虚函数,所以重写的函数参数要使用父类的参数。
  • 下列代码运行结果为:B->0
class A
{
public:
    virtual void test1(int a = 0)
    {
        cout << "A->" << a << endl;
    }
    virtual void test()   //父类函数被子类调用
    {
        test1(); //这里其实隐含了一个this,即this->test1()
          //子类调用test,传递指针p,所以这里是 p->test1()
    }
    
};

class B :public A
{
public:
    virtual void test1(int a = 1)
    {
        cout << "B->" << a << endl; 
              //理论上这里应该是输出B->1
              //但是这个函数重写父类的函数,是实现父类的函数
              //所以a在这里要使用父类的a = 0
    }
};


int main()
{
    B* p = new B;  //创建子类
    p->test();    //子类调用父类成员
    
    return 0;
}

虚析构函数的覆盖

  • 下列 类A 的析构函数被声明为虚函数,子类的析构函数与父类构成覆盖
class A
{
public: 
    virtual ~A()    //父类的析构函数声明为虚函数
    {
        cout << "~A" << endl;
    }
};

class B :public A
{
public:
    ~B()   //与父类的虚析构函数构成覆盖
    {
        cout << "~B" << endl;
    }
};

int main()
{
    A* p1 = new A;
    A* p2 = new B;
    delete p1;
    delete p2;
    return 0;
}
  • 构成覆盖后,delete p2时,会先调用子类的析构函数,再调用父类的析构函数。
  • 如果没有构成覆盖,则delete p2时只会调用父类的析构函数。
    C++多态

关键字final和override

  • final:修饰虚函数,使虚函数不能被覆盖。(加在父类中)

    • final修饰类时,表示这个类不能被继承。
  • override:修饰虚函数,检测是否正确覆盖。(加在子类中)
class A
{
public:
    virtual void test()final   //使该虚函数不能覆盖
    {
    }
}

class B: public A
{
public:
    virtual void test()override //检查虚函数是否正确覆盖
    {}
}

重载、覆盖、隐藏的对比

  • 重载:

    • 两个函数在同一作用域
    • 函数名相同,参数不同
  • 覆盖:

    • 两个函数分别在父类和子类中
    • 函数名、参数、返回值必须相同(协变除外)
    • 两个函数必须是虚函数
  • 隐藏:

    • 父类和子类拥有同名成员,当调用这些同名成员时,无法确定调用的是父类还是子类
    • 所以子类不能直接调用父类的成员,必须指明这个成员属于哪个类。
    • 即子类成员屏蔽了父类成员的直接访问(这就是隐藏)

    纯虚函数(抽象类)

  • 在虚函数后面加上 =0,则这个函数为纯虚函数。
  • 包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化出对象。
  • 子类继承抽象类后也不能实例化出对象,只有覆盖(重写)纯虚函数,子类才能实例化出对象。
  • 纯虚函数强制子类必须覆盖函数,更加体现出接口继承。
  • 纯虚函数不需要实现功能,只需要声明(重写时再实现功能)
class A  //抽象类
{
public:
    virtual void test() = 0;   //纯虚函数

}

虚表与虚表指针

  • 虚表全称虚函数表,用来存放类中所有虚函数的指针,一个类只会有一份虚表(无论创建多少个对象)
  • 虚表会在编译阶段生成,在运行阶段使用。
  • 虚表指针全称虚函数表指针,指向虚函数表。
  • 虚表指针是编译器自动创建的,在程序运行时,会根据对象类型去初始化虚表指针,从而让虚表指针指向所属类的虚表。
  • 注意:对象的前四个字节就是虚表的地址,虚表存放在常量区(虚表是不能人为更改的)

多态的示例

class A
{
public:
    virtual void test() //父类虚函数
    {
        cout << "A的类" << endl;
    }
};

class B :public A
{
public:
    virtual void test()   //虚函数的覆盖
    {
        cout << "B的类" << endl;
    }
};

void f1(A& p)  //使用引用
{
    p.test();
}

void f2(A p)
{
    p.test();
}

int main()
{
    A aa;
    B bb;
    f1(aa);   //输出 A的类
    f1(bb);   //输出 B的类

    f2(aa);   //输出 A的类
    f2(bb);   //输出 A的类
    return 0;
}
  • void f1(A& p) 使用父类的引用,引用无需拷贝,直接将传递的参数拿来使用,所以可以分别调用父类的 test() 和子类的 test() 。
  • void f2(A p) 使用父类的对象,相当于值传递,需要拷贝出一个临时变量,而拷贝时是不会拷贝虚表指针,虚表指针在程序运行时根据对象类型去初始化(对象p的类型是 A ,所以虚表指针指向 A的虚表)
  • 所以 void f2(A p) 函数中,无论传递哪种子类的对象,最终调用的虚函数都是会是 A 父类的虚函数。
    C++多态
点赞
收藏
评论区
推荐文章
cpp加油站 cpp加油站
4年前
c++头脑风暴-多态、虚继承、多重继承内存布局
本篇文章深入分析多态、虚继承、多重继承的内存布局以及实现原理。首先还是看一下思维导图:下面根据这个大纲一步一步的进行深入解析。一、没有虚函数时内存布局是怎样的1.没有虚函数时类的内存布局一个类没有虚函数的时候,其实就是结构体,它的内存布局就是按照成员变量的顺序来的。看如下代码:cppinclude<iostreamusingnamespaces
Wesley13 Wesley13
4年前
java实现多态中的虚函数相关概念
本文转载自参考博客1\.Java虚函数虚函数的存在是为了多态。C中普通成员函数加上virtual关键字就成为虚函数Java中其实没有虚函数的概念,它的普通函数就相当于C的虚函数,动态绑定是Java的默认行为。如果Java中不希望某个函数具有虚函数特性,可以加上final关键字变成非虚函数PS:其实C和Java在虚函
小万哥 小万哥
2年前
C++虚函数详解:多态性实现原理及其在面向对象编程中的应用
在面向对象的编程中,多态性是一个非常重要的概念。多态性意味着在不同的上下文中使用同一对象时,可以产生不同的行为。C是一种面向对象的编程语言,在C中,虚函数是实现多态性的关键什么是虚函数虚函数是一个在基类中声明的函数,它可以被子类重写并提供不同的实现
Wesley13 Wesley13
4年前
C++中基类虚析构函数的作用及其原理分析
虚析构函数的理论前提是执行完子类的析构函数,那么父类的虚构函数必然会被执行。那么当用delete释放一个父类指针所实例化的子类对象时,如果没有定义虚析构函数,那么将只会调用父类的析构函数,而不会调用子类的虚构函数,导致内存的泄漏。故: 继承时,要养成的一个好习惯就是,基类析构函数中,加上virtual。知识背景     
Wesley13 Wesley13
4年前
C++课程第五次博客——多态
\TOC\多态性Part1多态性概述多态是指同样的消息被不同类型的对象接收时导致不同的行为。在C中,所谓信息是指对类的成员函数的调用,不同的行为是指不同的实现,也就是调用了不同的函数。1)多态的类型分为四类:重载多态,强制多态,包含多态和参数多态。前两者为专用多态,而后者称为通用多态。2)
Stella981 Stella981
4年前
C++primer学习笔记(六)
1.virtual函数是基类希望派生类重新定义的函数,希望派生类继承的函数不能为虚函数。根类一般要定义虚析构函数。2.派生类只能通过派生类对象访问protected成员,不能用基类对象访问。基类定义为virtual就一直为虚函数,派生类写不写virtual都是虚函数。用做基类的类必须是已定义的。3.存在虚函数指针或引用
Wesley13 Wesley13
4年前
C++基类的析构函数定义为虚函数的原因
1:每个析构函数只会清理自己的成员(成员函数前没有virtual)。2:可能是基类的指针指向派生类的对象,当析构一个指向派生类的成员的基类指针,这时程序不知道这么办,可能会造成内存的泄露,因此此时基类的析构函数要定义为虚函数;基类指针可以指向派生类的对象(多态),如果删除该指针delete\\p,就会调用该指针指向的派生类的析构函数,而派生类
Wesley13 Wesley13
4年前
C++多态性总结
一,C多态性概述多态是指同样的消息被不同类型的对象接受时导致不同的行为。所谓消息是指对类的成员函数的调用,不同的行为是指不同的实现,也就调用不同的函数。换言之,多态指的就是用同样的接口访问功能不同的函数,从而实现“一个接口,多种方法”。二,多态性分类!在这里插入图片描述(https://osc
Wesley13 Wesley13
4年前
C++多态性与虚函数
  派生一个类的原因并非总是为了继承或是添加新的成员,有时是为了重新定义基类的成员,使得基类成员“获得新生”。面向对象的程序设计真正的力量不仅仅是继承,而且还在于允许派生类对象像基类对象一样处理,其核心机制就是多态和动态联编。(一)多态性  多态是指同样的消息被不同的对象接收时导致不同的行为。所谓消息是指对类成员函数的调用,不同的行为是指的不同的实现
小万哥 小万哥
2年前
Python 中多态性的示例和类的继承多态性
单词"多态"意味着"多种形式",在编程中,它指的是具有相同名称的方法/函数/操作符,可以在许多不同的对象或类上执行。函数多态性一个示例是Python中的len()函数,它可以用于不同的对象。字符串对于字符串,len()返回字符的数量:示例pythonx"H
小万哥 小万哥
1年前
深入理解 C++ 中的多态与文件操作
C多态多态(Polymorphism)是面向对象编程(OOP)的核心概念之一,它允许对象在相同操作下表现出不同的行为。在C中,多态通常通过继承和虚函数来实现。理解多态想象一个场景,你有一个动物园,里面有各种动物,如猫、狗、鸟等。每个动物都有自己的叫