C++四大特性——多态 的总结

2023-10-31

我们都知道,C语言和C++的区别就是,C语言是面对过程的程序设计,而C++是面对对象的程序设计。

面对对象的程序设计有4大特性:分别是,抽象、封装、继承、多态。

今天我们就来总结一下多态的内容。

多态:分为静态多态和动态多态:

静态多态:编译器在编译期间完成的,编译器会根据实参类型来推断该调用哪个函数,如果有对应的函数,就调用,没有则在编译时报错。

比如一个简单的加法函数:

include<iostream>
using namespace std;

int Add(int a,int b)//1
{
    return a+b;
}

char Add(char a,char b)//2
{
    return a+b;
}

int main()
{
    cout<<Add(666,888)<<endl;//1
    cout<<Add('1','2');//2
    return 0;
}

显然,第一条语句会调用函数1,而第二条语句会调用函数2,这绝不是因为函数的声明顺序,不信你可以将顺序调过来试试。

动态多态:其实要实现动态多态,需要几个条件——即动态绑定条件:
1、虚函数。基类中必须有虚函数,在派生类中必须重写虚函数。
2、通过基类类型的指针或引用来调用虚函数。

说到这,得插播一条概念:重写——也就是基类中有一个虚函数,而在派生类中也要重写一个原型(返回值、名字、参数)都相同的虚函数。不过协变例外。

协变:是重写的特例,基类中返回值是基类类型的引用或指针,在派生类中,返回值为派生类类型的引用或指针。

//协变测试函数
#include<iostream>
using namespace std;

class Base
{
public:
    virtual Base* FunTest()
    {
        cout << "victory" << endl;
        return this;
    }
};

class Derived :public Base
{
public:
    virtual Derived* FunTest()
    {
        cout << "yeah" << endl;
        return this;
    }
};

int main()
{
    Base b;
    Derived d;

    b.FunTest();
    d.FunTest();

    return 0;
}

结果就是:
victory
yeah

看到重写的概念有没有点眼熟,没错,又是函数名称相同,这在前面已经遇到过不止一个了吧,现在来回想一下,刚刚总结过的同名隐藏,函数重载,都是这样子的吧!还是总结一下比较好吧!

同名成员函数的关系表

看完这些我们再来总结一下,什么是多态呢?
这种由于派生类重写基类方法,然后用基类引用指向派生类对象,调用方法时候会进行动态绑定,这就是多态。

在前面为了解决菱形继承的二义性问题,我们引进了虚继承的概念,也就是在菱形的前两条边上加上关键字:virtual,相同的道理,我们把这个关键字加在成员函数的前面,那么这个成员函数就成了虚函数。

那么写虚函数要注意什么呢?

1、构造函数能不能作为虚函数呢?

class Base
{
public:
    Base()
        :_b(666) //初始化列表
    {
        cout << "Base()" << endl;
    }

    int _b;
};

int main()
{
    Base b;
    b._b = 8;
    return 0;
}

当我们把Base类中的构造函数前面加上 virtual 的时候,会发现编译不过去,报的错误是“inline是构造函数的唯一合法存储类”,这又是为什么呢?

构造函数的作用我们都知道,是创建对象的,而虚函数的调用是通过对象来进行的,这是矛盾的,所以说构造函数不能声明成虚函数。

2、静态成员函数可以声明成虚函数吗?

class Base
{
public:
    static void FunTest()
    {
        cout << "victory" << endl;
    }
};

int main()
{
    Base b;
    b.FunTest();
    Base::FunTest();
    return 0;
}

当我把static前面加上virtual的时候,立马就有一道红线出现在virtual下面,编译报错是“virtual不能和static一起使用”,我们来分析一下:

如果定义成静态成员函数,那么在这个函数可以通过类名和域作用符来调用,也就是说,不用创建对象就可以调用。

虚表地址的使用必须通过对象的地址才能获取。

3、赋值运算符使用虚函数还是非虚函数呢?

class Base
{
public:
    virtual Base& operator=(const Base& b)
    {
        return *this;
    }

    int _b;
};
class Derived :public Base
{
public:
    virtual Derived& operator=(const Derived& b)
    {
        return *this;
    }

    int _d;
};

int main()
{
    Base b1;
    Derived d1;
    b1._b = 1;
    d1._d = 2;

    Base& b2 = b1;
    b2 = d1;
    Base& b3 = d1;
    d1 = b2;//编译会报错
    return 0;
}

所以说,最好不要将赋值运算符重载函数声明成虚函数。

4、析构函数最好声明成虚函数,为什么?

那么我们分别看一下把析构函数声明成虚函数和非虚函数有什么区别吧!

class Base
{
public:
    Base()
        :_b(666)
    {}

    ~Base()
    {}
private:
    int _b;
};

class Derived:public Base
{
public:
    Derived()
        :_d(888)
    {}

    ~Derived()
    {}

private:
    int _d;
};

int main()
{
    Base* pb = new Derived;
    delete pb;

    return 0;
}

这里写图片描述

class Base
{
public:
    Base()
        :_b(666)
    {}

    virtual~Base()
    {}
private:
    int _b;
};

class Derived:public Base
{
public:
    Derived()
        :_d(888)
    {}

    virtual~Derived()
    {}

private:
    int _d;
};

int main()
{
    Base* pb = new Derived;
    delete pb;

    return 0;
}

这里写图片描述

从上面的两种不同的结果可以看出。析构函数没有声明成虚函数的时候,在调用过程中会忘掉派生类的析构函数,这样就会造成内存泄漏的问题。

主要原因是我们在定义pb的时候将它定义成了Base*的类型,所以最后delete的时候就会直接调用Base类的析构函数,而我们new的时候其实new的Derived类型的,所以还会调用Derived类的构造函数,这样就会造成内存泄露。

而把析构函数声明成虚函数的时候,会有效的避免这种情况。

还有一个重点注意的地方:

不要在构造函数和虚构函数中调用虚函数!因为在构造函数和虚构函数中对象是不完整的,调用虚函数可能会出现未定义的问题!

虚表指针——>虚表

当我们在测试函数的时候会发现,一个对象好像比以前大了点:

class Base
{
public:
    Base()
        :_b(6)
    {
        cout << "Base()" << endl;
    }

    virtual~Base()
    {
        cout << "~Base()" << endl;
    }
private:
    int _b;
};

class Derived :public Base
{
public:
    Derived()
        :_d(8)
    {
        cout << "Derived()" << endl;
    }

    virtual~Derived()
    {
        cout << "~Derived()" << endl;
    }

private:
    int _d;
};

int main()
{
    Derived d;
    cout << sizeof(d) << endl;
    return 0;
}

按照我们以往的经验,Derived继承了Base的成员,因此,结果应该是8,然而,答案是12,不必怀疑,不信自己验证。那么为什么会多出四个字节呢?

那么我们来调一下内存看一下多出来的四个字节是什么?

这里写图片描述

我们可以看到,下面的两个分别是_b和_d,而上面多了一行貌似是地址的东西,那么我们就来看一下这个地址到底是啥?

class A
{
public:
    A()
        :_a(1)
        , _b(2)
    {}

    virtual void Show()
    {
        cout << "A::Show()" << endl;
    }

    ~A()
    {}

    virtual void FunTest1()
    {
        cout << "A::FunTest1()" << endl;
    } 

private:
    int _a;
    int _b;
};

typedef void(*_pFun_t)();//_pFun为一个函数指针

void PrintVfptr(_pFun_t* _pPfun)
{
    while (*_pPfun)
    {
        (*_pPfun)();//调用虚函数
        _pPfun = (_pFun_t*)((int*)_pPfun + 1);
    }
}

void Test()
{
    A a;
    cout << sizeof(A) << endl;
    _pFun_t* pPFun = (_pFun_t *)(*(int*)&a); //定义一个虚表指针
    PrintVfptr(pPFun);
}

int main()
{
    Test();
    return 0;
}

这里写图片描述

从监视窗口可以看出,——vfptr即为虚表指针,这个指针下面凡是虚函数都包含在内,然后下面是两个成员变量。

这里写图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++四大特性——多态 的总结 的相关文章

随机推荐

  • 串口服务器之虚拟串口篇

    可将TCP IP连接 Modbus Tcp映射成本地虚拟串口 本地应用 InTouch WinCC 组态王等 通过访问虚拟串口就可以完成远程监控及数据传输功能 VX 18106118736 QQ 806904723 软件使用C 语言开发 更
  • matlab pwm如何设计,原来PWM这么简单!!

    原标题 原来PWM这么简单 基本原理 PWM的全称是 脉冲宽度调制 Pulse width modulation 是通过将有效的电信号分散成离散形式从而来降低电信号所传递的平均功率的一种方式 所以根据 面积等效法则 可以通过对改变脉冲的时间
  • Java实现简单的区块链

    区块链可以简单抽象成将一个个区块存放在一个链表中 每新增一个块就把它放置在链表尾端 并通过区块之间信息的传递形成独一无二的hash 来确保区块链的数据未被篡改过 区块的实现 区块的基本属性有 当前区块的hash值 前一个区块的hash值 当
  • c语言字母意义,%C是什么意思? c语言中?和:是什么意思

    导航 网站首页 gt C是什么意思 c语言中 和 是什么意思 C是什么意思 c语言中 和 是什么意思 相关问题 匿名网友 c单个字符输出的意思 s是输出字符串 d是输出整型 f是输出整型 这是在scanf printf这样的函数中 将参数类
  • mbed TLS 概述

    系统概要 mbedtls 也许是最小巧的ssl代码库 高效 便于移植和集成 支持常见的安全算法 如 AES DES RSA ECC SHA256 MD5 BASE64等等 除此之外还支持公钥证书体系 它提供了具有直观的 API 和可读源代码
  • 解决g2o无法编译出g2o_viewer的问题

    如果你的ubuntu系统版本较高 例如Ubuntu20 04 可能在利用原始的g2o进行编译时会提示找不到 Could NOT find QGLVIEWER missing QGLVIEWER LIBRARY 解决方法1 安装必要的库 su
  • can't locate node [xxxx] in package [yyyy]

    在运行范例的launch文件时 提示 ERROR cannot launch node of type chapter2 tutorials example2 a can t locate node example2 a in packag
  • 一、为什么从IDEA打开的JavaFx scene builder,imageView没有响应呢?

    问题描述 不知道大家有没有出现和我一样的问题 就是当用idea打开JavaFx scene builder时呢 imageView是无法正常使用的 解决方法 在JavaFx scene builder中新建一个页面 完成界面设置后保存即可
  • 部分交换主元的高斯消去法

    伪代码 pseudocode 程序 function Gauss a b tol if nargin 2 tol 1e 4 end n size a 1 dimention s zeros n 1 initialize save maxim
  • NBA球员能力雷达图matplotlib,pandas

    name BaseData 球员 score BaseData 得分 assist BaseData 助攻 rebound BaseData 篮板 steal BaseData 抢断 block BaseData 盖帽 num BaseDa
  • 【registry】registry 0.9 源码 在 windows下运行

    1 概述 我在本地mac环境运行了registry源码 然后想在winodws也正常运行 于是将mac下能用的源码拷贝到了windows下 结果一堆报错 基本上全是pom问题 此处我用了2天才搞定了下面是pom整理好的拍照 慎重呀 不要轻易
  • 【图像处理】基于收缩系数的粒子群优化和引力搜索算法的多级图像阈值研究【CPSOGSA】(Matlab代码实现)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 2 运行结果 3 参考文献 4 Matlab代码及文献 1 概述 文献来源 图像分割 IS 是图像处理和计
  • 前端学习——HTML5

    新增语义化标签 新增布局标签
  • C++之对象包含与成员函数不兼容的类型限定符---补充(5)《Effective C++》

    C 值对象那个包含与成员函数不兼容的类型限定符 在上篇博客中 运行代码时候 由于没有对show函数添加const 结果突然报了一个错误 对象包含与成员函数不兼容的类型限定符 所以本篇博客进行一个快速补充 在解释这个问题之前 我们先来看看如下
  • HCM SaaS市场百舸争流,肯耐珂萨为何能立于潮头?

    科技树总在飞快生长 叠加上后疫情这个宏大的时代背景 无论是从大的千行百业 抑或是具体的企业各个业务环节的角度来看 一场声势浩大的数字化浪潮正奔涌而来 浩浩汤汤 焦距拉长 基于企业这个微观视角 相对于其他业务环节 人力资源管理的重要性越发凸显
  • jquery 中 change input 事件失效

    今天工作的时候 遇到了用jquery选择 input输入框 监听input中的内容变化 然后将input中的内容获取到 赋值给另一个input输入框 这种逻辑本来很简单的 代码如下 showProductInfo append tr td
  • Tomcat内存优化4.1 内存泄漏——内存分析工具 MAT 的使用

    在eclipse安装 使用MAT插件 简介 Eclipse提供的一个内存分析工具 它是一个功能丰富的 JAVA 堆转储文件分析工具 可以帮助你发现内存漏洞和减少内存消耗 官网地址 Eclipse Memory Analyzer Open S
  • 从零搭建完整python自动化测试框架(UI自动化和接口自动化 )——持续更新

    本自动化测试框架采用python unittest 的基础来搭建 采用PO模式 数据驱动的思想 通过selenium来实现WEB UI自动化 通过request来实现接口自动化 移动终端的自动化也可在该框架基础上去构建补充 目录 总体框架
  • 线扫相机——机器视觉中无限制物体的检测(重要转载)

    在机器视觉中 在检测连续物体或者滚动物体时 线扫相机是最佳的解决方案 通常 它们能提供很高的分辨率 因为它们要求很高的速度和数据率 一 多条窄带拼成一副图像 线扫相机只抓取一行作为图像发送到电脑 主机电脑将所有的行进行组织拼接 如果不停止抓
  • C++四大特性——多态 的总结

    我们都知道 C语言和C 的区别就是 C语言是面对过程的程序设计 而C 是面对对象的程序设计 面对对象的程序设计有4大特性 分别是 抽象 封装 继承 多态 今天我们就来总结一下多态的内容 多态 分为静态多态和动态多态 静态多态 编译器在编译期