• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

C++:多态详细

武飞扬头像
includeevey
帮助1

前言

        想必大家都知道面向对象的三大特征:封装,继承,多态。封装的本质是:对外暴露必要的接口,但内部的具体实现细节和部分的核心接口对外是不可见的,仅对外开放必要功能性接口。继承的本质是为了复用,复用基类的数据成员和方法。对于多态而言,多态的实现要求必须是公有继承作为前提,这也是我们的学习顺序。那么这篇文章就带领大家一起学习多态!

目录

前言

Ⅰ.多态的概念

Ⅱ.多态的定义及实现

Ⅲ.抽象类

Ⅳ.多态的原理

Ⅴ.单继承和多继承关系的虚函数表

Ⅵ.继承和多态常见的面试问题


Ⅰ.多态的概念

多态的概念通俗来说:就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

下面我们通过一个例子进行理解,同为动物的小猫咪和小狗发出的不同声音。

☆有一个基类(Animal),它有两个派生类(Cat,Dog),在Animal中有个方法(Say),Cat和Dog都是继承于Animal。当Cat调用Say时会发出“喵~喵~喵~喵~”的声音,当Dog调用Say时会发出“汪~汪~汪~汪~”的声音,这就是多态的实现。

学新通

☆再简单的举一个例子:张三和李四都是学生,他们都想报考法律专业的学校,张三成绩比较优异就报了一个知名法律大学,李四的成绩就不是非常的拔尖,就随便报了一个学校。张三和李四去完成报考的行为,但是他们完成时就就产生不同的状态。

Ⅱ.多态的定义及实现

多态的构成条件 

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Cat和Dog继承了AnimalAnimal对象Say-动物语言,Cat对象Say-喵,Dog对象Say-汪。

那么在继承中构成多态的两个条件:

        ☆必须通过基类的指针或者引用调用虚函数

        ☆被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

学新通

虚函数 

虚函数:即被virtual修饰的类成员函数称为虚函数。

  1.  
    class Animal {
  2.  
    public:
  3.  
    virtual void Say(){
  4.  
    cout << " 动物语言 " << endl;
  5.  
    }
  6.  
    };

虚函数的重写

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

学新通

  1.  
    class Cat : public Animal {
  2.  
    public:
  3.  
    virtual void Say() {
  4.  
    cout << "喵~喵~喵~喵~" << endl;
  5.  
    }
  6.  
    };
  7.  
     
  8.  
    class Dog : public Animal {
  9.  
    public:
  10.  
    virtual void Say() {
  11.  
    cout << "汪~汪~汪~汪~" << endl;
  12.  
    }
  13.  
    };

学新通

注意:在重写基类虚函数时,派生类的虚函数在不加 virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。

  1.  
    /*virtual*/ void Say() {
  2.  
    cout << "汪~汪~汪~汪~" << endl;
  3.  
    }

当任何一个条件破坏,就会变成隐藏。

学新通

学新通

指针调用虚函数

学新通

注意:普通调用跟调用对象有关,多态调用是跟(指针/引用)指向的对象有关。

协变(基类与派生类虚函数返回值类型不同)  

子类重写基父类函数时,子类中有一个跟父类完全相同的虚函数,与父类虚函数返回值类型不同,但是要求返回值必须是一个父子类关系的指针或引用,称为协变。

  1.  
    class A{
  2.  
    };
  3.  
     
  4.  
    class B : public A {
  5.  
    };
  6.  
     
  7.  
    class Person {
  8.  
    public:
  9.  
    virtual A* f() { return new A; }
  10.  
    };
  11.  
     
  12.  
    class Student : public Person {
  13.  
    public:
  14.  
    virtual B* f() { return new B; }
  15.  
    };
学新通

析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

  1.  
    class Person {
  2.  
    public:
  3.  
    virtual ~Person() {
  4.  
    cout << "~Person()" << endl; }
  5.  
    };
  6.  
     
  7.  
    class Student : public Person {
  8.  
    public:
  9.  
    /virtual ~Student() {
  10.  
    cout << "~Student()" << endl; }
  11.  
    };
  12.  
     
  13.  
    void Test()
  14.  
    {
  15.  
    Person* p1 = new Person;
  16.  
    Person* p2 = new Student;
  17.  
     
  18.  
    delete p1;
  19.  
    delete p2;
  20.  
    }
学新通

只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。加 virtual关键字。

学新通

如果未加virtual关键字。通过观察发现,发生了内存泄漏。原本还需要释放Student类,但是这里没有。因为delete是使用指针调用析构-operator delete(ptr),这里未加virtual关键字就是普通调用,普通调用和对象类型有关,普通调用会发生隐藏关系,是什么类型就调用什么析构函数,所以就会调用两次Person。学新通

学新通

C 11 override 和 final

从上面可以看出,C 对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数 名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有 得到预期结果才来debug会得不偿失,因此:C 11提供了override和final两个关键字,可以帮助用户检测是否重写。

☆final:修饰虚函数,表示该虚函数不能再被重写

  1.  
    class Car
  2.  
    {
  3.  
    public:
  4.  
    virtual void Drive() final {}
  5.  
    };
  6.  
     
  7.  
    class Benz :public Car
  8.  
    {
  9.  
    public:
  10.  
    virtual void Drive() { cout << "Benz-舒适" << endl; }
  11.  
    };

学新通

实现一个不被继承的类

♢构造私有,c 98抽象类

  1.  
    class A
  2.  
    {
  3.  
    private:
  4.  
    A()
  5.  
    {}
  6.  
    };
  7.  
     
  8.  
    class Benz :public A
  9.  
    {};

 ♢定义时加 final,c 11

  1.  
    class A final
  2.  
    {
  3.  
    private:
  4.  
    A()
  5.  
    {}
  6.  
    };
  7.  
     
  8.  
    class Benz :public A
  9.  
    {};

☆override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

  1.  
    class Car{
  2.  
    public:
  3.  
    virtual void Drive(){}
  4.  
    };
  5.  
     
  6.  
    class Benz :public Car {
  7.  
    public:
  8.  
    virtual void Drive() override {cout << "Benz-舒适" << endl;}
  9.  
    };

学新通

重载、覆盖(重写)、隐藏(重定义)的对比

学新通

Ⅲ.抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生 类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

  1.  
    class Car
  2.  
    {
  3.  
    public:
  4.  
    //纯虚函数
  5.  
    virtual void Drive() = 0;
  6.  
    };
  7.  
     
  8.  
    class Benz :public Car
  9.  
    {
  10.  
    public:
  11.  
    //对纯虚函数进行重写
  12.  
    virtual void Drive()
  13.  
    {
  14.  
    cout << "Benz-舒适" << endl;
  15.  
    }
  16.  
    };
  17.  
     
  18.  
    class BMW :public Car
  19.  
    {
  20.  
    public:
  21.  
    virtual void Drive()
  22.  
    {
  23.  
    cout << "BMW-操控" << endl;
  24.  
    }
  25.  
    };
  26.  
     
  27.  
    void Test()
  28.  
    {
  29.  
    //Car p;//出错
  30.  
     
  31.  
    Car* pBenz = new Benz;
  32.  
    pBenz->Drive();
  33.  
    Car* pBMW = new BMW;
  34.  
    pBMW->Drive();
  35.  
    }
学新通

接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。 

为了更好的理解接口继承,下面我们通过试题进行深究。以下程序输出结果是什么()?

  1.  
    class A
  2.  
    {
  3.  
    public:
  4.  
    virtual void func(int val = 1){ std::cout << "A->" << val << std::endl; }
  5.  
    virtual void test(){ func(); }
  6.  
    };
  7.  
     
  8.  
    class B : public A
  9.  
    {
  10.  
    public:
  11.  
    void func(int val = 0){ std::cout << "B->" << val << std::endl; }
  12.  
    };
  13.  
     
  14.  
    int main(int argc, char* argv[])
  15.  
    {
  16.  
    B*p = new B;
  17.  
    p->test();
  18.  
    return 0;
  19.  
    }
学新通

A: A->0   B: B->1   C: A->1   D: B->0   E: 编译出错   F: 以上都不正确

参考答案:B: B->1,过程如图下

学新通

Ⅳ.多态的原理

虚函数表

这里常考一道笔试题:sizeof(Base)是多少?  

  1.  
    class Base
  2.  
    {
  3.  
    public:
  4.  
    virtual void Func1()
  5.  
    {
  6.  
    cout << "Func1()" << endl;
  7.  
    }
  8.  
    private:
  9.  
    char _c = 'a';
  10.  
    int _b = 1;
  11.  
    };
  12.  
     
  13.  
    int main()
  14.  
    {
  15.  
    cout << sizeof(Base) << endl;
  16.  
    return 0;
  17.  
    }
学新通

参考答案:12,过程如下图

学新通

通过观察测试我们发现p对象是12bytes,除了_b,_c成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代 表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,虚函数表也简称虚表。那么派生类中这个表放了些什么呢?我们接着往下分析。

针对上面的代码我们做出以下改造,我们增加一个派生类Derive去继承Base ,Derive中重写Func1 ,Base再增加一个虚函数Func2和一个普通函数Func3。

  1.  
    class Base
  2.  
    {
  3.  
    public:
  4.  
    virtual void Func1()
  5.  
    {
  6.  
    cout << "Base::Func1()" << endl;
  7.  
    }
  8.  
    virtual void Func2()
  9.  
    {
  10.  
    cout << "Base::Func2()" << endl;
  11.  
    }
  12.  
    void Func3()
  13.  
    {
  14.  
    cout << "Base::Func3()" << endl;
  15.  
    }
  16.  
     
  17.  
    private:
  18.  
    int _b = 1;
  19.  
    };
  20.  
     
  21.  
    class Derive : public Base
  22.  
    {
  23.  
    public:
  24.  
    virtual void Func1()
  25.  
    {
  26.  
    cout << "Derive::Func1()" << endl;
  27.  
    }
  28.  
     
  29.  
    private:
  30.  
    int _d = 2;
  31.  
    };
  32.  
     
  33.  
    int main()
  34.  
    {
  35.  
    Base b;
  36.  
    Derive d;
  37.  
     
  38.  
    return 0;
  39.  
    }
学新通

学新通

通过观察和测试,我们发现了以下几点问题:

☆ 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。

☆基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

☆另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,因为不是虚函数,所以不会放进虚表。

虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

☆总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

☆这里还有一个很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。但是很多时候都是这样深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的。

多态的原理

相信大家经过上面的理解,对于多态原理的面纱几乎已经解开了,对于多态的理解最重要是理解虚指针(_vfptr),虚函数(父子类被virtual关键字声明的函数),虚表(虚函数的类都有一个一维的虚函数表)。父子类通过继承关系的虚函数,通过虚指针指向虚表,当发生切片后,当子类虚函数重写了父类的虚函数,这时就会发生覆盖。

下面主要是对覆盖在进行深度理解,最后再通过汇编代码的角度来理解虚表,虚函数,虚函数指针。

  1.  
    class Person {
  2.  
    public:
  3.  
    virtual void BuyTicket() { cout << "买票-全价" << endl; }
  4.  
    };
  5.  
     
  6.  
    class Student : public Person {
  7.  
    public:
  8.  
    virtual void BuyTicket() { cout << "买票-半价" << endl; }
  9.  
    };
  10.  
     
  11.  
    void Func(Person& p)
  12.  
    {
  13.  
    p.BuyTicket();
  14.  
    }
  15.  
     
  16.  
    int main()
  17.  
    {
  18.  
    Person Jack;
  19.  
    Func(Jack);
  20.  
     
  21.  
    Student Tom;
  22.  
    Func(Tom);
  23.  
     
  24.  
    return 0;
  25.  
    }
学新通

学新通

通过汇编的角度理解虚表,我们发现在父类中调用自己函数,因为是普通调用,不管是通过虚函数指针调用虚表,还是直接调用虚表,都是在编译时已经从符号表确认了函数的地址,直接call。

  1.  
    void Func(Person* p)
  2.  
    {
  3.  
    p->BuyTicket();
  4.  
    }
  5.  
     
  6.  
    int main()
  7.  
    {
  8.  
    Person Jack;
  9.  
    Func(&Jack);
  10.  
    Jack.BuyTicket();
  11.  
     
  12.  
    return 0;
  13.  
    }

在汇编代码中,声明成员Jack,Jack调用Func函数,相当于把Jack对象头4个字节(虚表指针)移动到了edx,Jack直接调用类中的BuyTicket函数,相当于把虚表中的头4字节存的虚函数指针移动到了eax 。      

void Func(Person* p)

{

...

    p->BuyTicket();

// p中存的是Jack对象的指针,将p移动到eax中

001940DE  mov         eax,dword ptr [p]

// [eax]就是取eax值指向的内容,这里相当于把Jack对象头4个字节(虚表指针)移动到了edx

001940E1  mov         edx,dword ptr [eax]

// [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax

00B823EE  mov         eax,dword ptr [edx]

// call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后到对象的中取找的。

001940EA  call        eax  

001940EC  cmp         esi,esp  

}

int main()

{

...

// 首先BuyTicket虽然是虚函数,但是Jack是对象,不满足多态的条件,所以这里是地址普通函数的调用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call

        mike.BuyTicket();

00195182  lea         ecx,[mike]

00195185  call        Person::BuyTicket (01914F6h)  

...

}

当满足多态调用在编译时确定的,是运行起来以后到对象的中取找的。

  1.  
    void Func(Person* p)
  2.  
    {
  3.  
    p->BuyTicket();
  4.  
    }
  5.  
     
  6.  
    int main()
  7.  
    {
  8.  
    Student Tom;
  9.  
    Func(&Tom);
  10.  
    Tom.BuyTicket();
  11.  
     
  12.  
    return 0;
  13.  
    }

void Func(Person* p)
00DA458E  mov         eax,dword ptr [p]  
void Func(Person* p)
00DA4591  mov         edx,dword ptr [eax]  
00DA4593  mov         esi,esp  
00DA4595  mov         ecx,dword ptr [p]  
00DA4598  mov         eax,dword ptr [edx]  
00DA459A  call        eax  
00DA459C  cmp         esi,esp  

动态绑定与静态绑定

静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载

动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态。

不管虚函数的虚指针是指向父类虚表,还是子类的虚表其实都是一样的,该有虚表该有成员都是一样的,不一样的是子函数的虚表中的虚函数(可能发生了重写/覆盖)。

  1.  
    int main()
  2.  
    {
  3.  
    //普通调用--编译时/静态 绑定
  4.  
    Student Tom;
  5.  
    Func(&Tom);
  6.  
     
  7.  
    //多态调用--运行时/动态 绑定
  8.  
    Person Jack;
  9.  
    Func(&Jack);
  10.  
     
  11.  
    return 0;
  12.  
    }

学新通

总结:多态的原理实质就是,虚表是提前写好的,对象指向谁就调用谁的虚表,多态调用就是依靠虚表一系列的动作。指向父类调用父类的虚函数,指向子类调用子类的虚函数(可能覆盖),运行起来后去虚表中找。

虚表的存放区域?

  1.  
    class Person {
  2.  
    public:
  3.  
    virtual void BuyTicket() { cout << "买票-全价" << endl; }
  4.  
    };
  5.  
     
  6.  
    class Student : public Person {
  7.  
    public:
  8.  
    virtual void BuyTicket() { cout << "买票-半价" << endl; }
  9.  
    };
  10.  
    void Test()
  11.  
    {
  12.  
    int a = 0;
  13.  
    cout << "栈" << &a << endl;
  14.  
     
  15.  
    int* p1 = new int;
  16.  
    cout << "堆" << p1 << endl;
  17.  
     
  18.  
    const char* str = "hello world";
  19.  
    cout << "代码段/常量区" << (void*)str << endl;
  20.  
     
  21.  
    static int b = 0;
  22.  
    cout << "静态区/数据段" << &b << endl;
  23.  
     
  24.  
    Student s;
  25.  
    cout << "虚表:" << (void*)*((int*)&s1) << endl;
  26.  
    }
学新通

通过观察,我们发现代码段与虚表的地址是挨的最近,所以我们得出结论虚表是存放在代码段 

学新通 同一个类下,虚表是用一个。

  1.  
    void Test()
  2.  
    {
  3.  
    Student s1;
  4.  
    cout << "虚表1:" << (void*)*((int*)&s1) << endl;
  5.  
     
  6.  
    Student s2;
  7.  
    cout << "虚表2:" << (void*)*((int*)&s2) << endl;
  8.  
    }

学新通

理解: (void*)*((int*)&s1)

学新通

Ⅴ.单继承和多继承关系的虚函数表

需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类 的虚表模型前面我们已经看过了,没什么需要特别研究的

单继承中的虚函数表

  1.  
    class Base {
  2.  
     
  3.  
    public:
  4.  
    virtual void func1() { cout << "Base::func1" << endl; }
  5.  
    virtual void func2() { cout << "Base::func2" << endl; }
  6.  
     
  7.  
    private:
  8.  
    int a;
  9.  
    };
  10.  
     
  11.  
    class Derive :public Base {
  12.  
     
  13.  
    public:
  14.  
    virtual void func1() { cout << "Derive::func1" << endl; }
  15.  
    virtual void func3() { cout << "Derive::func3" << endl; }
  16.  
    virtual void func4() { cout << "Derive::func4" << endl; }
  17.  
     
  18.  
    private:
  19.  
    int b;
  20.  
    };
学新通

学新通

观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这 两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?下面我们使用代码打印 出虚表中的函数。

思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数
指针的指针数组,这个数组最后面放了一个nullptr

        ●先取b的地址,强转成一个int*的指针

        ●再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针

        ●再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。

        ●虚表指针传递给PrintVTable进行打印虚表

        ●需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。

学新通

 注意:

        平时使用 typedef定义一个别名是直接加在类型后面,例如:typedef  long double REAL;这里 typedef void(*) ()VFPTR是错误的,函数指针的语法规定是将VFPTR别名放入函数指针类型()中

         ☆在传参时,PrintVTable中参数是函数指针的数组,就不能只传对象4byte的地址,那么就应该传函数指针(VFPTR*)(*(int*)&b)。

  1.  
    //函数指针:通过地址调用函数
  2.  
    typedef void(*VFPTR) ();
  3.  
     
  4.  
    //函数指针的数组:虚表地址是连续的,通首地址连续查看虚函数地址
  5.  
    void PrintVTable(VFPTR vTable[])
  6.  
    {
  7.  
     
  8.  
    cout << " 虚表地址>" << vTable << endl;
  9.  
    for (int i = 0; vTable[i] != nullptr; i)
  10.  
    {
  11.  
    printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
  12.  
    VFPTR f = vTable[i];
  13.  
    f();
  14.  
    }
  15.  
    cout << endl;
  16.  
    }
  17.  
     
  18.  
    int main()
  19.  
    {
  20.  
    Base b;
  21.  
    Derive d;
  22.  
     
  23.  
    VFPTR* vTableb = (VFPTR*)(*(int*)&b);
  24.  
    PrintVTable(vTableb);
  25.  
    VFPTR* vTabled = (VFPTR*)(*(int*)&d);
  26.  
    PrintVTable(vTabled);
  27.  
     
  28.  
    return 0;
  29.  
    }
学新通

通过打印发现func3和func4是在对象d的虚表中,d对象的func1函数也在虚表中覆盖了父类的funcl函数。下图也通过不同颜色区分出不同对象对应的虚拟地址空间。

学新通

为了能够同时测试32位和64位下的虚表地址,32位下是4byte,64为下是8byte,又该如何呢?

这里就通过传参时,传入二级指针(void**),当32位下传int*时解引用是4byte,但是在64下传int*时解引用是8byte,显然这时候是行不通的,但是传二级指针不管是32位还是64位下解引用都是地址,地址不管怎么样都是4byte,这里也可以写成(int**),(double**)等等都是可以的,但通常情况下是写成(void**)。

  1.  
    //32位和64位同时可以打印虚表地址
  2.  
    VFPTR* vTableb = (VFPTR*)(*(void**)&b);
  3.  
    PrintVTable(vTableb);

多继承中的虚函数表

在多继承下,子类会继承两个父类的虚函数表。

学新通

子类继承了两个父类的虚函数表,而且都会与两个父类的虚函数进行重写。

学新通 注意:为了打印出第二个父类虚函数表的地址,需要取第一个父类地址的偏移量例如:(char*)&d sizeof(Base1)或者 Base2* ptr2 = &d;

  1.  
    class Base1 {
  2.  
    public:
  3.  
    virtual void func1() { cout << "Base1::func1" << endl; }
  4.  
    virtual void func2() { cout << "Base1::func2" << endl; }
  5.  
     
  6.  
    private:
  7.  
    int b1;
  8.  
    };
  9.  
     
  10.  
    class Base2 {
  11.  
    public:
  12.  
    virtual void func1() { cout << "Base2::func1" << endl; }
  13.  
    virtual void func2() { cout << "Base2::func2" << endl; }
  14.  
     
  15.  
    private:int b2;
  16.  
    };
  17.  
     
  18.  
    class Derive : public Base1, public Base2 {
  19.  
     
  20.  
    public:
  21.  
    virtual void func1() { cout << "Derive::func1" << endl; }
  22.  
    virtual void func3() { cout << "Derive::func3" << endl; }
  23.  
     
  24.  
    private:
  25.  
    int d1;
  26.  
    };
  27.  
     
  28.  
    typedef void(*VFPTR) ();
  29.  
     
  30.  
    void PrintVTable(VFPTR vTable[])
  31.  
    {
  32.  
    cout << " 虚表地址>" << vTable << endl;
  33.  
    for (int i = 0; vTable[i] != nullptr; i)
  34.  
    {
  35.  
    printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
  36.  
    VFPTR f = vTable[i];
  37.  
    f();
  38.  
    }
  39.  
    cout << endl;
  40.  
    }
  41.  
     
  42.  
    int main()
  43.  
    {
  44.  
    Base1 b1;
  45.  
    Base2 b2;
  46.  
    PrintVTable((VFPTR*)(*(int*)&b1));
  47.  
    PrintVTable((VFPTR*)(*(int*)&b2));
  48.  
     
  49.  
    Derive d;
  50.  
     
  51.  
    VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
  52.  
    PrintVTable(vTableb1);
  53.  
     
  54.  
    //VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d sizeof(Base1)));
  55.  
    //PrintVTable(vTableb2);
  56.  
    Base2* ptr2 = &d;
  57.  
    PrintVTable((VFPTR*)(*(int*)&ptr2));
  58.  
     
  59.  
    return 0;
  60.  
    }
学新通

最后通打印我们发现,子类会与两个父类发生重写,但是子类未重写的虚函数放在第一个继承父类部分的虚函数表中。

学新通

菱形继承、菱形虚拟继承

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的 模型,访问基类成员有一定得性能损耗,在实际中也很少用。对于学习知识来说是可以见见的,过于深究会头昏脑胀。

        ☆菱形继承

菱形继承就是重复继承,在继承的时候不加virtual关键字。

学新通

  1.  
    class Base {
  2.  
    public:
  3.  
    virtual void func() { cout << "Base1::func" << endl; }
  4.  
    private:
  5.  
    int b;
  6.  
    };
  7.  
     
  8.  
    class Base1 : public Base {
  9.  
    public:
  10.  
    virtual void func1() { cout << "Base1::func1" << endl; }
  11.  
     
  12.  
    private:
  13.  
    int b1;
  14.  
    };
  15.  
     
  16.  
    class Base2 : public Base{
  17.  
    public:
  18.  
    virtual void func2() { cout << "Base2::func2" << endl; }
  19.  
     
  20.  
    private:int b2;
  21.  
    };
  22.  
     
  23.  
    class Derive : public Base1, public Base2 {
  24.  
    public:
  25.  
    virtual void func1() { cout << "Derive::func1" << endl; }
  26.  
    virtual void func2() { cout << "Derive::func3" << endl; }
  27.  
    virtual void func3() { cout << "Derive::func3" << endl; }
  28.  
     
  29.  
    private:
  30.  
    int d1;
  31.  
    };
  32.  
     
学新通

学新通

         ☆菱形虚拟继承

为了解决菱形继承的二义性和数据冗余问题,用过加virtual关键字形成菱形继承。由于前面的“菱形继承”中的类的内部数据和接口都是完全一样的,为了解决冗余,只是我们采用了虚拟继承:其省略后的源码如下所示:

class Base {……};
class Base1 : virtual public B{……};
class Base2: virtual public B{……};
class  Derive : public B1, public B2{ …… };

菱形虚拟继承其实结构与菱形继承是一样的,不一样是的加了虚拟后,会单独形成一个虚基表进行记录变量的偏移量,这里只需要明白的是虚函数表和虚基表是不同的,要讨论的结构如下:

学新通

学新通

Ⅵ.继承和多态常见的面试问题

概念查考

1. 下面哪种面向对象的方法可以让你变得富有( )

A: 继承 B: 封装 C: 多态 D: 抽象

2. ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关, 而对方法的调用则可以关联于具体的对象。

A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定

3. 面向对象设计中的继承和组合,下面说法错误的是?()

A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用

B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用

C:优先使用继承,而不是组合,是面向对象设计的第二原则

D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封 装性的表现

4. 以下关于纯虚函数的说法,正确的是( )

A:声明纯虚函数的类不能实例化对象  B:声明纯虚函数的类是虚基类

C:子类必须实现基类的纯虚函数 D:纯虚函数必须是空函数

5. 关于虚函数的描述正确的是( )

A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型  B:内联函数不能是虚函数

C:派生类必须重新定义基类的虚函数  D:虚函数可以是一个static型的函数

6. 关于虚表说法正确的是( )

A:一个类只能有一张虚表

B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表

C:虚表是在运行期间动态生成的

D:一个类的不同对象共享该类的虚表

7. 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )

A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址

B:A类对象和B类对象前4个字节存储的都是虚基表的地址

C:A类对象和B类对象前4个字节存储的虚表地址相同

D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表

8. 下面程序输出结果是什么? () 

  1.  
    #include<iostream>
  2.  
    using namespace std;
  3.  
     
  4.  
    class A{
  5.  
    public:
  6.  
    A(char *s) { cout<<s<<endl; }
  7.  
    ~A(){}
  8.  
    };
  9.  
     
  10.  
    class B:virtual public A
  11.  
    {
  12.  
    public:
  13.  
    B(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
  14.  
    };
  15.  
     
  16.  
    class C:virtual public A
  17.  
    {
  18.  
    public:
  19.  
    C(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
  20.  
    };
  21.  
     
  22.  
    class D:public B,public C
  23.  
    {
  24.  
    public:
  25.  
    D(char *s1,char *s2,char *s3,char *s4):B(s1,s2),C(s1,s3),A(s1)
  26.  
    { cout<<s4<<endl;}
  27.  
    };
  28.  
     
  29.  
    int main() {
  30.  
    D *p=new D("class A","class B","class C","class D");
  31.  
    delete p;
  32.  
    return 0;
  33.  
    }
学新通

A:class A class B class C class D                 B:class D class B class C class A

C:class D class C class B class A                 D:class A class C class B class D

9. 多继承中指针偏移问题?下面说法正确的是( )

  1.  
    class Base1 {  public:  int _b1; };
  2.  
     
  3.  
    class Base2 {  public:  int _b2; };
  4.  
     
  5.  
    class Derive : public Base1, public Base2 { public: int _d; };
  6.  
     
  7.  
    int main(){
  8.  
    Derive d;
  9.  
    Base1* p1 = &d;
  10.  
    Base2* p2 = &d;
  11.  
    Derive* p3 = &d;
  12.  
    return 0;
  13.  
    }

 A:p1 == p2 == p3         B:p1 < p2 < p3         C:p1 == p3 != p2         D:p1 != p2 != p3

10. 多继承中指针偏移问题?下面说法正确的是( )  

  1.  
    class Base1 {  public:  int _b1; };
  2.  
     
  3.  
    class Base2 {  public:  int _b2; };
  4.  
     
  5.  
    class Derive : public Base2, public Base1 { public: int _d; };
  6.  
     
  7.  
    int main(){
  8.  
    Derive d;
  9.  
    Base1* p1 = &d;
  10.  
    Base2* p2 = &d;
  11.  
    Derive* p3 = &d;
  12.  
    return 0;
  13.  
    }

 A:p1 == p2 == p3         B:p1 > (p2 = p3)         C:p1 == p3 != p2         D:p1 != p2 != p3

学新通

问答题 

1. 什么是多态?

答:多态分为两种,一种是静态的多态是静态绑定,在程序编译期间确定了程序的行为,函数重载;一种动态的多态是动态绑定,通过继承下虚函数重写,父类的指针指向在父类的虚函数进行动态绑定,运行起来去虚表中找到对应的虚函数,指向子类调用子类。

2. 什么是重载、重写(覆盖)、重定义(隐藏)?

答:函数重载发生在同一作用域并且只需要函数名相同;重写是子类的虚函数对父类的虚函数进行覆盖,在不同的作用域且在继承关系下函数需要三同(参数,函数名,返回值)和父类函数被关键字virtual修饰 ;重定义是子类继承父类下,在不同作用域,满足函数名相同就构成隐藏。

3. 多态的实现原理?

答:通过继承下虚函数重写,父类的指针指向在父类的虚函数进行动态绑定,运行起来去虚表中找到对应的虚函数,指向子类调用子类。

4. inline函数可以是虚函数吗? 

答:不能的,但是在语法上是可以的,只是编译器会忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。当不构成多态是可以具有内联属性,构成多态不具有内联属性。

 5. 静态成员可以是虚函数吗?

答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式 无法访问虚函数表,所以静态成员函数无法放进虚函数表。

6. 构造函数可以是虚函数吗?

答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

7.析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

答:可以,并且最好把基类的析构函数定义成虚函数。参考本节课件内容  

 8. 对象访问普通函数快还是虚函数更快?

答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函 数表中去查找。

9. 虚函数表是在什么阶段生成的,存在哪的?

答:虚函数表是在编译阶段就生成的构造函数初始化列表中初始化【虚表指针】,一般情况 下存在代码段(常量区)的。  

10. C 菱形继承的问题?虚继承的原理?

答:菱形继承会产生二义性和数据冗余。虚基表指针在本类中找到虚基表,本类的虚基表通过偏移量计算找到该虚基类中变量的值。注意这里不要把虚函数表和虚基表搞混了。  

11. 什么是抽象类?抽象类的作用?

答:在C 中,含有纯虚拟函数的类称为抽象类,它不能生成对象;目的是为了重写,达成 多态,继承的是接口;抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。  

 ☺   [ 作者 ]   includeevey

 📃  [ 日期 ]   2023/2/1
 📜  声明 ]   到这里就该说再见了,若本文有错误和不准确之处,恳望读者批评指正!
                    有则改之无则加勉!若认为文章写的不错,一键三连加关注!


 学新通

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgcafgb
系列文章
更多 icon
同类精品
更多 icon
继续加载