C++中虚函数机制分析


1 虚函数机制

  C++中的虚函数主要是为实现多态。多态就是用父类型的指针指向其子类的实例对象,然后通过父类的指针调用子类实例对象的成员函数。这可以让父类的指针有“多种形态”,是一种泛型技术使用不变代码来实现可变算法)。

         包含虚函数的类或者从包含虚函数基类派生的类,在创建对象时候,编译器会悄悄在对象内存空间额外分配一个虚表指针(VPTR),该指针指向类的虚表(VTABLE)。虚表VTABLE用来存储虚虚函数地址,可以看作是一个函数指针数组,它属于类但不属于对象,供对象共享访问。虚表指针VPTR属于对象,用来访问类的虚表VTABLE。如果派生类是单一继承则只有一个VPTR,如果是多重继承则可能有多个VPTR。在C++的标准规格说明书中说明,编译器必需要保证虚函数表的指针VPTR存在于对象实例内存空间中最前面的位置

         通过基类指针调用虚函数(多态调用)时,首先取得对象V P T R,然后通过它在V TA B L E表中查找函数地址,这样就能调用正确的函数。


2 虚函数内存布局

    以下内容在Ubuntu 16.04 + gcc version 5.4.0上验证。

2.1 多重普通继承

1)代码片段如下:

class B{
	public:
		B():b(5) {}
		virtual void fb1() { cout << "[B::fb1]"; }
		virtual void fb() { cout << "[B::fb]"; }
	private:
		int b;		
};

class B1: public B{
	public:
		B1():b1(10) {}
		virtual void fb1() { cout << "[B1::fb1]"; }
		virtual void fb2() { cout << "[B1::fb2]"; }
		virtual void fb3() { cout << "[B1::fb3]"; }
	private:
		int b1;		
};


class B2: public B{
	public:
		B2():b2(20) {}
		virtual void fb1() { cout << "[B2::fb1]"; }
		virtual void fb2() { cout << "[B2::fb2]"; }
		virtual void fb3() { cout << "[B2::fb3]"; }
	private:


		int b2;


};


class EXT : public B1, public B2{
	public:
		EXT():e(30) {}
		virtual void fb1() { cout << "[EXT::fb1]"; }
		virtual void fe1() { cout << "[EXT::fe1]"; }
	private:
		int e;
};



#define RED "\033[31m"
#define GREEN "\033[32m"
#define YELLOW "\033[33m"
#define BLUE "\033[34m"
#define RESET "\033[0m"


typedef void(*fun)(void);


int main()

{

    fun pfun = NULL;
    EXT e;

    int **pVtab = (int**)&e;

    cout<<BLUE<<"sizeof class B "<<sizeof(class B)<<endl;<span style="color:#3366FF;">//4(B::vptr)+4(B::b)</span>
    cout<<BLUE<<"sizeof class B1 "<<sizeof(class B1)<<endl;<span style="color:#3366FF;">//4(B1::vptr)+4(B::b)+4(B1::b1)</span>
    cout<<"sizeof class B2 "<<sizeof(class B2)<<endl;<span style="color:#3366FF;">//4(B2::vptr)+4(B::b)+4(B2::b2)</span>
    cout<<"sizeof class EXT "<<sizeof(class EXT)<<endl<<endl;<span style="color:#3366FF;">//4(B1::VPTR)+4(B::b)+4(B1:b1)+4(B2:VPTR)+4(B:b)+4(B2:b2)+4(EXT::e)
</span>
    cout<<RED<<"B1::VPTR1->";
    pfun = (fun)pVtab[0][0];
    pfun();


    pfun = (fun)pVtab[0][1];
    pfun();

    pfun = (fun)pVtab[0][2];
    pfun();

    pfun = (fun)pVtab[0][3];
    pfun();

    pfun = (fun)pVtab[0][4];
    pfun();

    cout<<"\nB::b "<<pVtab[1]<<endl;
    cout<<"B1::b1 "<<pVtab[2]<<endl;

    cout<<YELLOW<<"B2::VPTR2->";
    pfun = (fun)pVtab[3][0];
    pfun();

    pfun = (fun)pVtab[3][1];
    pfun();

    pfun = (fun)pVtab[3][2];
    pfun();

    pfun = (fun)pVtab[3][3];
    pfun();

    cout<<"\nB:b "<<pVtab[4]<<endl;
    cout<<"B2:b2 "<<pVtab[5]<<endl;
    cout<<"EXT::e "<<pVtab[6]<<endl<<endl;

    cout<<RESET<<endl;

    e.fb1();
    cout<<endl;

    e.fe1();
    cout<<endl;

    //以下必须指定作用域调用否则有二义性,对于对象e其2基类均有如下函数
    e.B1::fb();
    cout<<endl;

    e.B2::fb();
    cout<<endl;

    e.B1::fb2();
    cout<<endl;

    e.B2::fb2();
    cout<<endl;

    e.B1::fb3();
    cout<<endl;

    e.B2::fb3();
    cout<<endl;


    return 0;

} 

2)对象e内存布局如下指示:

C++中虚函数机制分析_第1张图片

从运行打印结果看:

1)对象e占用内存大小为28字节(4(B1::VPTR)+4(B::b)+4(B1:b1)+4(B2:VPTR)+4(B:b)+4(B2:b2)+4(EXT::e)=28);

2)虚表指针VPTR1和VPTR2分别指向不同的虚表VTABLE;

3)子类的非重写虚函数(EXT::fe1)放在了继承的第一基类(B1)的虚表后面处;

4)子类重写虚函数(EXT::fb1)分别存在继承的第一基类(B1)和第二基类(B2)虚表中,覆盖了基类的相应函数,这样就实现了通过基类指针调用子类对象函数的多态特性;

5)整体内存布局按照子类继承基类的顺序依次分配,B1->B2->EXT;

6)对于各个类首先分配虚表指针然后分配成员变量;

7)由于B是B1和B2的公共基类,所以导致了类B1和类B2对于类B的重复继承(B::b有2份);

8)从2张虚表看fb,fb2,fb3在子类EXT的2个基类虚表中均可以找到,所以访问它们需要指定作用域否则产生二义性;


2.2 多重虚拟继承


2.2.1 公共基类是虚基类

1) 部分代码片段

class B{
	public:
		B():b(5) {}
		virtual void fb1() { cout << "[B::fb1]"; }
		virtual void fb() { cout << "[B::fb]"; }
	private:
		int b;		
};

class B1: <strong><span style="color:#3333FF;">virtual</span></strong> public B{
	public:
		B1():b1(10) {}
		virtual void fb1() { cout << "[B1::fb1]"; }
		virtual void fb2() { cout << "[B1::fb2]"; }
		virtual void fb3() { cout << "[B1::fb3]"; }
	private:
		int b1;		
};


class B2: <span style="color:#3333FF;"><strong>virtual</strong></span> public B{
	public:
		B2():b2(20) {}
		virtual void fb1() { cout << "[B2::fb1]"; }
		virtual void fb2() { cout << "[B2::fb2]"; }
		virtual void fb3() { cout << "[B2::fb3]"; }
	private:


		int b2;


};


class EXT : public B1, public B2{
	public:
		EXT():e(30) {}
		virtual void fb1() { cout << "[EXT::fb1]"; }
		virtual void fe1() { cout << "[EXT::fe1]"; }
	private:
		int e;
};

2)内存布局



从运行打印结果看:

1)对象e占用内存大小为28字节(4(B1::vptr)+4(B1::b1)+4(B2::vptr)+4(B2::b2)+4(EXT::e)+4(B::vptr)+4(B::b)=28);

2)有3个虚表指针B1::VPTR和B2::VPTR2和B::VPTR分别指向不同的虚表VTABLE;

3)子类的非重写虚函数(EXT::fe1)放在了继承的第一基类(B1)的虚表后面处;

4)子类重写虚函数(EXT::fb1)分别存在继承的第一基类(B1)和第二基类(B2)虚表中,覆盖了基类的相应函数,这样就实现了通过基类指针调用子类对象函数的多态特性;子类重写的虚函数(EXT::fb1)在虚基类虚表中不再出现。

5)整体内存布局按照子类继承基类的顺序依次分配,B1->B2->EXT->B; 虚基类(B)内存分配位于最后。

6)对于各个类首先分配虚表指针然后分配成员变量;

7)B是B1和B2的公共基类, 当其作为虚基类时候避免了类B1和类B2对于类B的重复继承(B::b只有一份);

8)从3张虚表看fb2,fb3在子类EXT的2个基类(B1和B2)虚表中均可以找到,所以访问它们需要指定作用域否则产生二义性;

   fb仅在公共基类(B)虚表中出现是唯一的public继承而来,可以直接访问不用指定作用域。

9)虚拟继承关系,虚基类和其派生的子类均有各自虚表Vtable; 普通非虚拟继承关系,仅有基类有虚表子类将虚函数插入基类虚表;

10)虚拟继承关系时,子类重写的虚函数仅出现在子类虚表,虚基类虚表不再出现;


2.2.2 非公共基类是虚基类

1)代码片段

class B{
	public:
		B():b(5) {}
		virtual void fb1() { cout << "[B::fb1]"; }
		virtual void fb() { cout << "[B::fb]"; }
	private:
		int b;		
};

class B1: public B{
	public:
		B1():b1(10) {}
		virtual void fb1() { cout << "[B1::fb1]"; }
		virtual void fb2() { cout << "[B1::fb2]"; }
		virtual void fb3() { cout << "[B1::fb3]"; }
	private:
		int b1;		
};


class B2: public B{
	public:
		B2():b2(20) {}
		virtual void fb1() { cout << "[B2::fb1]"; }
		virtual void fb2() { cout << "[B2::fb2]"; }
		virtual void fb3() { cout << "[B2::fb3]"; }
	private:


		int b2;


};


class EXT : <strong><span style="color:#3333FF;">virtual</span></strong> public B1, <strong><span style="color:#3333FF;">virtual</span></strong> public B2{
	public:
		EXT():e(30) {}
		virtual void fb1() { cout << "[EXT::fb1]"; }
		virtual void fe1() { cout << "[EXT::fe1]"; }
	private:
		int e;
};


2)内存布局

C++中虚函数机制分析_第2张图片

1)对象e占用内存大小为32字节(4(EXT::vptr)+4(EXT::e)+4(B1::vptr)+4(B::b)+4(B1::b1)+4(B2::vptr)+4(B::b)+4(B2::b2)=32);

2)有3个虚表指针B1::VPTR和B2::VPTR2和EXT::VPTR分别指向不同的虚表VTABLE;

3)子类的非重写虚函数(EXT::fe1)放在了自己的虚表处,因为子类自己有虚表;

4)子类重写虚函数(EXT::fb1)仅存在于自己的虚表中; 子类重写的虚函数(EXT::fb1)在虚基类虚表中不再出现。

5)虚基类(B1和B2)内存分配位于其派生的子类EXT后。

6)对于各个类首先分配虚表指针然后分配成员变量;

7)B是B1和B2的公共基类, 但其是非虚基类导致了类B1和类B2对于类B的重复继承(B::b有2份);

8)从3张虚表看fb,fb2,fb3在子类EXT的2个基类(B1和B2)虚表中均可以找到,所以访问它们需要指定作用域否则产生二义性;

9)虚拟继承关系,虚基类和其派生的子类均有各自虚表Vtable; 普通非虚拟继承关系,仅有基类有虚表子类将虚函数插入基类虚表;

10)虚拟继承关系时,子类重写的虚函数仅出现在子类的虚表,虚基类的虚表或者其它虚表中不再出现;


    从上面虚函数实验结果看内存布局有如下特点:

1)分配内存时,如果类有虚表,那么先分配对象虚表指针再分配对象成员变量;

2)对于有公共基类的继承关系使用场景,公共基类最好作为虚基类,否则会出现重复继承情况,浪费空间;

3)对于纯粹普通继承(没有任何虚拟继承)场景,按照子类继承基类的先后次序关系依次分配基类和子类的内存;

4)虚基类和其派生子类都有对应虚表,这种场合被子类重写函数仅出现在子类虚表,其它虚表不再出现;

5)虚基类内存分配位于其派生子类之后;



你可能感兴趣的:(C++,虚函数,机制,内存布局)