C++对象模型之多态

虚函数:在理解多态之前,我们需要先来理解什么叫做虚函数?在成员函数之前冠以关键字“virtual”时,此时这个函数被称之为“虚函数”, 当在子类中定义了一个和父类完全一样的虚函数时,可以称为子类的这个函数重写(或者覆盖了)父类的这个虚函数。简单来说,虚函数使方法在基类和派生类中的行为不同。

多态:很多时候,我们希望同一个方法在派生类和基类的行为是不一样的,换句话来说,方法的行为应取决于调用该方法的对象,这种较复杂的行为称之为“多态”。

来看一个例子:

#include<iostream>
using namespace std;
class person{
public:
void fun()
	{
		cout<<"I am person"<<endl;
	}

};
class student : public person{
public:
	void fun()
	{
		cout<<"I am student"<<endl;
	}
};

int main()
{
	//Test();
	person p;
	student s; 
	person& p1=p;//基类对象赋给基类引用
	p1.fun();
	person& p2=s;//派生类对象赋给基类引用
	p2.fun();
	system("pause");
	return 0;
}

运行结果为:


我们发现两次调用fun函数都调用了基类的fun函数,但是如果我们将fun函数定义为虚函数呢?


很明显,在fun函数前加上关键字virtual后,两次调用分别调用了基类和派生类的fun; 说明了一个问题:如果方法是通过引用或者指针而不是直接通过对象调用时(对象不能构成多态),如果没有使用关键字virtual,程序将根据引用或者指针的类型来选择方法;如果使用了关键字virtual,程序将根据引用或者指针指向的对象类型来选择方法。

构成多态的两个必要条件:

不构成多态的常规调用--和 对象的类型有关;

构成多态--和指向对象有关

1.父类的指针或者引用;

2.调用的函数必须是虚函数的重写;

通过上述例子,对虚函数的总结如下:

1. 派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同。(协变除外,协变指返回值可以不同,返回值为父类的指针或者引用)
2. 基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。
3. 只有类的成员函数才能定义为虚函数。
4. 静态成员函数不能定义为虚函数。
5. 如果在类外定义虚函数,只能在声明函数时加virtual,类外定义函数时不能加virtual。
6. 构造函数不能为虚函数(构造函数除了初始化数据成员还会初始化虚表),虽然可以将operator=定义为虚函数,但是最好不要将operator=定义为虚函数,因为容易使用时容易引起混淆。
7. 不要在构造函数和析构函数里面调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会发生未定义的行为。

8. 最好把基类的析构函数声明为虚函数,这样可以保证释放派生对象时,按正确的顺序调用析构函数(析构函数比较特殊,因为派生类的析构函数跟基类的析构函数名称不一样,但是构成覆盖,这里是因为编译器做了特殊处理,导致基类和派生类的析构函数名一致)

现在我们大概了解了多态是什么以及需要注意的一些问题,那么很好奇多态底层究竟是如何实现的呢?

这里先来举一个简单的例子:

#include<iostream>
using namespace std;
class  Base{
public:
	virtual void fun1()
	{
	}
private:
	int a;
};
int main()
{
	cout<<sizeof(Base)<<endl;
	Base b;
	system("pause");
	return 0;
}

猜一下,程序的运行结果是4吗?No!!运行结果为8。为什么是8呢?类Base里面明明只有一个整型成员a呀!通过监视窗口我们可以看到:


对象b里面不仅存了一个数据成员a,还存在一个指针,这个指针其实就是虚表指针(全称为虚函数表指针)。虚函数是通过一块连续的内存来存储虚函数的地址。

#include<iostream>
using namespace std;
class  Base{
public:
	virtual void fun1()
	{
		cout<<"Base::fun1"<<endl;
	}
	virtual void fun2()
	{
		cout<<"Base::fun2"<<endl;
	}
    void fun3()
	{
	}
private:
	int a;
};
class Derive:public Base
{
public:
	virtual void fun1()
	{
		cout<<"Derive::fun1()"<<endl;
	}
	virtual void fun3()
	{
	   cout<<"Derive::fun3()"<<endl;
	}
};
int main()
{
	cout<<sizeof(Base)<<endl;
	Base b;
	Derive d;
	Base* p=&b;
	p->fun2();
	p->fun1();
	system("pause");
	return 0;
}

通过监视窗口可以看到:


在子类对象的虚表中,继承了父类的虚函数,父类对象的虚表中有两个虚函数时没有问题的,子类的虚函数表fun1覆盖了父类的fun1;但是在程序中子类对象的虚表还有一个fun3函数为虚函数,却没有出现在子类的虚表中,难道它真的不存在吗?

我们可以通过一段代码打印虚表:

#include<iostream>
using namespace std;
class Base
{
public :
virtual void func1()
{
    cout<<"Base::func1" <<endl;
}
virtual void func2()
{
    cout<<"Base::func2" <<endl;
}
private :
     int a ;
};
class Derive :public Base
{
public :
virtual void func1()
{
  cout<<"Derive::func1" <<endl;
}
virtual void func3()
{
  cout<<"Derive::func3" <<endl;
}
virtual void func4()
{
  cout<<"Derive::func4" <<endl;
}
private :
int b ;
};
typedef void (* FUNC) ();//便于看出打印出来的是哪个函数的地址
void PrintVTable (int** VTable)
{
cout<<" 虚表地址>"<< VTable<<endl ;
for (int i = 0; VTable[i ] != NULL; ++i)//虚函数表以NULL结束
{
printf(" 第%d个虚函数地址 :0X%p,->", i , VTable[i ]);
FUNC f = (FUNC) VTable[i ];
f();//调用对应的函数
}
cout<<endl ;
}
void Test1 ()
{
Base b1 ;
Derive d1 ;
Derive d2 ;
int** VTable1 = (int**)(*( int**)&b1 );//拿到指向虚表的指针
int** VTable2 = (int**)(*( int**)&d1 );//根据平台(64位或者32位)来确定解引用一次取多少个字节
int** VTable3 = (int**)(*( int**)&d2 );//提高程序的可移植性
PrintVTable(VTable1 );
PrintVTable(VTable2 );
PrintVTable(VTable3 );
}

int main()
{
	Test1();
	system("pause");
	return 0;
}

运行结果为:


可以看出,子类里面新增的虚函数实际上是存在虚表里的,所以有些问题通过监视窗口是看不出来的,因为有些场景下编译器进行了优化。通过上述代码和运行结果来看,同类型的对象共用一份虚函数表。

多继承下的虚函数表:

#include<iostream>
using namespace std;
class Base1
{
public :
virtual void func1()
{
    cout<<"Base1::func1" <<endl;
}
virtual void func2()
{
    cout<<"Base1::func2" <<endl;
}
private :
     int a ;
};
class Base2{
public :
virtual void func1()
{
    cout<<"Base2::func1" <<endl;
}
virtual void func2()
{
    cout<<"Base2::func2" <<endl;
}
private :
     int b ;
};
class Derive :public Base1,public Base2
{
public :
virtual void func1()
{
  cout<<"Derive::func1" <<endl;
}
virtual void func3()
{
  cout<<"Derive::func3" <<endl;
}
virtual void func4()
{
  cout<<"Derive::func4" <<endl;
}
private :
int d;
};

int main()
{
	cout<<sizeof(Derive)<<endl;
	system("pause");
	return 0;
}

上述程序的运行结果为20,并不是16,因为Base1和Base2各有一个虚表指针。如图,通过监视窗口可以看到:


多继承的虚函数对象模型如下:


继承的两个基类的虚函数地址不同,没有共用一张虚表。这又有一个问题,子类Deriver中的func3和func4都是虚函数,那么这两个虚函数时放在Base1的虚表里,还是放在Base2的虚表里呢?我们还是可以向上面一样通过打印虚表来看一下,代码如下:

#include<iostream>
using namespace std;
class Base1
{
public :
virtual void func1()
{
    cout<<"Base1::func1" <<endl;
}
virtual void func2()
{
    cout<<"Base1::func2" <<endl;
}
private :
     int a ;
};
class Base2{
	public :
virtual void func1()
{
    cout<<"Base2::func1" <<endl;
}
virtual void func2()
{
    cout<<"Base2::func2" <<endl;
}
private :
     int b ;
};
class Derive :public Base1,public Base2
{
public :
virtual void func1()
{
  cout<<"Derive::func1" <<endl;
}
virtual void func3()
{
  cout<<"Derive::func3" <<endl;
}
virtual void func4()
{
  cout<<"Derive::func4" <<endl;
}
private :
int d ;
};
typedef void (* FUNC) ();
void PrintVTable (int* VTable)
{
cout<<" 虚表地址>"<< VTable<<endl ;
for (int i = 0; VTable[i ] != 0; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i , VTable[i ]);
FUNC f = (FUNC) VTable[i ];
f();
}
cout<<endl ;
}

int main()
{
	Derive d;
    int* VTable1= (int*)(*( int*)&d );
    PrintVTable(VTable1 );
    int* VTable2= (int*)(*( int*)((char*)&d +sizeof(Base1)));
    PrintVTable(VTable2);
	system("pause");
	return 0;
}

运行结果为:


第一个虚表为子类Base1的虚表,第二个虚表为父类Base2的虚表,可以看出func4放在了Base1的虚表里,说明子类新增的虚函数时存在于Base1中的,一般来说,先继承哪个父类,就把新增的虚函数地址放在哪个父类的虚函数表里。

注意:并不是所有的虚函数调用都要去虚表里面去找函数的地址,只有构成多态的时候调用虚函数才会到虚表里面去找。

菱形继承

存在了多继承,那么就会存在菱形继承;

我们来通过一段代码看一下什么叫菱形继承:

#include<iostream>
using namespace std;
class A
{
public:
	virtual void fun1()
	{

	}
public:
	int _a;
};
class B:public A
{
public:
	virtual void fun1()
	{

	}
	int _b;
};
class C:public A
{
public:
	virtual void fun1()
	{

	}
	int _c;
};
class  D:public B,public C
{

public:
	int _d;

};


int main()
{
	cout<<sizeof(D)<<endl;
	D d;
	d.B::_a=1;
	d.C::_a=2;
	d._b=3;
	d._c=4;
	d._d=5;
	system("pause");
    return 0;
}

经过调试,通过内存窗口我们可以看到如下的内存模型:


所以可以得出菱形继承的对象模型如下:


菱形继承的隐患:造成了数据冗余和二义性问题。

菱形虚拟继承

代码如下:

#include<iostream>
using namespace std;
class A
{
public:
	virtual void fun1()
	{}
public:
	int _a;
};
class B:virtual public A
{
public:
	virtual void fun1()
	{}
	int _b;
};
class C:virtual public A
{
public:
	virtual void fun1()
	{}
	int _c;
};
class  D:public B,public C
{
public:
	void fun1()
	{}
public:
	int _d;

};


int main()
{
	D d;
	d.B::_a=1;
	d.C::_a=2;
	d._b=3;
	d._c=4;
	d._d=5;
	system("pause");
    return 0;
}

通过观察内存窗口:


在单步调试过程中,可以发现最后的那个地址(0x006FF740)里面的内容开始变为1,后来又变为2,说明这个地址里面存放的是基类A里面的_a,并且因为是虚拟继承的原因,B和C里面共享一个A类数据成员。

所以我们可以得出菱形虚拟继承的对象模型如下:


那么我们把上述代码中的B类变为如下后会出现什么问题呢?

class B:virtual public A
{
public:
	virtual void fun1()
	{}
	virtual void fun3()
	{}
	int _b;
};

运行调试后,我们打开内存窗口:


不难发现,B类比前面多了4个字节;我们很容易想到,B类重写了A类的fun1(),并且有了自己的虚函数fun3(),所以我们会猜测多的这4个字节会不会是B类的虚函数表指针呢?是的,没错,再来捋一捋D的这个内存结构图:


类B有两个指针,分别是虚函数表指针和虚基表指针,那前面两个指针究竟哪个是虚函数表指针,哪个是虚基表指针呢?

我们分别进入到这两个地址里面去观察一下:

第一个地址:0x0136ccc8


这就是一个普通的虚函数表,里面存放的是B类的虚函数的地址。

第二个地址:0x0136cce4


看到这里,我们就会明白,类B对象模型的这两个指针确实一个是虚函数表指针,一个是虚基表指针,并在在VS这个平台下虚函数表指针在虚基表指针的前面(其他平台不一定是这样),虚函数表里面存放的是虚函数的地址,虚基表里面存放了两个内容:分别为相对于虚函数表的偏移量和相对于公共基类的偏移量。

说到这里,我们可能还会抱有一种怀疑的态度,我们不妨通过代码再来验证一下:

#include<iostream>
using namespace std;
class A
{
public:
	virtual void fun1()
	{}
	virtual void fun2()
    {}

public:
	int _a;
};
class B:virtual public A
{
public:
	virtual void fun1()
	{}
	virtual void fun3()
	{}
	int _b;
};
class C:virtual public A
{
public:
	virtual void fun1()
	{}
	virtual void fun4()
	{}
	int _c;
};
class  D:public B,public C
{
public:
	virtual void fun1()
	{}
	virtual void fun5()
	{}
public:
	int _d;

};

int main()
{
	cout<<"D的大小为:"<<sizeof(D)<<endl;
	D d;
	d._a=1;
	d._b=2;
	d._c=3;
	d._d=5;
	system("pause");
        return 0;
}

想一下,如果上面分析正确,那么D的大小应该是多大?首先B类有一个虚表指针和一个虚基表指针以及自己的数据成员_b,大小为12;C类也有一个虚表指针和一个虚基表指针以及自己的数据成员_c,大小为12;D类有一个成员_d,大小为4;A类有一个虚表指针和自己的成员_a,大小为8;总大小应该为36个字节。那么到底是不是呢?

程序的运行结果为:


完全符合预期!我们再通过内存窗口观测一下对象模型:


再来一段代码打印一下上述对象模型的虚函数表:

#include<iostream>
using namespace std;
class A
{
public:
	virtual void fun1()
	{
		cout<<"A::fun1()"<<endl;
	}
	virtual void fun2()
    {
		cout<<"A::fun2()"<<endl;
	}

public:
	int _a;
};
class B:virtual public A
{
public:
	virtual void fun1()
	{
		cout<<"B::fun1()"<<endl;
	}
	virtual void fun3()
	{
		cout<<"B::fun3()"<<endl;
	}
	int _b;
};
class C:virtual public A
{
public:
	virtual void fun2()
	{
		cout<<"C::fun2()"<<endl;
	}
	virtual void fun4()
	{
		cout<<"C::fun4()"<<endl;
	}
	int _c;
};
class  D:public B,public C
{
public:
	virtual void fun3()
	{
		cout<<"D::fun3()"<<endl;
	}
	virtual void fun5()
	{
		cout<<"D::fun5()"<<endl;
	}
public:
	int _d;

};
typedef void (* FUNC) ();
void PrintVTable (int** VTable)
{
cout<<" 虚表地址>"<< VTable<<endl ;
for (int i = 0; VTable[i ] != NULL; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i , VTable[i ]);
FUNC f = (FUNC) VTable[i ];
f();
}
cout<<endl ;
}
int main()
{
	D d;
	d._a=1;
	d._b=2;
	d._c=3;
	d._d=5;
	PrintVTable((int**)(*((int**)&d)));//D
	PrintVTable((int**)((*(int**)((char*)&d+sizeof(B)-sizeof(A)))));//B
	PrintVTable((int**)((*(int**)((char*)&d+sizeof(D)-sizeof(A)))));//C
    system("pause");
    return 0;
}

运行结果为:


刚开始一看,看到这个运行结果还有点懵逼,这个结果怎么出来的呢?根据这个结果,我们先来画出D类的内存模型为:


这样还是不够清晰,我们来一步步进行分析:

菱形虚拟继承是多继承和虚继承的结合,B和C是从基类虚拟继承而来所以会有两个偏移量表,在继承过程中先构造D类对象,形成一个虚表.此时的虚表为:

&A::fun1()   &A::fun2();

B虚继承A:fun1()构成重写,并且新增加一个虚函数fun3();形成两个虚表:

&B::fun3()   和  &B::fun1() ,&A::fun2()

C虚继承A:fun2构成重写,并且新增了虚函数fun4(),形成两个虚表:

&C:fun4()   和  &A::fun1() ,&C::fun2()

在多继承时,基类的初始化顺序按照声明顺序依次执行:所以先执行public B:在虚继承中,派生类中未构成重写的函数需要放在基类中第一个虚表的后面,所以此时的虚表变为:

&D::fun3(),&D::fun5() 和 &B::fun1(),&A::fun2()

再执行public C:派生类中构成重写的虚函数放在了B的虚表里,因为fun4()没有重写,所以fun4()的虚表不变,此时虚表为:

&B::fun1(),&A::fun2() 和 &A::fun1(),&C::fun2()

此时相对于基类构成了重写,此时两张虚表变为一张:

&B::fun1() ,&C::fun2()

所以,最终的虚表为:

&D::fun3(),&D::fun5()

&C::fun4()

&B::fun1(),&C::fun2()

来一张图更清晰的理解一下这个过程:


静态联编和动态联编

函数名联编:将源代码中的函数调用解释为执行特定的函数代码块。在C语言中,每个函数名都对应不同的函数,但在C++中,由于函数重载,编译器必须查看函数参数和函数名才能确定使用哪个函数。其实编译器可以在编译过程中完成这种联编。

静态联编:在 编译过程中的联编,又称“早期联编”。

动态联编:如果基类中有虚函数,那么具体该使用哪个函数在编译期是无法确定的,因为编译器不知道用户将使用哪种类型的对象,所以编译器必须生成能够在程序运行时选择正确的虚方法的代码。

我们一般可以这样理解:编译器对非虚方法使用静态联编。对虚方法使用动态联编。

再看一段代码:

#include<iostream>
using namespace std;
class Base
{
public :
virtual void func1()
{
cout<<"Base::func1" <<endl;
}
virtual void func2()
{
cout<<"Base::func2" <<endl;
}
void display ()
{
cout<<"display()" <<endl;
}
void display (int i)
{
cout<<"display(int i)->" <<i<< endl;
}
private :
int a ;
};
class Derive :public Base
{
public :
virtual void func1()
{
cout<<"Derive::func1" <<endl;
}
private :
int b ;
};
void func (Base& b)
{
b.func1 ();
//b.func2();
b.display ();
b.display (10);
}
void Test1 ()
{
Base b1 ;
Derive d1 ;
func(b1 );
func(d1 );
}
int main()
{
	Test1();
	system("pause");
	return 0;
}

运行结果为:


对于下面两句代码我们转到反汇编层去看一下:



很明显可以看到对于不是虚函数的display而言,去哪调用函数是在编译期就决定的,即静态多态。而对于是虚函数的func1而言,运行时要去虚表里面去查找调用函数的地址,即动态多态。







猜你喜欢

转载自blog.csdn.net/qq_39344902/article/details/80206225