【C++】多态

简介: 多态是面向对象编程中的重要特性,允许通过基类引用调用派生类的具体方法,实现代码的灵活性和扩展性。其核心机制包括虚函数、动态绑定及继承。通过声明虚函数并让派生类重写这些函数,可以在运行时决定具体调用哪个版本的方法。此外,多态还涉及虚函数表(vtable)的使用,其中存储了虚函数的指针,确保调用正确的实现。为了防止资源泄露,基类的析构函数应声明为虚函数。多态的底层实现涉及对象内部的虚函数表指针,指向特定于类的虚函数表,支持动态方法解析。

前言:

OOP特性,封装、继承、多态,然多态是继承之下的一个重要的特性。

多态的介绍

  • 多态是面向对象编程中的关键概念,它允许同一个接口或父类引用指向多种实际类型,并根据实际类型来调用相应的方法。
  • 多态的实现主要依赖于继承、虚函数以及动态绑定。
  • 多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。

多态

一、多态构成的条件

  • 基类声明虚函数。
  • 派生类重写基类的虚函数。
  • 将基类指针或引用指向派生类对象,通过基类指针或引用访问虚函数.
  • 虚函数的声明方式是在成员函数的返回值类型前添加 virtual 关键字。

简单的多态实现

class base
{
   
    virtual void func()
    {
   
        cout << "void func()" << endl;
    }
};

class derive:public base
{
   
    virtual void func()
    {
   
        cout << "class derive:public base" << endl;
    }
};

二、虚函数

  • 虚函数是通过在类的成员函数声明前加上virtual关键字来定义的。
class BaseClass {
   
public:
    virtual ReturnType FunctionName(Parameters) 
    {
   
        // 函数体
    }
};

纯虚拟函数

  • 可以通过在类的虚函数前面加上关键字 = 0 来声明一个纯虚拟函数。
  • 这样声明的函数没有实现,要求派生类必须提供这个函数的定义。
class Base {
   
public:
    virtual void pureVirtualFunction() = 0; // 纯虚拟函数声明
};

class Derived : public Base {
   
public:
    void pureVirtualFunction() override {
    // 派生类提供纯虚拟函数的定义
        // 函数实现...
    }
};

虚函数重写的规则

  • 函数签名一致性:派生类重写基类虚函数时,必须保持函数签名的一致性,包括函数名、参数列表和返回类型(==协变返回类型除外==)。
  • 访问控制:重写的虚函数不能具有比基类函数更严格的访问控制。
  • override关键字C++11引入了override关键字,用于明确指出派生类中的函数是对基类虚函数的重写。如果没有正确重写基类虚函数,编译器将报错。
  • final关键字final关键字可以用于修饰虚函数,表示该虚函数在其派生类中不能被进一步重写,即锁定重写。

协变

  • 协变:基类与派生类虚函数返回值类型不同
class Person 
{
   
    public:
    virtual A* f() {
   return new A;}
};
class Student : public Person 
{
   
    public:
    virtual B* f() {
   return new B;}
};

三、重写虚构函数

  • 如果基类的析构函数不是虚函数,并且基类指针指向派生类对象时被删除。
  • 编译器将只调用基类的析构函数,忽略派生类中可能存在的额外资源。
  • 这会导致这些资源没有被正确释放,从而引起内存泄漏或其他问题。
class Person 
{
   
    public:
    virtual ~Person() {
    cout << "~Person()" << endl; }
};
class Student : public Person 
{
   
    public:
    virtual ~Student() {
    cout << "~Student()" << endl; }
};

int main()
{
   
  Person* p1 = new Person;
 Person* p2 = new Student;
 delete p1;
 delete p2;

}

注意:针对上面的代码运行我们创建了Student对象,但是没有调用~Student()

  • 如果基类的析构函数不是虚函数,并且基类指针指向派生类对象时被删除,编译器将只调用基类的析构函数,忽略派生类中可能存在的额外资源。
  • 这会导致这些资源没有被正确释放,从而引起内存泄漏或其他问题。
  • 为了确保多态环境下调用析构函数的正确性,基类的析构函数应该被声明为虚函数。

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

flowchart LR
三个概念对比-->重载
重载-->1.两个函数在同一个作用域
重载-->2.两个函数同名和同参
三个概念对比-->重写
重写-->1.两个函数分别在基类和派生类
重写-->2.函数名返回值和参数均相同协变除外
三个概念对比-->隐藏
隐藏--->1.函数名相同
隐藏--->2.两个函数分别在基类和派生类

多态的底层

class Base
{
   
public:
 virtual void Func1()
 {
   
 cout << "Func1()" << endl;
 }
private:
 int _b = 1;
};
  • 这段代码的大小在x86大小是 8,在行x64下是16

  • 除了_b成员,还多一个__vfptr放在对象的前面(注意有些 平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代 表virtual,f代表function)。

  • 一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,虚函数表也简称虚表

下段代码,运行结果:

class Base
{
   
public:
    virtual void Func1()
    {
   
        cout << "Base::Func1()" << endl;
    }
    virtual void Func2()
    {
   
        cout << "Base::Func2()" << endl;
    }
    void Func3()
    {
   
        cout << "Base::Func3()" << endl;
    }
private:
    int _b = 1;
};

class Derive : public Base
{
   
public:
    virtual void Func1()
    {
   
            cout << "Derive::Func1()" << endl;
    }
private:
    int _d = 2;
};
  • 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚 表指针也就是存在部分的另一部分是自己的成员。

  • 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表 中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数 的覆盖。重写是语法的叫法,覆盖是原理层的叫法

抛出一个问题:为什么不是子类的指针和引用和父类的对象

  1. 子类的指针或引用通常用于指向子类对象,而父类对象不包含子类特有的信息
  2. 父类类的指针和引用进行切片后,切出共有的信息,但是不进行虚表的拷贝,只进行切片,循着虚表即可访问虚函数
  3. 如果是父类的对象,要实现多态就要就进行子类虚表的拷贝,如果实现了虚表的拷贝,在调用父类成员是,就会访问子类函数

==这里值得反复理解==

  • Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。

  • 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr

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

再次抛出一个问题:虚函数存在哪的?虚表存在哪的?

  1. 虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。
  2. 对象中存的不是虚表,存的是虚表指针。

深入底层

怎么确定下面的结论真实的:

  1. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
  2. 对象中存的不是虚表,存的是虚表指针
class Person {
   
public:
    virtual void BuyTicket() {
    cout << "买票-全价" << endl; }

    virtual void Func1() 
    {
   
        cout << "Person::Func1()" << endl;
    }

    virtual void Func2() 
    {
   
        cout << "Person::Func2()" << endl;
    }

protected:
    int _a = 0;
};

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

private:
    virtual void Func3()
    {
   
        cout << "Student::Func3()" << endl;
    }
protected:
    int _b = 1;
};

void test4()
{
   
    Person ps;
    Student st;

    int a = 0;
    printf("栈:%p\n", &a);

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

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

    const char* str = "hello world";
    printf("常量区:%p\n", str);

    printf("虚表1:%p\n", *((int*)&ps));
    printf("虚表2:%p\n", *((int*)&st));
}

生成结果:

  • 再此可以推断虚表是储存在常量区的。

加上这段代码

typedef void(*FUNC_PTR) ();

void PrintVFT(FUNC_PTR* table)
{
   
    for (size_t i = 0; table[i] != nullptr; i++)
    {
   
        printf("[%d]:%p->", i, table[i]);

        FUNC_PTR f = table[i];
        f();
    }
    printf("\n");
}
  • Person类
  • Student类

多继承

class Base1 {
   
public:
 virtual void func1() {
   cout << "Base1::func1" << endl;}
 virtual void func2() {
   cout << "Base1::func2" << endl;}
private:
 int b1;
};
class Base2 {
   
public:
 virtual void func1() {
   cout << "Base2::func1" << endl;}
 virtual void func2() {
   cout << "Base2::func2" << endl;}
private:
 int b2;
};
class Derive : public Base1, public Base2 {
   
public:
 virtual void func1() {
   cout << "Derive::func1" << endl;}
 virtual void func3() {
   cout << "Derive::func3" << endl;}
private:
 int d1;
};
  • 多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
目录
相关文章
|
2月前
|
编译器 C++
C++入门12——详解多态1
C++入门12——详解多态1
47 2
C++入门12——详解多态1
|
7月前
|
C++
C++中的封装、继承与多态:深入理解与应用
C++中的封装、继承与多态:深入理解与应用
173 1
|
2月前
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
88 1
|
4月前
|
存储 编译器 C++
|
5月前
|
存储 编译器 C++
【C++】深度解剖多态(下)
【C++】深度解剖多态(下)
57 1
【C++】深度解剖多态(下)
|
5月前
|
存储 编译器 C++
|
5月前
|
机器学习/深度学习 算法 C++
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
|
5月前
|
Java 编译器 C++
【C++】深度解剖多态(上)
【C++】深度解剖多态(上)
59 2
|
5月前
|
程序员 C++
【C++】揭开C++多态的神秘面纱
【C++】揭开C++多态的神秘面纱
|
5月前
|
机器学习/深度学习 PyTorch 算法框架/工具
C++多态崩溃问题之在PyTorch中,如何定义一个简单的线性回归模型
C++多态崩溃问题之在PyTorch中,如何定义一个简单的线性回归模型