C++ - 多态, 虚函数讲解

多态概念

多态: 通俗的说就是不同的人, 完成相同的任务会产生不同的结果.

如: 学期结束开始放假了, 我们会买回家的票,

老师买的是全价票, 而我们是学生, 学生买票是有优惠的.

C++ 中多态又分为静态多态和动态多态:

静态多态 (又称为编译时多态): 主要是依靠函数重载和模板来实现的.

动态多态 (又称为运行时多态): 主要是依靠继承和虚函数来实现的

那么这里讲的是动态多态, 即由继承和虚函数实现的多态.

虚函数

上面说的虚函数, 又是什么函数?

在成员函数中, 被 virtual 关键字修饰的就是虚函数.

cpp 复制代码
calss person
{
public:
    virtual void func()
    {
        cout << "买票" << endl;
    }

};

那么这个虚函数和普通函数有什么不一样.

这个不一样主要体现在继承中.

先了解 "虚函数重写" (又称为 覆盖)

在派生类中, 存在和基类完全相同的虚函数 (两个函数要求函数名, 返回值, 参数列表完全相同), 那么就称子类重写了父类的虚函数.

cpp 复制代码
calss person
{
public:
    virtual void func()
    {
        cout << "买票" << endl;
    }

};

class sutent : public person
{
public:
    virtual void func()
    {
        cout << "买学生票" << endl;
    }
};

在 student 继承了 person 类, 并且重新实现了虚函数 func, 那么这个 func 函数就被重写了.

多态一定是发生在继承关系中的, 并且一定是被 virtual 修饰的成员函数.

如果没有没 virtual 修饰, 那么就是 继承 那一章里讲的 "隐藏"

C++ - 继承-CSDN博客 不知道隐藏可以看看.

多态的使用

想要发生多态, 还有一个条件:

子类对象, 被赋值给父类的指针或引用

cpp 复制代码
class person
{
public:
    virtual void func()
    {
        cout << "买票" << endl;
    }

};

class sutent : public person
{
public:
    virtual void func()
    {
        cout << "买学生票" << endl;
    }
};

int main()
{
    person p;
    student s;
    p.func();
    s.func();
    // 上面两个 p和s 调用 func 函数, 并没有发生多态, 各自调用自己的 func 函数

    person* ps = &s;
    ps->func();
    // 子类对象, 被赋值给了父类的指针, 此时通过 ps 调用的就是 student 的 func 函数
    // 这种情况就是多态
    return 0;
}

在 继承 中讲过, 子类可以赋值给父类指针或引用,

并且父类指向的是子类对象的切片.

在代码中, ps 指针指向的是子类对象 s.

虽然 ps 是一个 person 父类的指针, 但指向的对象还是 student 的对象.

而此时 func 函数被重写了, 所以这里调用的就是子类的 func 函数.

打印的结果就应该是: 买票 - 买学生票 - 买学生票

多态的特殊写法

  1. 在子类中, 没有在函数前加上 virtual 修饰, 但是依然构成重载.
cpp 复制代码
class person
{
public:
    virtual void func()
    {
        cout << "买票" << endl;
    }

};

class sutent : public person
{
public:
    void func() // 依然构造重写
    {
        cout << "买学生票" << endl;
    }
};

在C++中, 只要基类中的函数使用了 virtual 修饰, 那么在子类中, 满足函数名, 返回值, 参数列表相同, 依然还是能构成重写.

但是在日常写代码过程中, 还是推荐在子类中加上 virtual, 让代码更加规整.

一定要是基类中使用 virtual 修饰, 在子类中修饰是无法构成重载的.

  1. 基类与派生类的返回值可以不相同, 但是返回值必须满足某种条件.

基类和派生类的虚函数可以有不同的返回值, 但这些返回值必须是基类返回类型的派生类型

cpp 复制代码
class A{};
class B : public A
{};


class person
{
public:
    virtual A* func()
    {
        return new A;
    }

};

class sutent : public person
{
public:
    virtual B* func()
    {
        return new B;
    }
};

多态的底层实现

多态的底层实现是依靠虚函数表来完成的.

cpp 复制代码
class A
{
public:
    virtual void func()
    {};
    
    int a;
};

int main()
{
    cout << sizeof(A);
    return 0;
}

这段代码最后打印出的结果是什么?

A 类中只有一个 int 类型的变量, 那么大小不就是 4.

实际答案应该是: 8 或 12.

在32位机器下是8, 64位机器下是12

在 A 类中, 除了 int 类型的变量 a, 还有一个隐藏的变量,

这个隐藏的变量是一个指针类型的数据. 这个指针指向了一个虚函数表.

可以看到, vfptr 这个指针, 指向一个函数数组 (虚函数表), 这个数组中就存储的各个虚函数的地址.

当实例化出来的对象调用虚函数时, 就会先通过虚函数表指针, 找到虚函数表, 然后再从虚函数表中查找要调用的虚函数的地址.

虚函数表指针: 只要一个类中由虚函数, 那么这个类实例化出来的对象就都会有一个虚函数表指针.

和有没有被继承没有关系.

那么现在我们知道了, 一个类只要有虚函数, 就会存在一个虚函数指针, 指向一个虚函数表.

cpp 复制代码
class person
{
public:
    virtual void func()
    {
    }

};

class student : public person
{
public:
    virtual void func()
    {
    }
};

int main()
{
    person p;
    student s;
    return 0;
}

通过上面的图我们可以知道, 基类和派生类所指向的虚函数表不是同一个.

基类和派生类有各自的虚函数表.

派生类中重写了虚函数 func, 子类的虚函数表中指向的 func 函数和 父类的虚函数表中指向的 func 函数的地址时不一样的. 而 func1 这个函数没有被重写, 两张虚表指向的就是同一个地址.

所以, 当子类对象去调用虚函数时, 会通过虚函数表去查找要调用的是哪个函数.

父类子类指向的函数不同, 所以执行的结果也就不同.

函数是被所有对象共享的, 所以一个类一个虚表, 而不是一个对象一个虚表.

final 和 overried 关键字

当一个类或虚函数被 final 修饰了, 那么这个类就不能再被继承, 这个虚函数不能再被重写.

cpp 复制代码
class person final
{
};

// person 被 final 修饰了, 不能被继承了, 所以这里是错误的
class student : public person
{
};
cpp 复制代码
class person
{
public:
    virtual void func() final
    {
    }

};

class student : public person
{
public:
     // 在基类 person 中 func 函数被 final 修饰了, 所以不能被重写, 这里重写是错误的
    virtual void func()
    {
    }
};

overried 关键字用于明确指出派生类中的一个函数是基类中虚函数的重写版本.

当虚函数重写错误时, 就会报错提醒我们.

cpp 复制代码
class person {
public:
    virtual void func() {
        std::cout << "person::func()" << std::endl;
    }
};

class student : public person {
public:
    void func() override { // 明确指出这是重写基类中的虚函数
        std::cout << "student::func()" << std::endl;
    }
};

抽象类和纯虚函数

在虚函数的后面写上 =0, 那么这个函数就被称为纯虚函数.

如果一个类中包含了纯虚函数, 那么这个类就被称为抽象类.

抽象类是不能实例化出对象的, 抽象类就是专门用来继承的一种特殊类.

子类在继承抽象类之后, 需要重写纯虚函数, 否者子类也会变成一个抽象类, 无法实例化出对象.

cpp 复制代码
class person
{
public:
    virtual void func() =0; // 这样就完成了一个纯虚函数的定义
                            // 而这个 person 类也就成为了抽象类, 无法实例化
};

class student : public person
{
public:
    virtual void func()
    {
    }
};

注意事项

  1. 虚函数一定要是成员函数.

定义:

虚函数(Virtual Function)是一种可以在派生类(子类)中被重写(Override)的成员函数.

所以内联函数, 静态函数, 都不能是虚函数, 因为他们都不是成员函数.

  1. 析构函数最好定义为虚函数.
cpp 复制代码
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;
    return 0;
}

如果析构函数没有写为虚函数, 那么在使用 delete p2 时,

调用的就是 person 类型的析构函数.

这样就有可能导致空间没有释放, 造成内存泄漏的问题.

当析构函数写为虚函数时, delete 调用虚构函数时,

就会发生多态, 调用的就是实际类型的析构函数, 就能正确的释放空间.

  1. 构造函数不能时析构函数

首先: 一个对象要被实例化出来, 就需要调用析构函数进行初始化,

而想要发生多态, 就需要通过虚函数指针访问虚函数表, 找到要执行的虚函数

但是, 此时对象还没有没创建出来, 那么虚函数表指针也就没有被创建,

既然没有虚函数表指针, 怎么可能发生多态.

综上: 在执行构造函数时, 虚函数表指针还没有被创建, 那么就找不到对应的虚函数.

当构造函数设为虚函数后, 想要执行构造函数, 就需要虚函数表指针, 虚函数表指针又需要构造函数初始化. 形成了一个死循环.

结论: 构造函数不能被谁为虚函数

相关推荐
努力学习编程的伍大侠3 分钟前
基础排序算法
数据结构·c++·算法
数据小爬虫@17 分钟前
如何高效利用Python爬虫按关键字搜索苏宁商品
开发语言·爬虫·python
ZJ_.19 分钟前
WPSJS:让 WPS 办公与 JavaScript 完美联动
开发语言·前端·javascript·vscode·ecmascript·wps
Narutolxy25 分钟前
深入探讨 Go 中的高级表单验证与翻译:Gin 与 Validator 的实践之道20241223
开发语言·golang·gin
Hello.Reader32 分钟前
全面解析 Golang Gin 框架
开发语言·golang·gin
禁默43 分钟前
深入浅出:AWT的基本组件及其应用
java·开发语言·界面编程
yuyanjingtao1 小时前
CCF-GESP 等级考试 2023年9月认证C++四级真题解析
c++·青少年编程·gesp·csp-j/s·编程等级考试
Code哈哈笑1 小时前
【Java 学习】深度剖析Java多态:从向上转型到向下转型,解锁动态绑定的奥秘,让代码更优雅灵活
java·开发语言·学习
程序猿进阶1 小时前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
qq_433618441 小时前
shell 编程(二)
开发语言·bash·shell