第五章 C++多态与虚函数

以下是本人整理的C++基础知识点,内容并不包含全面的C++知识,只是对C++重点内容、特点进行整理和归纳。

5.1 C++多态和虚函数介绍

虚函数的作用
    让基类指针能够访问派生类的成员函数
    构成多态

虚函数声明
    在基类成员函数的声明前面增加 virtual 关键字
        定义时不用


虚函数产生多态的原因
    有了虚函数,当基类指针指向基类对象时就使用基类成员(函数和变量),指向派生类对象时就使用派生类成员
    虚函数的存在,基类指针有了多种形态或表现方式,所以称为多态

指针多态和引用多态
    指针多态:指向灵活,多态一般是说指针
    引用多态:指向单一,多态体现不足

多态的用途
    一个指针变量 p 就可以调用所有派生类的虚函数,减少指针数量

5.2 C++虚函数注意事项以及构成多态的条件

虚函数的注意事项
    只在声明处加 virtual ,定义处不用
    只需将基类中的函数声明为虚函数
        这样所有派生类中具有遮蔽关系的同名函数都将自动成为虚函数
    只有派生类的同名函数覆盖基类的虚函数,并且函数原型相同才能构成多态
    
    将构造函数声明为虚函数没有意义
    析构函数可以声明为虚函数,而且有时候必须要声明为虚函数

构成多态的条件
    存在继承关系
    继承关系中有同名虚函数,并且是覆盖关系(函数原型相同)
    通过基类指针调用虚函数

5.3 C++虚析构函数的必要性

构造函数不能是虚函数的原因
    派生类不能继承基类的构造函数,没有意义
    在执行构造函数之前对象尚未创建完成,虚函数表不存在,也没有指向虚函数表的指针,所以没法调用

某些情况下,析构函数必须要声明为虚函数的原因
    基类指针p指向动态生成的派生类对象,
    如果基类析构函数不是虚函数,【delete p】操作时,只会调用基类虚构函数,不调用派生类析构函数
        造成内存泄漏

    如果基类析构函数是虚函数,【delete p】操作时,会调用派生类的析构函数,同时派生类的析构函数会隐式调用基类的析构函数
        不会造成内存泄漏


基类析构函数多数情况下应该声明为虚函数的原因
    派生类的析构函数也会自动成为虚函数,编译器会根据指针的指向来选择函数,所以会调用派生类析构函数,再调用基类析构函数
    否则就有内存泄露的风险

5.4 C++纯虚函数和抽象类

纯虚函数
    纯虚函数特点
        纯虚函数不需要在本类内实现,把类变成抽象类
        只有类的成员函数才能声明为纯虚函数,普通函数不能(编译报错)

    纯虚函数语法
        virtual 返回值类型 函数名 (函数参数) = 0;
        例子:virtual float area() = 0;


抽象类
    什么是抽象类
        包含纯虚函数的类称为抽象类

    抽象类的特点
        抽象类自身无法实例化
            抽象类中的纯虚函数是不完整的,没有函数体,无法分配空间,也无法调用

        抽象类通常是作为基类,让派生类去实现纯虚函数
        派生类必须实现纯虚函数才能被实例化
            纯虚函数给派生类提供的强制约束条件,不实现就不能实例化


    抽象类的作用
        约束派生类的功能
            必须要实现抽象类中的纯虚函数,否则不能实例化

        实现多态
            用基类指针访问派生类成员(变量和函数)

5.5 C++虚函数表

指针访问类的成员函数过程
    普通成员函数
        编译器根据指针类型找到该函数

    虚函数
        派生类有同名函数:编译器根据指针指向找到该函数
        派生类没有同名函数:编译器根据指针类型找到该函数


虚函数表(vtable)和虚函数指针
    虚函数表介绍
        包含虚函数的类,用来创建对象时会额外增加一个数组(虚函数表),数组元素是虚函数的入口地址
        虚函数表和对象是分开存储,对象中新增一个指针,指向虚函数表
        作用:在创建对象时额外加入,使得编译器能通过指针指向的对象找到虚函数,实现多态

    虚函数表和虚函数指针
        虚函数表
            用来创建对象时会额外增加一个数组,数组元素是虚函数的入口地址

        虚函数指针
            创建对象时,在对象中新增的一个指针,指向虚函数表的起始位置


    虚函数的内存模型
        虚函数指针
            位于对象的起始位置

        虚函数表
            虚函数存储顺序
                本类类内虚函数存储顺序
                    按照声明顺序,从低到高存储

                派生类虚函数存储顺序
                    基类虚函数先存储,派生类虚函数后存储,派生类同型虚函数覆盖基类虚函数


            虚函数表存储特点
                同型虚函数只会出现一次
                同型虚函数,在虚函数表的位置下标是固定的,跟继承层次无关
                派生类的同型虚函数会覆盖基类的虚函数



    指针调用虚函数例子
         p 为基类指针,指向派生类,display()为虚函数,在虚函数表第2个位置(索引为1)
        通过p调用虚函数:p -> display();
        编译器的转换:( *( *(p+0) + 1 ) )(p); 
            (p+0) 是虚函数指针在对象中的地址
            *(p+0) 虚函数指针内容:虚函数表的首地址
            ( *(p+0) + 1 ) :虚函数表第2个元素地址
             *( *(p+0) + 1 ) :虚函数表第2个元素内容:虚函数地址
            ( *( *(p+0) + 1 ) )(p) :调用虚函数

5.6 C++ typeid运算符:获取类型信息

typeid介绍
    typeid属于运算符,用来获取一个数据类型或表达式的类型信息
    typeid会把获取到的类型信息保存到一个 type_info 类型的对象里面,并返回该对象的常引用

typeid语法
    typeid( dataType ):dataType 可以是变量、对象、内置类型,结构体和类
    typeid( expression ):表达式
    注意:必须带上括号

typeid用途
    判断两个类型、数据的类型是否相等(一般用 ==判断)
        内置类型的比较
            typeid(int) == typeid(int)
            typeid(a) == typeid(int)
            typeid(a) == typeid(a)

        类的比较
            typeid(obj1) == typeid(*p1) //true
            注意:指针类型只跟定义时的类型有关,指针间赋值不会改变



type_info 类型介绍
    type_info的成员函数
        name() :返回类型的名称
        raw_name() :返回名字编码(Name Mangling)算法产生的新名称
             VC/VS 独有

        hash_code() :返回当前类型对应的 hash 值
            hash 值是用来唯一标志当前类型的整数,跟编译器有关
             VC/VS 和较新的 GCC 下有效


    特点type_info 
        一个类型不管使用了多少次,编译器都只为它创建一个type_info 对象,所有 typeid 都返回这个对象的引用
        编译器只会为使用了 typeid 运算符的类型创建
        带虚函数的类(包括继承来的),编译器一定会创建 type_info 对象

5.7 C++ RTTI机制

什么是RTTI机制
    在程序运行后,才确定类型信息的机制
    Run-Time Type Identification

C++什么情况下使用RTTI机制
    只有在类中包含虚函数时使用
    原因
        当包含虚函数且实现多态时,基类指针所指向对象的类型可能是不确定的,用户可以通过运行时输入的值来决定基类指针指向基类或者派生类对象
        这就导致了在编译时,指针指向对象类型的不确定

    例子
        cin<<i;//运行时输入
        if(i == 0){p = new Base();}//p指向对象类型不一致
        else(i == 1){p = new Derived()}


如何实现RTTI机制
    包含虚函数的类创建对象的内存变化
        在虚函数表vftable开头插入一个指针,指针指向该类对应的 type_info 对象

    运行阶段获取类型信息的操作
        **(p->vfptr - 1)
            p为指向对象的指针
            vfptr 为虚函数指针
            p->vfptr指向虚函数表第一个元素
            p->vfptr - 1 是指向type_info 对象的指针
			*(p->vfptr - 1)是type_info 对象地址
			**(p->vfptr - 1)是type_info 对象


实现RTTI机制的缺点
    对象内存额外增加对象type_info信息的存储,增加内存消耗

5.8 C++静态绑定和动态绑定,彻底理解多态

符号和符号绑定
    符号
        变量名和函数名统称为符号
            变量名:存储变量的内存的首地址
            函数名:存储函数代码的内存的首地址

        符号只是地址的一种助记符,程序被编译和链接成可执行程序后,它们都会被替换成地址

    符号绑定
        通过符号(变量名和函数名),找到对应地址的过程
        函数绑定属于符号绑定的一种


函数绑定
    函数绑定过程
        找到函数名对应的地址
        然后将函数调用处用该地址替换

    静态绑定和动态绑定
        静态绑定
            编译期间(包括链接期间)就能找到函数名对应的地址,完成函数的绑定

        动态绑定
            编译期间(包括链接期间)不能找到函数名对应的地址,必须在程序运行后根据具体的环境或者用户操作才能决定