首页 > 其他分享 >第五章:多态、抽象类、虚函数、虚函数表

第五章:多态、抽象类、虚函数、虚函数表

时间:2024-06-11 21:33:50浏览次数:16  
标签:函数 派生类 多态 基类 抽象类 重写 指针

一、虚函数:

1.1虚函数的概念:

  • 被virtual修饰的类成员函数称为虚函数。
  • 通过重写虚函数,可以实现多态。

        

1.2如何重写虚函数:

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

        

1.3虚函数重写的条件:

  • 基类和派生类的虚函数需要virtual修饰。
  • 函数名、参数类型、返回列表类型都要相同

二、多态:

2.1多态的概念:

  • 通过同一个接口来操作不同的对象,并产生不同的结果。具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

2.2使用虚函数构成多态的条件:

  • 被调用的函数必须是虚函数。
  • 必须通过基类的指针或者引用调用虚函数。
  • 派生类必须对基类的虚函数进行重写。

2.3多态的案例演示1:

  • 通过基类的引用调用虚函数。
  • 通过callfun函数,callfun函数的参数是基类的引用。
class Base //基类
{
public:
    virtual void func()
    {
        cout << "Base::func()" << endl;
    }
};

class Derived : public Base //派生类
{
public:
    vittual void func()//重写虚函数
    {
        cout << "Derived::func()" << endl;
    }
};

void callFunc(Base& base)//写一个函数,参数为基类的引用或指针,如果传递的是派生类,会发生切片
{
    base.func();
}

int main() {
    Base base;
    Derived derived;

    callFunc(base);    // 调用 Base::func()
    callFunc(derived); // 调用 Derived::func()

    return 0;
}

2.4多态的案例演示2:

  • 通过指针调用虚函数实现多态。
class Base 
{
public:
    virtual void func() 
    {
        cout << "Base::func()" << endl;
    }
};

class Derived : public Base 
{
public:
    virtual void func()
    {
        cout << "Derived::func()" << endl;
    }
};

int main() {
    Base* ptrBase;//基类指针
    Derived derived;//创建派生类对象
    
    ptrBase = &derived; // 基类指针指向派生类对象
    ptrBase->func();    

    return 0;
}

2.5派生类的虚函数可以不用virtual修饰:

  • 构成多态时,派生类的虚函数可以不加virtual修饰。
  • 因为继承自基类的虚函数在派生类中自动是虚函数。
  • 换句话说,一旦一个函数在基类中被声明为虚函数,它在派生类中继续保持虚函数的属性,不需要再次使用 virtual 关键字进行修饰。

2.6涉及多态的使用,要将析构函数设置为虚函数:

  • 在C++中,如果类设计中涉及多态行为,基类的析构函数必须声明为虚函数。这是为确保通过基类指针或引用调用虚函数时,删除派生类对象时能够正确调用派生类的析构函数,从而正确释放资源并执行完整的析构过程。
  • 由于delete是按照指针的静态类型来调用析构函数的,多态传递的是基类的指针或引用,如果不将派生类的虚构函数设置为多态的虚函数,delete在释放派生类对象时也会调用基类的析构函数而不是派生类的析构函数。

2.7题目分析:

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;
}
  • 实例化一个B类对象,并用p指针指向这个对象。
  • 使用p指针调用test函数。由于p是B类指针,B类继承了A类的test函数,由于A类将test函数设置为虚函数但是B类没有重写,所以调用的test函数仍然是A类的版本,A类版本test函数会调用fun函数,而且fun函数在b类中重写了,就会调用B类的fun函数。
  • B类重写A类的fun函数,只是重写花括号中的内容,与参数的缺省值无关,所以使用的依然是A类fun函数的参数缺省值。
  • 结果为B->1。

2.8重写涉及的两个关键字override和final:

  • override 关键字用于明确地标记派生类中重写了基类虚函数的函数。
  • 当使用 override 关键字时,编译器会检查该函数是否真正地重写了基类中的虚函数,如果没有,则会发出警告或错误。
  • override 关键字有助于确保派生类正确地覆盖了基类中的虚函数,提高代码的可读性和可维护性。

        

  • final 关键字用于标记一个虚函数或类,表示该函数或类不能被派生类重写或继承。
  • 当使用 final 关键字时,编译器会阻止任何尝试重写或继承的操作,如果有尝试,则会发出编译错误。
  • final 关键字可以应用于虚函数,以防止派生类进一步重写该函数,也可以应用于类,以防止其他类继承该类。

        

2.9重写和隐藏的对比:

  • 重写函数的参数列表类型、返回类型和函数名都必须与基类中的虚函数完全相同。
  • 隐藏函数具有相同的名称,但不在意参数列表和返回类型。
  • 重写是实现多态性的一种方式,而隐藏则是一种隐藏基类实现的方式。

三、抽象类:

3.1抽象类的概念:

  • 包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。
  • 派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
  • 纯虚函数规定了:继承抽象类的派生类必须重写纯虚函数,纯虚函数更体现出了接口继承。 
  • 可以通过是否能实例化出对象,来判定一个类是不是抽象类。

        

3.2纯虚函数的概念:

  • 在虚函数的后面写上 =0 ,则这个函数为纯虚函数。

        

 3.3接口继承和实现继承:

  • 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
  • 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写而达成多态,继承的是接口。
  • 纯虚函数比虚函数更能体现接口继承。

四、虚函数表:

4.1虚函数指针和虚函数表:

  • 包含虚函数的类,会比没有虚函数的类多出一个指针,这个指针叫做虚函数表指针。
  • 虚函数表指针指向的是虚函数表。
  • 虚函数表本质是一个函数指针数组,里面存放的是当前类的所有虚函数的地址。
  • 通常情况下,虚函数表的长度是固定的,并且在编译时已经确定了。由于虚函数表的长度是固定的,因此不需要像普通的C风格的数组一样在末尾放置一个特殊的标记(例如nullptr)来表示数组的结束。

4.2派生类和虚函数表:

  • 一个派生类单继承有虚函数的基类,他只有一个虚函数表。
  • 一个派生类,如果是多继承,则他有几个基类,就会有几张虚表。
  • 当派生类继承了含有虚函数的基类时,通常会先将基类的虚函数表内容拷贝一份到派生类的虚函数表中。这样可以确保派生类的虚函数表包含基类的所有虚函数,并且保留了基类虚函数表中虚函数的顺序。
  • 如果派生类重写了基类中的某个虚函数,那么它会用自己的实现覆盖虚函数表中基类对应位置的虚函数指针。这样,在调用这个虚函数时,会调用派生类的实现而不是基类的。
  • 如果派生类新增加了虚函数,这些虚函数会按照它们在派生类中的声明顺序,依次增加到派生类虚表的末尾。这样,派生类的虚表会包含基类的虚函数以及派生类自己新增加的虚函数。

五、多态的本质:

5.1传递指针和引用:

  • 多态的本质就是通过基类指针或引用来调用派生类对象的虚函数,实现了动态绑定。
  • 当将派生类对象的地址赋给基类指针时,会发生切片,此时的切片切掉的是派生类相较于基类多出的成员,即将派生类对象截断为基类对象。但是由于派生类重写了基类的虚函数,此时基类的虚函数表仍然是派生类的虚函数表,所以通过基类指针调用的虚函数实际上是派生类的虚函数实现。 

5.2传递对象:

  • 将一个基类对象作为参数传递,而不是基类的指针或引用,会导致对象切片,此时的切片相当于将派生类赋值给基类,相当于拷贝构造,是不会拷贝虚表指针过去的,这种情况会丧失多态性。

六、细节理解:

  • 虚函数放在代码段,虚表不放在代码段。它通常放在全局数据区或静态数据区。
  • 虚函数一定是会被放进虚表的。
  • 虚表是一个指针数组,虚表指针和虚表第一个函数的地址重合。我们可以在得到虚表指针后打印所有虚表函数的地址。
  • 指针决定看一个类型要读取多长字节的数据。一个对象的指针,对他++,他会跳过一个对象的大小。如果我们要遍历一个虚表得到他的所有虚函数的地址。就需要将跳过的大小限定为4个字节。通过将对象的指针强转为int*,就可以让他每次只跳过4个字节而不是一个对象的大小。然后再遍历就能得到所有虚函数的地址。比如一个对象d,(*((int*)&d))就可以得到虚表的地址,并能够通过循环,打印出虚表所有虚函数的地址。

七、面试题:

  • . 什么是多态?
  • 2. 什么是重载、重写(覆盖)、重定义(隐藏)?
  • 3. 多态的实现原理?
  • 4. inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中去。
  • 5. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
  • 6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表 阶段才初始化的。
  • 7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。
  • 8. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针 对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函 数表中去查找。
  • 9. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况 下存在代码段(常量区)的。
  • 10. C++菱形继承的问题?虚继承的原理?
  • 11. 什么是抽象类?抽象类的作用?答:抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。

标签:函数,派生类,多态,基类,抽象类,重写,指针
From: https://blog.csdn.net/weixin_63716012/article/details/139583832

相关文章

  • 仿函数&模板特化
    仿函数基本介绍仿函数的本质就是一个类,此类中运算符重载了括号()!所以它使用起来和函数很相似,就叫做仿函数在标准库的优先级队列的类模板中有一个缺省参数叫less,这个less就是一个仿函数,它会将优先级队列变成大堆,在算法库的sort函数默认是升序,其实就是用的less......
  • CH06_函数
    CH06_函数概述作用:将一段可复用的代码封装起来,减少代码重复。一个较大的程序,一般分为若干个程序块,每个模块实现特定的功能。函数的定义函数的定义一般主要有5个步骤:返回值类型函数名参数列表函数体语句返回值语法:返回值类型函数名(参数列表){函数体语句......
  • MFC案例:利用SetTimer函数编写一个“计时器”程序
    一、希望达成效果    利用基于对话框的MFC项目,做一个一方面能够显示当前时间;另一方面在点击开始按钮时进行读秒计时,计时结果动态显示,当点击结束时读秒结束并保持最后结果。二、编程步骤及相关代码、注释   1、启动VS->创建新项目->MFC应用-项目名称:MFCtimer->......
  • 继承/多继承/菱形继承/虚继承/多态
    以下是一个简单的比喻,将多态概念与生活中的实际情况相联系:比喻:动物园的讲解员和动物表演想象一下你去了一家动物园,看到了许多不同种类的动物,如狮子、大象、猴子等。现在,动物园里有一位讲解员,他会为每种动物表演做简单的介绍。在这个场景中,我们可以将动物比作是不同的类,而每......
  • 静态数据成员/静态成员函数/运算符重载
    搭建一个货币的场景,创建一个名为RMB的类,该类具有整型私有成员变量yuan(元)、jiao(角)和fen(分),并且具有以下功能:(1)重载算术运算符+和-,使得可以对两个RMB对象进行加法和减法运算,并返回一个新的RMB对象作为结果。(2)重载关系运算符>,判断一个RMB对象是否大于另一个RMB......
  • wimlib API 提供了一系列用于处理 Windows 映像文件(.wim 文件)的函数和数据结构,使开发
    wimlibAPI提供了一系列用于处理Windows映像文件(.wim文件)的函数和数据结构,使开发人员能够在其应用程序中集成对WIM文件的创建、修改和提取功能。以下是一些常见的wimlibAPI:WIM文件的创建和初始化:wimlib_create_new_wim():创建一个新的WIM文件。wimlib_open_wim():......
  • C程序函数调用&系统调用
    理解程序的执行我们要知道CPU可以自由地访问寄存器、内存。另外,程序是由操作系统执行的,所以操作系统能够控制程序的所有执行情况,限制程序的行为。程序地执行过程:程序是一个二进制文件,包含程序的代码指令、代码中的文本信息等(参考C语言的程序的各种段)执行一个程序后,会将这个二......
  • 箭头函数
    基本用法ES6允许使用“箭头”(=>)定义函数。varf=v=>v;varf=function(v){returnv;};如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。varf=()=>5;//等同于varf=function(){return5};varsum=(num1,num2)=>nu......
  • C# 字段 属性 方法 构造函数 索引器 事件 嵌套类型 常量 运算符重载
    字段声明字段字段初始化静态字段常量字段只读字段字段的访问然而属性声明属性自动实现的属性只读属性只写属性属性的逻辑处理属性的访问修饰符属性和字段的区别属性的用途总结索引器索引器的基本语法使用索引器索引器的关键点语法参数访问和设置异常处理性能重载使用......
  • 如果引用另一个文件函数
    提问Rust如果引用另一个文件函数回答使用pubpubfnfib(n:u32)->u32{returnifn<2{n}else{fib(n-1)+fib(n-2)}}参考https://rustwiki.org/zh-CN/book/ch07-05-separating-modules-into-different-files.html#:~:text=Rust......