【C++初阶学习】之 继承与多态(多态)

一、什么是多态

    多态通俗的来讲,就是多种状态,就是一个事物在不同条件下表现出的不同状态。比如出门这件事,受天气,气温不同条件的影响,会表现出不同的状态,下雨天出门就要打伞,夏天出门就要穿得凉快一点,冬天出门就要穿厚一点。同样是出门,因外界条件不同,会有多种具体的实现方式。

  • 多态的定义:多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。

二、多态的定义及实现

在继承中要构成多态还有两个条件:

1. 调用函数的对象必须是指针或者引用

2. 被调用的函数必须是虚函数,且完成了虚函数的重写

PS:指针的两种类型:静态类型-->声明变量的类型(编译期间已经确定)

                                     动态类型-->实际指向空间的类型(代码执行期间,若构成重写则会变成动态类型)

  • 虚函数就是加上virtual关键字的函数,重写虚函数就是在派生类中,有跟基类中名字,参数,类型,返回值完全相同的虚函数。
  • 有一个特殊情况也可以构成重写:协变。
//重写的虚函数的返回值可以不同,但是必须分别是基类指针和派生类指针或者基类
//引用和派生类引用。
class A{};
class B : public A {};
class Person {
public:
 virtual A* f() {return new A;}
};
class Student : public Person {
public:
 virtual B* f() {return new B;}
};

#析构函数的重写问题

只要基类中的析构函数为虚函数,就构成了析构函数的重写,无需与派生类同名。

因为在编译过程中,析构函数名称统一被替换为destructor,所以基类中的析构函数最好给成虚函数。

三、多态的原理

①、虚表的介绍及构建过程

论证方法:1.构造继承方式的场景
                  2.验证对象模型
                  3.验证虚表的内容(通过所掌握的内容推断)
 

从上图中看出,显然虚函数比普通成员函数多了一个叫_vfptr的东西,从名字上看,应该是存放了一个什么指针,下面来看看这是什么指针。

class Base
{//测试代码
public:
 virtual void Func1(){cout << "Base::Func1()" << endl;}//基类虚函数F1
 virtual void Func2(){cout << "Base::Func2()" << endl;}//基类虚函数F2
 void Func3(){cout << "Base::Func3()" << endl;}//基类普通函数F3
private:
 int _b = 1;
};
class Derive : public Base
{
public:
 virtual void Func1(){cout << "Derive::Func1()" << endl;}//派生类重写F1
private:
 int _d = 2;
};
int main()
{
 Base b;
 Derive d;
 return 0;
}

这个存放虚函数的表格,被叫做虚表。

总结一下:

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

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

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

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

 

②、多态函数的调用过程

要构成多态的调用形式:首先多态的两个条件都要满足
引用的哪个类的对象,将来调用具体类的虚函数
1.取虚表的地址&vfptr(取对象地址,从对象前四个字节中取虚表的地址)
2.传递this指针
3.从虚表中取将要调用虚函数的地址(&vfptr+虚函数在虚表中的偏移量)
4.调用该虚函数

③、单继承、多继承中的虚表构建规律

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(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	Base b;
	Derive d;
	// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的
	//指针数组,这个数组最后面放了一个nullptr
	// 1.先取b的地址,强转成一个int*的指针
	// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
	// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
	// 4.虚表指针传递给PrintVTable进行打印虚表
	// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面
    //没有放nullptr,导致越界,这是编译器的问题。清理解决方案,再编译就好了。
	VFPTR* vTableb = (VFPTR*)(*(int*)&b);
	PrintVTable(vTableb);
	VFPTR* vTabled = (VFPTR*)(*(int*)&d);
	PrintVTable(vTabled);
	return 0;
}

单继承中派生类的虚表构建:a.先将基类中的虚表内容拷贝一份到派生类虚表中。

                                               b.如果派生类重写了基 类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数。

                                               c.派生类自己新增加的虚函数按其在 派生类中的声明次序增加到派生类虚表的最后。

class Base1 {
public:
 virtual void func1() {cout << "Base1::func1" << endl;}
 virtual void func2() {cout << "Base1::func2" << endl;}
private:
 int b1;
};
class Base2 {
public:
 virtual void func1() {cout << "Base2::func1" << endl;}
 virtual void func2() {cout << "Base2::func2" << endl;}
private:
 int b2;
};
class Derive : public Base1, public Base2 {
public:
 virtual void func1() {cout << "Derive::func1" << endl;}
 virtual void func3() {cout << "Derive::func3" << endl;}
private:
 int d1;
};

多继承中派生类的虚表构建:

与单继承的不同点:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。


以上,继承与多态的知识点已经大体介绍完毕。还有一些细小的知识点如override,final关键字的用法,抽象类生成对象的函数,综合两者的菱形继承和菱形虚拟继承等,改日重开一贴再议。

如有错误、不准确之处,欢迎提议,感激不尽。

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章