C++三大特性----"多态"

    多态,顾名思义多种状态,在C++中大致可分为静态多态与动态多态,其中静态多态(早绑定)是编译器在编译期间通过实参的类型推断要调用哪个函数,若有对应的函数,则调用,否则报错,最能体现静态多态的就是函数重载以及泛型编程;而对于动态多态(也称晚绑定,动态绑定),则是在运行期间根据实参决定函数的调用的。

    下面我们重点来讨论动态绑定:

    一般来说,动态绑定有两个先决条件:首先在基类中必须存在以virtual关键字修饰的虚函数,而且这个虚函数在派生类当中必须被重写;其次,对于虚函数的调用必须使用基类类型的指针或对应的引用来进行调用。

    这里我们先分析一下这两个条件,首先什么是重写?

    在C++中,基类中可以被继承的成员函数大致可以分成两种,一种希望被直接继承而不要做任何改变的函数,另一种则是希望派生类进行覆盖的函数,也就是我们定义的虚函数,而重写则是在派生类中对原本基类中的虚函数进行重新定义并覆盖。

    这里要穿插一点东西,那就是在继承体系下,同名函数的三种关系重载,重写(覆盖)以及同名隐藏的区别。

1.重载:函数重载必须是在同一作用域(基类与派生类属于不同作用域),函数名相同,参数列表不同(参数的个数不同,或参数的类型不同,或参数的顺序不同),与返回值无关。

2.重写(覆盖):构成重写必须是在不同的作用域下(基类和派生类的作用域),函数名相同,参数相同,返回值相同(协变除外),基类中的函数必须以virtual关键字修饰,访问限定符可以不同。

3.同名隐藏:构成同名隐藏必须在不同作用域下(基类和派生类作用域),函数名相同,在基类和派生类中不构成重写。

    分析了重载,重写(覆盖)以及同名隐藏的区别,我们来回归主题,讲了这么多,我们一直在说一个东西,那就是虚函数,我们只知道虚函数是被virtual关键字修饰的成员函数,那么究竟有哪些函数可以作为虚函数呢?

    要明白这点,我们得深入的来分析一下动态绑定下的对象模型以及函数的调用过程。

测试代码1:

#include <iostream>
using namespace std;


class Base
{
public:
	virtual void funtest1()
	{
		cout<<"Base::funtest1()"<<endl;
	}

	virtual void funtest2()
	{
		cout<<"Base::funtest2()"<<endl;
	}
public:
	int _b;
};

class Dirvate:public Base
{
public:
	void funtest1()override
	{
		cout<<"Dirvate::funtest1()"<<endl;
	}

	void funtest2()override
	{
		cout<<"Dirvate::funtest2()"<<endl;
	}
public:
	int _d;
};

void Fun(Base& b)
{
	b.funtest1();
	b.funtest2();
}

int main()
{
	Base b;
	b._b=1;
	Dirvate d;
	d._b=2;
	d._d=3;
	Fun(b);
	Fun(d);
	return 0;
}

首先,我们来查看基类对象b的内存空间:


我们很明显发现,对象b的内存空间的前4个字节中存放了一个地址,而通过内存查看这个地址我们发现在这个地址附近存放了上图所示的类似于地址的内容。



通过查看汇编代码,我们发现在以基类对象b为实参时,对虚函数的调用底层实现如上图所示,都分别执行了一次call指令,而跳转的位置正式基类对象b的内存空间的前4个字节保存的指针所指向空间中保存的两个地址。

同样,我们也用同样的方法来查看派生类对象d的内存空间:


汇编代码底层实现:



我们发现和基类对象b的情况完全相同,也就是说对于有虚函数的类的对象在前4个字节会存放一个虚表指针,而这个指针指向一张虚表,而这张虚表当中存放着虚函数的入口地址。

为此我们进行进一步的验证:

测试代码2:

class Base
{
public:
	virtual void funtest1()
	{
		cout<<"Base::funtest1()"<<endl;
	}

	virtual void funtest2()
	{
		cout<<"Base::funtest2()"<<endl;
	}
public:
	int _b;
};

class Dirvate:public Base
{
public:
	void funtest1()override
	{
		cout<<"Dirvate::funtest1()"<<endl;
	}

	void funtest2()override
	{
		cout<<"Dirvate::funtest2()"<<endl;
	}
public:
	int _d;
};

typedef void (*fun)();

void Fun()
{
	Base b;
	
	fun* pfun=(fun*)(*(int*)(&b));
	while(*pfun)
	{
		(*pfun)();
		pfun=(fun*)((int*)pfun+1);
	}
}


int main()
{
	Fun();
	return 0;
}

测试结果:


很明显,上述测试用例并未对函数进行调用,而是通过对前4个字节中保存的地址进行强转,解引用来实现的,而结果表明确确实实进行了虚函数的调用,因此对于虚表中存放虚函数的入口地址的说法是正确的。

    因此,我们可以总结一点:当类中存在虚函数的时候,编译器一般都会维护一张虚表,里面存放着虚函数的入口地址,并在对象的前4个字节中保存一个虚表指针指向这张虚表。而对于虚函数的调用,则是通过虚表指针找到虚表,并进行相应的偏移找到相应的虚函数入口地址进行调用。

   由此,我们回过头来讨论一下,那些函数可以作为虚函数。

①构造函数:不能作为虚函数

        通过上面的讨论我们知道,调用虚函数必定得通过虚表指针和虚表,而能找到虚表指针的前提是必须得有对象,因为虚表指针就存放在对象的前4个字节中,而当我们还未进行构造函数的调用时,对象显然是未被创建的,因此根本无法找到虚表指针和虚表。

②析构函数:建议用作虚函数

        举个例子:

测试代码:

class A
{
public:
	A()
		:_a(1)
	{
		cout<<"A()"<<endl;
	}
	~A()
	{
		cout<<"~A()"<<endl;
	}
private:
	int _a;
};

class B:public A
{
public:
	B()
		:_b(2)
	{
		cout<<"B()"<<endl;
	}
	~B()
	{
		cout<<"~B()"<<endl;
	}
private:
	int _b;
};

int main()
{
	A* pa=new B;
	delete pa;
	return 0;
}
测试结果:


我们来分析这段代码:首先我们通过A类类型的指针pa构建了一个B类的对象,并用pa指向,但是在释放这块空间的时候,由于pa是A*类型的,所以只调用了A类的析构函数如上图所示,但是这样的话,我们根据上面的结果可以很明确的知道,程序将A类的那部分空间释放了,但B类的那部分空间呢?并没有释放,因此这个程序会出现内存泄漏的问题,而问题所在则是在于它构建了一个派生类的对象,但却只释放了基类对象对应的空间,而如果将析构函数声明为虚函数,就可以很好的规避这类的问题(虚函数的调用是取决于对象的类型的)。

    ③赋值运算符重载:可以用作虚函数,但不建议

        赋值运算符的重载成员函数满足作为虚函数的前提条件,但是它有一个问题:

class A
{
public:
	virtual A& operator=(A& a)
	{}
private:
	int _a;
};

class B:public A
{
public:
	virtual B& operator=(B& b)
	{}
private:
	int _b;
};

int main()
{
	A a;
	B b;
	A& ra=a;
	ra=b;//派生类对象给基类对象赋值(没问题)
	A& raa=b;
	raa=a;//基类对象给派生类对象赋值(不符合赋值兼容性规则,但在这里由于赋值运算符重载是虚函数,这里会调用派生类当中的赋值运算符重载函数)
	return 0;
}
    ④静态成员函数与友元函数

        首先,静态成员函数不存在this指针,它是整个类共享的,因此无法找到对象,更不要说找到虚表指针和虚表了。

        其次,对于虚函数,前提必须得是成员函数,而友元函数不是成员函数

综上,对于虚函数,我们可以总结出一条:任何构造函数之外的非静态成员函数均可作为虚函数,但实际用的时候,要不要作为虚函数,那得视情况而定,像赋值运算符重载虽然可以,但不建议作虚函数使用。

    既然我们已经得知对于带有虚函数的类的对象会有一个虚表指针指向一张虚表,那么下面我们来分析一下在不同的继承体系下,基类与派生类的虚表变化。

1.单继承

测试代码3:

class Base
{
public:
	virtual void Fun1()
	{
		cout<<"Base::Fun1()"<<endl;
	}

	virtual void Fun2()
	{
		cout<<"Base::Fun2()"<<endl;
	}
	int _b;
};

class Derive:public Base
{
public:
	virtual void Fun0()
	{
		cout<<"Derive::Fun0()"<<endl;
	}
	virtual void Fun2()
	{
		cout<<"Derive::Fun2()"<<endl;
	}

	int _d;
};

typedef void (*Fun)();
void FunTest()
{
	Base b;
	Fun* pFun=(Fun*)(*((int*)(&b)));
	while(*pFun)
	{
		(*pFun)();
		pFun=(Fun*)((int*)pFun+1);
	}

	Derive d;
	pFun=(Fun*)(*((int*)(&d)));
	while(*pFun)
	{
		(*pFun)();
		pFun=(Fun*)((int*)pFun+1);
	}
}

int main()
{
	FunTest();
	return 0;
}
测试结果:


由此,我们可以发现只要是虚函数就会将它的入口地址保存在虚表当中,并且是按照声明顺序存放的,而在单继承下,首先派生类将基类的虚表进行一份拷贝,若在派生类中有进行重写,则以派生类中的对应虚函数入口地址将原本基类中相对应的进行覆盖,否则不做修改,而对于派生类中新增的虚函数,把它们的地址放在末尾。


2.多继承

测试代码4:

class Base1
{
public:
	virtual void Fun1()
	{
		cout<<"Base1::Fun1()"<<endl;
	}

	virtual void Fun2()
	{
		cout<<"Base1::Fun2()"<<endl;
	}
	int _b1;
};

class Base2
{
public:
	virtual void Fun3()
	{
		cout<<"Base2::Fun3()"<<endl;
	}
	
	virtual void Fun4()
	{
		cout<<"Base2::Fun4()"<<endl;
	}

	int _b2;
};
class Derive:public Base1,public Base2
{
public:
	virtual void Fun0()
	{
		cout<<"Derive::Fun0()"<<endl;
	}
	virtual void Fun2()
	{
		cout<<"Derive::Fun2()"<<endl;
	}
	virtual void Fun3()
	{
		cout<<"Derive::Fun3()"<<endl;
	}

	int _d;
};

int main()
{
	Derive d;
	d._b1=1;
	d._b2=2;
	d._d=3;
	return 0;
}
查看派生类对象d的内存:

由此,我们发现对象b的内存中多出了两个虚表指针,而根据多继承的对象模型以及上面讨论过的单继承的虚表变化,我们可以想到这两个虚表指针分别指向从Base1和Base2的拷贝下来的虚表的。
所以,我们做了如下测试:

测试代码5:

class Base1
{
public:
	virtual void Fun1()
	{
		cout<<"Base1::Fun1()"<<endl;
	}

	virtual void Fun2()
	{
		cout<<"Base1::Fun2()"<<endl;
	}
	int _b1;
};

class Base2
{
public:
	virtual void Fun3()
	{
		cout<<"Base2::Fun3()"<<endl;
	}
	
	virtual void Fun4()
	{
		cout<<"Base2::Fun4()"<<endl;
	}

	int _b2;
};
class Derive:public Base1,public Base2
{
public:
	virtual void Fun0()
	{
		cout<<"Derive::Fun0()"<<endl;
	}
	virtual void Fun2()
	{
		cout<<"Derive::Fun2()"<<endl;
	}
	virtual void Fun3()
	{
		cout<<"Derive::Fun3()"<<endl;
	}

	int _d;
};

typedef void (*Fun)();
void FunTest()
{
	Derive d;

	cout<<"Base1:"<<endl;
	Base1& b1=d;
	Fun* pFun=(Fun*)(*((int*)(&b1)));
	while(*pFun)
	{
		(*pFun)();
		pFun=(Fun*)((int*)pFun+1);
	}

	cout<<"Base2:"<<endl;
	Base2& b2=d;
	pFun=(Fun*)(*((int*)(&b2)));
	while(*pFun)
	{
		(*pFun)();
		pFun=(Fun*)((int*)pFun+1);
	}

}


int main()
{
	FunTest();
	return 0;
}
测试结果:


由此,我们可以总结出多继承的虚表变化规则:

跟单继承类似,只要是虚函数就会将它的入口地址保存在虚表当中,并且是按照声明顺序存放的,而在多继承下,由于有多个基类,因此会拷贝多份虚表,并将地址存放在对应的前4个字节中,而对于拷贝下来的虚表,若当中虚函数在派生类中发生重写,则以派生类中的虚函数入口地址覆盖,否则不做改动,而对于自己新增的虚函数则将它的入口地址放在第一个继承的基类拷贝下来的虚表的末尾。

3.菱形继承

测试代码6:

class A
{
public:
	virtual void fun1()
	{
		cout<<"A::fun1()"<<endl;
	}

	int _a;
};

class B1:public A
{
public:
	virtual void fun1()
	{
		cout<<"B1::fun1()"<<endl;
	}
	virtual void fun2()
	{
		cout<<"B1::fun2()"<<endl;
	}

	int _b1;
};

class B2:public A
{
public:
	virtual void fun1()
	{
		cout<<"B2::fun1()"<<endl;
	}
	virtual void fun3()
	{
		cout<<"B2::fun3()"<<endl;
	}

	int _b2;
};

class C:public B1,public B2
{
public:
	virtual void fun0()
	{
		cout<<"C::fun0()"<<endl;
	}
	virtual void fun1()
	{
		cout<<"C::fun1()"<<endl;
	}
	virtual void fun2()
	{
		cout<<"C::fun2()"<<endl;
	}
	
	int _c;
};



int main()
{
	C c;
	c.B1::_a=1;
	c._b1=2;
	c.B2::_a=3;
	c._b2=4;
	c._c=5;

	return 0;
}
查看c对象的内存空间:

由此,我们发现在菱形继承中,与多继承一样,在对象中加入了两个虚表指针,并指向两个从基类B1,B2拷贝下来的虚表,至于虚表的变化,我们进行进一步测试:

测试代码7:

class A
{
public:
	virtual void fun1()
	{
		cout<<"A::fun1()"<<endl;
	}

	int _a;
};

class B1:public A
{
public:
	virtual void fun1()
	{
		cout<<"B1::fun1()"<<endl;
	}
	virtual void fun2()
	{
		cout<<"B1::fun2()"<<endl;
	}

	int _b1;
};

class B2:public A
{
public:
	virtual void fun1()
	{
		cout<<"B2::fun1()"<<endl;
	}
	virtual void fun3()
	{
		cout<<"B2::fun3()"<<endl;
	}
	virtual void fun4()
	{
		cout<<"B2::fun4()"<<endl;
	}

	int _b2;
};

class C:public B1,public B2
{
public:
	virtual void fun0()
	{
		cout<<"C::fun0()"<<endl;
	}
	virtual void fun1()
	{
		cout<<"C::fun1()"<<endl;
	}
	virtual void fun2()
	{
		cout<<"C::fun2()"<<endl;
	}
	virtual void fun4()
	{
		cout<<"C::fun4()"<<endl;
	}
	
	int _c;
};

typedef void (*Fun)();
void funtest()
{
	C c;

	cout<<"B1:"<<endl;
	B1& b1=c;
	Fun* pFun=(Fun*)(*((int*)(&b1)));
	while(*pFun)
	{
		(*pFun)();
		pFun=(Fun*)((int*)pFun+1);
	}

	cout<<"B2:"<<endl;
	B2& b2=c;
	pFun=(Fun*)(*((int*)(&b2)));
	while(*pFun)
	{
		(*pFun)();
		pFun=(Fun*)((int*)pFun+1);
	}
}

int main()
{
	funtest();
	return 0;
}
测试结果:


由此,我们可以发现菱形继承的虚表变化的规则基本和多继承一致,只不过,在菱形继承中对于起始的基类A中的虚函数,基类B1,B2都会继承一份,由此在派生类C的两个虚表中都会有fun1出现,对于两个虚表刚开始拷贝下来都并没有的虚函数(即新增的虚函数),则放在第一个虚表的末尾。

4.虚拟继承

测试代码8:

class A
{
public:
	virtual void fun1()
	{
		cout<<"A::fun1()"<<endl;
	}

	int _a;
};

class B:virtual public A
{
public:
	virtual void fun1()
	{
		cout<<"B::fun1()"<<endl;
	}
	virtual void fun2()
	{
		cout<<"B::fun2()"<<endl;
	}

	int _b;
};

int main()
{
	B b;
	b._a=1;
	b._b=2;
	return 0;
}

查看派生类对象b的内存空间:


由此,我们可以发现,在派生类对象的派生类自带部分,前4个字节存放虚表指针,后4个字节存放偏移量表指针,最后才是存放对象的成员变量,而基类的虚表指针与成员变量则是与虚拟继承规则相同,放在最后。最重要的一点,那就是原本单继承只有一个虚表指针,但在这里我们很清楚的发现,多了一个A类的虚表指针,因此,我们进行进一步测试。

测试代码9:

class A
{
public:
	virtual void fun1()
	{
		cout<<"A::fun1()"<<endl;
	}
	virtual void fun2()
	{
		cout<<"A::fun2()"<<endl;
	}

	int _a;
};

class B:virtual public A
{
public:
	virtual void fun1()
	{
		cout<<"B::fun1()"<<endl;
	}
	virtual void fun3()
	{
		cout<<"B::fun3()"<<endl;
	}

	int _b;
};

typedef void (*Fun)();
void funtest()
{
	B b;

	cout<<"A:"<<endl;
	A& a=b;
	Fun* pFun=(Fun*)(*((int*)(&a)));
	while(*pFun)
	{
		(*pFun)();
		pFun=(Fun*)((int*)pFun+1);
	}

	cout<<"B:"<<endl;
	pFun=(Fun*)(*((int*)(&b)));
	while(*pFun)
	{
		(*pFun)();
		pFun=(Fun*)((int*)pFun+1);
	}
}

int main()
{
	funtest();
	return 0;
}
测试结果:


由此我们可以得知,在虚拟继承的对象模型中,增加了一个A类的虚表指针,而指向的虚表中用来保存基类A类的虚函数,而正因为如此,A类的虚函数便不再出现在其他虚表当中了。

5.菱形虚拟继承

测试代码10:

class A
{
public:
	virtual void fun1()
	{
		cout<<"A::fun1()"<<endl;
	}

	int _a;
};

class B1:virtual public A
{
public:
	virtual void fun1()
	{
		cout<<"B1::fun1()"<<endl;
	}
	virtual void fun2()
	{
		cout<<"B1::fun2()"<<endl;
	}

	int _b1;
};

class B2:virtual public A
{
public:
	virtual void fun1()
	{
		cout<<"B2::fun1()"<<endl;
	}
	virtual void fun3()
	{
		cout<<"B2::fun3()"<<endl;
	}
	virtual void fun4()
	{
		cout<<"B2::fun4()"<<endl;
	}

	int _b2;
};

class C:public B1,public B2
{
public:
	virtual void fun0()
	{
		cout<<"C::fun0()"<<endl;
	}
	virtual void fun1()
	{
		cout<<"C::fun1()"<<endl;
	}
	virtual void fun2()
	{
		cout<<"C::fun2()"<<endl;
	}
	virtual void fun4()
	{
		cout<<"C::fun4()"<<endl;
	}
	
	int _c;
};
int main()
{
	C c;
	c._a=1;
	c._b1=2;
	c._b2=3;
	c._c=4;
	return 0;
}
查看对象c的内存空间:



鉴于上面对虚拟继承的分析,我们进行类似的测试:

测试代码11:

class A
{
public:
	virtual void fun1()
	{
		cout<<"A::fun1()"<<endl;
	}

	int _a;
};

class B1:virtual public A
{
public:
	virtual void fun1()
	{
		cout<<"B1::fun1()"<<endl;
	}
	virtual void fun2()
	{
		cout<<"B1::fun2()"<<endl;
	}

	int _b1;
};

class B2:virtual public A
{
public:
	virtual void fun1()
	{
		cout<<"B2::fun1()"<<endl;
	}
	virtual void fun3()
	{
		cout<<"B2::fun3()"<<endl;
	}
	virtual void fun4()
	{
		cout<<"B2::fun4()"<<endl;
	}

	int _b2;
};

class C:public B1,public B2
{
public:
	virtual void fun0()
	{
		cout<<"C::fun0()"<<endl;
	}
	virtual void fun1()
	{
		cout<<"C::fun1()"<<endl;
	}
	virtual void fun2()
	{
		cout<<"C::fun2()"<<endl;
	}
	virtual void fun4()
	{
		cout<<"C::fun4()"<<endl;
	}
	
	int _c;
};

typedef void (*Fun)();
void funtest()
{
	C c;

	cout<<"B1:"<<endl;
	B1& b1=c;
	Fun* pFun=(Fun*)(*((int*)(&b1)));
	while(*pFun)
	{
		(*pFun)();
		pFun=(Fun*)((int*)pFun+1);
	}

	cout<<"B2:"<<endl;
	B2& b2=c;
	pFun=(Fun*)(*((int*)(&b2)));
	while(*pFun)
	{
		(*pFun)();
		pFun=(Fun*)((int*)pFun+1);
	}

	cout<<"A"<<endl;
	A& a=c;
	pFun=(Fun*)(*((int*)(&a)));
	while(*pFun)
	{
		(*pFun)();
		pFun=(Fun*)((int*)pFun+1);
	}
}

int main()
{
	funtest();
	return 0;
}
测试结果:


由此,我们可以发现,菱形虚拟继承在菱形继承的基础上,增加了一个A类的虚表指针,并指向一张虚表,用于保存A类的虚函数的入口地址,以防止在继承过程中出现基类虚函数被B1继承的同时,被B2也继承了,导致调用函数时不明确的问题(二义性)。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值