• 类对象切割对虚函数调用的影响


    背景

    现在有CFish和CAnimal两个类,并且CFish类继承于CAnimal类,它们都有breath这样的接口,只是表现形式不同,所以用虚函数来定义,类关系如下图所示;
    这里写图片描述
    图一 类图关系

    其代码实现如下:

    //基类
    class CAnimal
    {
    public:
        CAnimal()
        {
            //构造函数
            cout << "CAnimal Constructor" << endl;
        }
        ~CAnimal()
        {
            //析构函数
            cout << "CAnimal Destructor" << endl;
        } 
        virtual void breath()
        {
           cout << "CAnimal breath" <<endl;
        }
    };
    //继承类CFish
    class CFish:public CAnimal
    {
    public:
        CFish()
        {
            //构造函数
            cout << "CFish Constructor" << endl;
        }
        ~CFish()
        {
            //析构函数
            cout << "CFish Destructor" << endl;
        } 
    
        virtual void breath()
        {
            cout << "CFish breath" << endl;
        }
    };

    现在我们使用这两个类来分析对象切割,也就是发生Object slicing时对虚函数有何影响,示例代码如下:

    int _tmain(int argc, _TCHAR* argv[])
    {
        CFish FishObj;
        CFish *pFish = new CFish;
    
        cout << "case test begin..." << endl << endl;
        //case1
        cout << "case1" <<endl;
        FishObj.breath();
    
        //case2
        cout << "case2" <<endl;
        pFish->breath();
    
        //case3
        cout << "case3" <<endl;
        ((CAnimal*)(&FishObj))->breath();
    
        //case4, 对象切割,对象发生向上强制转换
        cout << "case4" <<endl;
        ((CAnimal)FishObj).breath();
    
    
        cout << "case test end..." << endl << endl;
    
        if (NULL != pFish)
        {
            delete pFish;
            pFish = NULL;
        }
    
        return 0;
    }

    经vs2008输出如下的测试结果:

    //CFish FishObj 定义对象输出
    CAnimal Constructor
    CFish Constructor
    //new CFish new对象时输出
    CAnimal Constructor
    CFish Constructor
    
    case test begin...
    
    case1
    CFish breath
    case2
    CFish breath
    case3
    CFish breath
    
    case4
    CAnimal breath      //------> 出乎意外,竟不是CFish breath
    CAnimal Destructor  //------> 出乎意外,竟有调用析构函数
    case test end...
    
    //函数返回栈对象析构
    //以及delete对象析构
    CFish Destructor
    CAnimal Destructor
    CFish Destructor
    CAnimal Destructor

    毫无疑问,case1-case3都是调用CFish类中的breath函数,因为pFish 、FishObj在构造对象结束后,他们的虚函数表内容已经确定,都是存在CFish::breath接口,但case4输出结果却比较特殊,因为语句((CAnimal)FishObj)发生了向上强制转换,导致对象切割,在切割过程有对象重新构造,导致虚函数表中的内容发生变化,具体分析如下;

    对象切割分析

    我们知道,派生类通常会比基类大,因为派生类不仅有基类的成员还有派生类本身的成员,经过向上转换(派生类对象强制转换为基类对象),就会造成对象内容被切割(object slicing).

    当代码执行到((CAnimal)FishObj).breath()语句时,发生了object slicing,其过程如下:

    这里写图片描述
    图二 对象切割流程

    备注:本例中m_data1和m_data2是虚拟的,只有vptr是真实的。

    从图中可以看出,当发生强制转换时有两个步骤:

    1. 发生CAnimal对象构造,同时将vptr中的值被赋值为CAnimal::breath的地址
    2. ((CAnimal)FishObj).breath()调用转换为临时对象的breath调用。

    我们也可以从汇编代码中看出实际执行情况,汇编代码如下:

    这里写图片描述
    图三 代码汇编分析

    在汇编代码中也验证了图二流程分析的正确性,需要注意的是在强制转换过程中,编译器会主动为我们合成一个构造函数,不是我们定义的那个构造函数,但调用的析构函数都是同一个

    通过以上分析,用例4的测试结果也就不感到意外了。

    总结

    如果类对象发生了切割或者向上强制转换,会产生临时对象,使得这个临时对象变成真正的CAnimal类对象,而不是CFish对象。

    在分析问题过程中,也了解到一个类的构造函数有多个,但是其析构函数只有一个,因为析构函数没有返回值,没入参,也就无法实现析构函数的重载。

  • 相关阅读:
    java 用代码实现判断字符串的开头和结尾
    java基础 1-path
    C#基础(语句 for循环)
    C#基础(数组)
    C#基础(语句 if else)
    C#基础(变量、常量、运算符)
    继承-person
    继承-字母表
    继承-monkey
    继承-people
  • 原文地址:https://www.cnblogs.com/jinxiang1224/p/8468266.html
Copyright © 2020-2023  润新知