C++多态虚表问题2019-09-07

\color{blue}{C++多态分析}
多态是指:程序运行时,调用方法的行为应取决于调用该方法的对象,一般情况下,我们一般要求父类的指针可以根据具体所指的对象类型,来执行不同的函数.
知识点:

  • C++支持两种多态性:编译时的多态性,运行时的多态性
    a.编译时的多态性:通过重载函数来实现
    b.运行时的多态性:通过虚函数来实现
  • 将派生类指针或者引用转换为基类引用或者指针被称为上行强制转换,这使得公有继承不需要进行显示类型转换.因此对于基类引用或指针作为参数的函数调用,将进行上行转换.
  • C++ new一个对象时,只为类中的成员变量分配空间,对象之间共享成员函数.
    \color{blue}{综合如下代码}
#include <iostream>
using namespace std ;
class A{
public:
    int a;
    A(){}
    ~A(){}
    //virtual
    virtual void print(){
        cout << "A method" << endl ;
 // void print(){
   //     cout << "A method" << endl ;
    }
};
class B:public A{
public:
    int a;
    B(){}
    ~B(){}
    void print(){
        cout << "B method" << endl ;
    }
};
//指针传递
void print1( A *s ){
    cout<<"address s:"<<s<<endl;
    cout<<"address a:"<<&(s->a)<<endl;
    s->print();
}
//按值传递会发生拷贝
void print2(A s){
    cout<<"address s:"<< &s <<endl;
    cout<<"address a:"<<&(s.a)<<endl;
    s.print();
}
//引用传递
void print3(A &s){
    cout<<"address s:"<< &s <<endl;
    cout<<"address a:"<<&(s.a)<<endl;
    s.print();
}
int main()
{
    A* p = new A() ;
    B* s = new B() ;
//    A a;
//    B b;
    print1(p);
    print1(s);
    print2(*p);
    print2(*s);
    print3(*p);
    print3(*s);

    delete(p);
    delete(s);
    return 0;
}

输出结果:

address s:0x1001c20
address a:0x1001c28
A method
address s:0x1001c40
address a:0x1001c48
B method
address s:0x7ffe1c669880
address a:0x7ffe1c669888
A method
address s:0x7ffe1c669880
address a:0x7ffe1c669888
A method
address s:0x1001c20
address a:0x1001c28
A method
address s:0x1001c40
address a:0x1001c48
B method

从运行结果来看:

  • 在父类中实现了虚函数,则当按指针和按引用传递时,会发生隐式上行转换,且能根据对象的类型来调用相应的print()函数;
  • 由于按值传递时,进行的是会进行拷贝操作,所以输出的地址与其他的不同,且只能数据的类型调用相应的函数print();
  • 在输出的地址中,打印的父类对象的首地址和成员a的地址不同,第二次打印的是子类的对象首地址和其成员a的地址,两个值也不同,都差了8字节.这八字节被用来存储它们各自的虚函数指针了.该指针指向该类的虚函数表.(64位机,指针占八个字节)
    \color{blue}{不使用虚函数时}
    输出结果为:
address s:0x651c20
address a:0x651c20
A method
address s:0x651c40
address a:0x651c40
A method
address s:0x7ffc6bb49250
address a:0x7ffc6bb49250
A method
address s:0x7ffc6bb49250
address a:0x7ffc6bb49250
A method
address s:0x651c20
address a:0x651c20
A method
address s:0x651c40
address a:0x651c40
A method

很明显,都是根据指针的类型来进行调用的父类中的print()函数,而且地址方面类的首地址与变量的地址相同,没有产生vptr虚函数指针.
下面着重分析一下虚函数表:
\color{blue}{虚函数表分析}
虚成员函数的动态联编:

class Brass{...virtual void Viewaccount()...}; 
class Brassplus : public Brass{...virtual void Viewaccount()..};
Brassplus ophelia;   //派生类对象
Brass* bp;             //基类指针
bp = &ophelia;          //代表了基类指针指向派生类对象
bp->Viewaccout();      //调用派生类内重定义的虚函数  

注:如果在基类中没有对Viewacout()声明为虚函数,则bp->Viewaccout将根据指针类型(Brass*)调用Brass::Viewaccout.编译器对非虚函数采用的方法是静态联编;然而若在基类中声明为了虚函数,则bp->Viewaccout将根据对象的类型来调用Brassplus::Viewaccout,在该例子中对象ophelia的类型为Brassplus;总之编译器对虚函数采用动态联编.这种机制是由内部虚函数表来实现的.
\color{blue}{先看代码}

#include <iostream>
using namespace std;
class Base {
public:
    virtual void f() {cout<<"base::f"<<endl;}
    virtual void g() {cout<<"base::g"<<endl;}
    virtual void h() {cout<<"base::h"<<endl;}
};

class Derive : public Base{
public:
    void g() {cout<<"derive::g"<<endl;}
};

int main () {
    cout<<"size of Base: "<<sizeof(Base)<<endl;

    typedef void(*Func)(void);//函数指针类型的定义,作用是声明一个(void*)()类型的函数指针,该函数指针指向一个入参和函数类型均为void的函数.
    Base b;
    Base *d = new Derive();   //基类指针指向派生类对象
    long* pvptr = (long*)d;     //pvptr指向d对象的首地址
    cout << d <<endl;
    long* pvptr = (long*)d;  //pvtr的值此时就是d,而*pvptr的值就等于下面vptr的值,而vptr的值就是虚函数表的首地址
    cout << hex<<*pvptr << endl;   
    long* vptr = (long*)*pvptr;   //vptr的值等于*pvptr
    cout << vptr << endl;

    Func f = (Func)vptr[0];
    Func g = (Func)vptr[1];
    Func h = (Func)vptr[2];

    f();
    g();
    h();

}

输出结果:

size of Base: 8
0x10a9030
400f88
0x400f88
base::f
derive::g
base::h

\color{blue}{解析}

  • 在64位操作系统中,指针的长度为8字节.
  • new一个对象时, 只为类中成员变量分配空间, 对象之间共享成员函数;所以我们从sizeof(Base)=8可以看到g++编译在类中自动添加了一个8字节的成员变量,这个变量就是vptr指向虚函数表的指针.

虚函数表的作用

  • 虚函数表是一个存储成员函数指针的数据结构;
  • 虚函数表是由编译器自动生成与维护的;
  • virtual成员函数会被编译器放入虚函数表中;
  • 存在虚函数时,每个对象都有一个指向虚函数的指针(vptr指针);
  • 在实现多态的过程中,父类和派生类都有vptr指针;
  • 父类对象的vptr指向父类的虚函数表,子类对象的vptr指向子类的虚函数表;定义子类对象时,vptr先指向父类的虚函数表,在父类构造完成之后,子类的vptr才指向自己的虚函数表.
    上面的程序中:


    image.png

    从运行结果也可以看出程序的设计中:

  • d对象的首地址就是pvptr.
  • 取pvptr的值就是vptr-----虚函数表的地址,即*pvptr=vptr;
  • 取vptr中[0][1][2]的值就是这三个函数的地址
  • 通过函数地址就直接可以运行三个虚函数了。
  • 函数表中Base::g()函数指针被Derive中的Derive::g()函数指针覆盖, 所以执行的时候是调用的Derive::g();

改程序采用gdb调试运行的输出:

//载入main
gdb main
//列出Base类
(gdb) list Base
1   #include <iostream>
2   
3   using namespace std;
4   
5   class Base {
6   public:
7       virtual void f() {cout<<"base::f"<<endl;}
8       virtual void g() {cout<<"base::g"<<endl;}
9       virtual void h() {cout<<"base::h"<<endl;}
10  };
//查看Base类中各个函数的地址
(gdb) info line 7
Line 7 of "test.cpp" starts at address 0x400bd6 <Base::f()>
   and ends at 0x400be2 <Base::f()+12>.
(gdb) info line 8
Line 8 of "test.cpp" starts at address 0x400c02 <Base::g()>
   and ends at 0x400c0e <Base::g()+12>.
(gdb) info line 9
Line 9 of "test.cpp" starts at address 0x400c2e <Base::h()>
   and ends at 0x400c3a <Base::h()+12>.
//列出派生类的代码
(gdb) list Derive
7       virtual void f() {cout<<"base::f"<<endl;}
8       virtual void g() {cout<<"base::g"<<endl;}
9       virtual void h() {cout<<"base::h"<<endl;}
10  };
11  
12  class Derive : public Base{
13  public:
14      void g() {cout<<"derive::g"<<endl;}
15  };
//查看派生类中重写的虚函数的地址
(gdb) info line 14
Line 14 of "test.cpp" starts at address 0x400c5a <Derive::g()>
   and ends at 0x400c66 <Derive::g()+12>.
//start 运行,并按n逐步调试
18  int main () {
(gdb) n
19      cout<<"size of Base: "<<sizeof(Base)<<endl;
(gdb) n
size of Base: 8
22      Base b;
(gdb) n
23      Base *d = new Derive();
(gdb) n
25      long* pvptr = (long*)d;
(gdb) n
26      cout << *pvptr << endl;
(gdb) n
4197784
27      long* vptr = (long*)*pvptr;
(gdb) n
28      cout << *vptr << endl;
(gdb) n
4197334
30      Func f = (Func)vptr[0];
(gdb) n
31      Func g = (Func)vptr[1];
(gdb) n
32      Func h = (Func)vptr[2];
(gdb) n
34      f();
(gdb) n
base::f
35      g();
(gdb) n
derive::g
36      h();
(gdb) n
base::h
38      return 0;
//print d对象,可以发现d的对象的首地址与vptr的首地址相同,也就是函数表的地址
(gdb) p *d  即等于p *pvptr
$2 = {_vptr.Base = 0x400d98 <vtable for Derive+16>}
(gdb) p vptr
$3 = (long *) 0x400d98 <vtable for Derive+16>
//查看虚函数表内函数的地址,看看是否与之前查看的函数地址一致
(gdb) p (long*)vptr[0]
$4 = (long *) 0x400bd6 <Base::f()>  //与Base类中相同
(gdb) p (long*)vptr[1]
$5 = (long *) 0x400c5a <Derive::g()> //与派生类中相同
(gdb) p (long*)vptr[2]
$6 = (long *) 0x400c2e <Base::h()>  //与Base类中相同

子类父类中关于指针转换的问题

\color{blue}{代码}

#include <iostream>

using namespace std;

class A
{
public:
    virtual void funA() = 0;     //纯虚函数
    int m_testA;
};

class B
{
public:
    virtual void funB(int c) = 0;    //纯虚函数
    int m_testB;
};

class C : public A, public B
{
public:
    virtual void funA(){cout << "funA" << endl;}
    void funB(int c){cout << c << endl;}

};

int main()
{
    C *ptest = new C;
    void *p = (void*)ptest;  //p=ptest
    //虽然pA是父类A的指针,但运行虚函数时是从虚表里找到可以执行的虚函数,且含有纯虚函数的类不能被实例化
    A *pA = (A*)p;    
    pA->funA();
    B *pB = (B*)p;
    pB -> funB(2);

    pB = (B*)ptest;
    pB -> funB(2);
    return 0;
}

输出结果:

funA
funA
2

\color{blue}{分析}
A pA = (A)p;这一条命令中的p为class C类型,所以他直接调用C类型中的funA()函数;而在 B pB = (B)p输入这条命令之后,并没有运行输出C类中的funb函数而运行了funA函数,但pB = (B*)ptest;就可以正常输出funB()内容.
C类中的对象内存布局:

A---------------> vfptr
A--------------->m_testA
B--------------->vfptr
B--------------->m_testB 

B pB = (B)p这条命令输入之后还是将ptest的首地址按照A类进行解析了.具体要从汇编底层角度去看.
原因就在于这是一个多重继承,我们提前把ptest指针转换为了void*指针,即把this指针进行了转换,转换为了其他类型,然后再转换为父类指针.犹如有个对象需要delete时,一定要确保指针类型是原来的类型再做delete,否则会导致析构函数没有调用而内存泄漏.

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容