多态和多态对象模型

一、虚函数&多态

虚函数--在类的成员函数前面加上virtual关键字,这个函数就被称为虚函数。

虚函数的重写--在子类中定义了一个与父类中虚函数完全相同的函数,则称子类的这个函数重写(覆盖)了这个函数。

多态--当我们使用基类的指针或引用调用上面所说的虚函数时,调用的是基类中的虚函数;当使用子类的指针或引用时调用的是子类的虚函数。

形成多态的条件--必须是父类的指针或引用;调用的函数必须是虚函数的重写;和建立指针和引用的对象有关。

    为了更加清楚的了解什么是多态,我们建立一个场景:一个学生都去买一个旅游景点的票,它们调用了同一个函数,却分别买到了成人票和学生票。

class Person
{
public:
	virtual void BuyTickets()
	{
		cout << "买成人票!" << endl;
	}
};
class Student : public Person 
{
public:
	virtual void BuyTickets()
	{
		cout << "买学生票!" << endl;
	}
};
void Buy(Person& p)
{
	p.BuyTickets();
}
int main()
{
	Person p;
	Student s;
	Buy(p);
	Buy(s);
	system("pause");
	return 0;
}

从结果中可以看出多态的特点--只与对象有关,跟它所指向的类型无关。


注意:

  • 派生类对虚函数进行重写时,要求函数名,返回值,参数完全与基类一致(协变除外);
  • 基类中定义了虚函数,派生类中始终保持它的特性;
  • 只有类的成员函数可以定义为虚函数;静态成员函数不能定义为虚函数;
  • 如果在类外定义虚函数,只需在声明处将关键字virtual;
  • 构造函数不能定义为虚函数;赋值运算符的重载最好不要定义为虚函数;
  • 最好不要在构造和析构函数中调用虚函数,因为此时对象是不完整的,可能会发生未定义行为;
  • 最好将析构函数定义为虚函数。

下面重点分析为什么要将基类的析构函数定义为虚函数?

假设定义一个类B的对象,并且让类A的指针指向它:

class A
{
public:
	~A()
	{
		cout << "~A()" << endl;
	}
};
class B : public A
{
public:
	~B()
	{
		cout << "~B()" << endl;
	}
};
int main()
{
	A* p = new B;
	delete p;
	system("pause");
	return 0;
}
当释放该空间时,发现我们申请的是B的对象,释放的确是A。


而当我们给基类的析构函数加上关键字virtual时,结果就正确了。

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

    这是因为操作系统对析构函数做了些处理,使得析构函数的函数名都一样,在加上虚函数,就构成了多态,这样调用析构函数时就与对象有关,就能正确的释放空间了。

二、多态的原理

(一) 虚表

虚函数表是通过一块连续的内存来保存虚函数的地址。

class A
{
public:
	virtual void func1()
	{}
	virtual void func2()
	{}
};
int main()
{
	A a;
	system("pause");
	return 0;
}

    每个对象都有一个虚表,这个虚表中保存着该对象在调用虚函数时应该调用的虚函数的地址,这样,就让调用时只与对象有关,实现了多态。

(二) 探索C++对象模型

1 探索单继承对象模型

针对下面的继承代码:

class A
{
public:
	A(int num = 1)
		:_a(num)
	{}
	virtual void func1()
	{
		cout << "A::func1()" << endl;
	}
	virtual void func2()
	{
		cout << "A::func2()" << endl;
	}
protected:
	int _a;
};
class B : public A
{
public:
	B(int num = 2)
		:_b(num)
	{}
	virtual void func1()
	{
		cout << "B::func1()" << endl;
	}
	virtual void func3()
	{
		cout << "B::func3()" << endl;
	}
protected:
	int _b;
};

尝试打印上述类的虚表:

typedef void (*VFUNC)();
void PrintVTable(int* VTable)
{
	printf("虚表地址:%p\n", VTable);
	int i = 0;
	for (; VTable[i] != NULL; i++)
	{
		printf("%d:%p->", i, VTable[i]);
		VFUNC f = (VFUNC)VTable[i];
		f();
		cout << endl;
	}
}
int main()
{
	A a;
	PrintVTable((int*)*((int*)(&a)));
	B b;
	PrintVTable((int*)*((int*)(&b)));
	system("pause");
	return 0;
}


2 探索多继承的内存布局

class A
{
public:
	A(int num = 1)
		:_a(num)
	{}
	virtual void func1()
	{
		cout << "A::func1()" << endl;
	}
	virtual void func2()
	{
		cout << "A::func2()" << endl;
	}
protected:
	int _a;
};
class B
{
public:
	B(int num = 2)
		:_b(num)
	{}
	virtual void func3()
	{
		cout << "B::func3()" << endl;
	}
protected:
	int _b;
};
class C : public A, public B
{
public:
	C(int num = 3)
		:_c(num)
	{}
	virtual void func1()
	{
		cout << "C::func1()" << endl;
	}
	virtual void func3()
	{
		cout << "C::func3()" << endl;
	}
	virtual void func4()
	{
		cout << "C::func4()" << endl;
	}
protected:
	int _c;
};

通过打印虚函数表和查看内存来搞清楚它在内存中是如何组织存放的:

typedef void (*VFUNC)();
void PrintVTable(int* VTable)
{
	printf("虚表地址:%p\n", VTable);
	int i = 0;
	for (; VTable[i] != NULL; i++)
	{
		printf("%d:%p->", i, VTable[i]);
		VFUNC f = (VFUNC)VTable[i];
		f();
		cout << endl;
	}
}
int main()
{
	C c;
	PrintVTable((int*)*((int*)(&c)));
	PrintVTable((int*)*((int*)(&c)+sizeof(A)/4));

	system("pause");
	return 0;
}

总结:

  • 在多重继承中,有多个虚表,继承了几个类,就有几个虚表;
  • 当C类继承其他类重写虚函数时,直接将对应虚表中的该函数都替换成C中的重写的虚函数;
  • 当C类有新的虚函数要添加到虚表中时,将该虚函数添加到第一个虚表中;

3 探索菱形继承的内存对象模型(无虚继承)

class AA
{
public:
	AA(int a = 1)
		:_a(a)
	{}
	virtual void f1()
	{
		cout << "AA::f1()" << endl;
	}
protected:
	int _a;
};
class BB : public AA
{
public:
	BB(int b = 2)
		:_b(b)
	{}
	virtual void f1()
	{
		cout << "BB::f1()" << endl;
	}
	virtual void f2()
	{
		cout << "BB::f2()" << endl;
	}
protected:
	int _b;
};
class CC : public AA
{
public:
	CC(int c = 3)
		:_c(c)
	{}
	virtual void f1()
	{
		cout << "CC::f1()" << endl;
	}
	virtual void f3()
	{
		cout << "CC::f3()" << endl;
	}
protected:
	int _c;
};
class DD : public BB, public CC
{
public:
	DD(int d = 4)
		:_d(d)
	{}
	virtual void f1()
	{
		cout << "DD::f1()" << endl;
	}
	virtual void f4()
	{
		cout << "DD::f4()" << endl;
	}
protected:
	int _d;
};

同样通过打印虚函数表和观察内存来进行研究:

typedef void (*VFUNC)();
void PrintVTable(int* VTable)
{
	printf("虚表地址:%p\n", VTable);
	int i = 0;
	for (; VTable[i] != NULL; i++)
	{
		printf("%d:%p->", i, VTable[i]);
		VFUNC f = (VFUNC)VTable[i];
		f();
		cout << endl;
	}
}
int main()
{
	DD d;
	int a = sizeof(AA);
	PrintVTable((int*)*((int*)(&d)));
	PrintVTable((int*)*((int*)(&d)+sizeof(AA)/4+1));

	system("pause");
	return 0;
}

总结:从图中,我们可以看出,菱形继承(在没有虚拟继承时)更多重继承类似,当重写虚函数时,将其继承的所有父类的虚函数表的该函数都替换成重写的函数;当增加虚函数时,将该函数增加进继承的父类的第一个虚函数表。

当然,也可以看到,在菱形继承中,有2个_a,这就造成了数据冗余和二义性的问题。解决的办法是菱形继承的虚拟继承。

4 菱形继承的虚拟继承的内存对象模型

这种情况比较复杂,分好几种情况:

  • DD类不对它继承的基类的虚函数进行重写
class AA
{
public:
	AA(int a = 1)
		:_a(a)
	{}
	virtual void f1()
	{
		cout << "AA::f1()" << endl;
	}
protected:
	int _a;
};
class BB : virtual public AA
{
public:
	BB(int b = 2)
		:_b(b)
	{}
	virtual void f1()
	{
		cout << "BB::f1()" << endl;
	}
	virtual void f2()
	{
		cout << "BB::f2()" << endl;
	}
protected:
	int _b;
};
class CC : virtual public AA
{
public:
	CC(int c = 3)
		:_c(c)
	{}
	virtual void f1()
	{
		cout << "CC::f1()" << endl;
	}
	virtual void f3()
	{
		cout << "CC::f3()" << endl;
	}
protected:
	int _c;
};
class DD : public BB, public CC
{
public:
	DD(int d = 4)
		:_d(d)
	{}
protected:
	int _d;
};

在这种情况下,程序编译不过,DD类必须对f1()函数进行重写。


  • 对f1()函数进行重写,但BB类和CC类中并不增添新的虚函数,DD类中增添新的虚函数f4()
class AA
{
public:
	AA(int a = 1)
		:_a(a)
	{}
	virtual void f1()
	{
		cout << "AA::f1()" << endl;
	}
protected:
	int _a;
};
class BB : virtual public AA
{
public:
	BB(int b = 2)
		:_b(b)
	{}
protected:
	int _b;
};
class CC : virtual public AA
{
public:
	CC(int c = 3)
		:_c(c)
	{}
protected:
	int _c;
};
class DD : public BB, public CC
{
public:
	DD(int d = 4)
		:_d(d)
	{}
	virtual void f1()
	{
		cout << "DD::f1()" << endl;
	}
	virtual void f4()
	{
		cout << "DD::f4()" << endl;
	}
protected:
	int _d;
};

打印出类DD的虚函数,再结合它的内存:


        可以看到,公共有的函数存放在最下面,和变量_a在一起,一样属于类BB、CC、DD公有的部分;还可以看出,当类BB、CC都不添加新的虚函数时,没有各自的虚表,当DD类增加新的虚函数时,将该虚函数放到以一个继承的类的虚函数表中。此时的内存对象模型如下:


  • BB、CC类都有自己的虚函数时
class AA
{
public:
	AA(int a = 1)
		:_a(a)
	{}
	virtual void f1()
	{
		cout << "AA::f1()" << endl;
	}
protected:
	int _a;
};
class BB : virtual public AA
{
public:
	BB(int b = 2)
		:_b(b)
	{}
	virtual void f1()
	{
		cout << "BB::f1()" << endl;
	}
	virtual void f2()
	{
		cout << "BB::f2()" << endl;
	}
protected:
	int _b;
};
class CC : virtual public AA
{
public:
	CC(int c = 3)
		:_c(c)
	{}
	virtual void f1()
	{
		cout << "CC::f1()" << endl;
	}
	virtual void f3()
	{
		cout << "CC::f3()" << endl;
	}
protected:
	int _c;
};
class DD : public BB, public CC
{
public:
	DD(int d = 4)
		:_d(d)
	{}
	virtual void f1()
	{
		cout << "DD::f1()" << endl;
	}
	virtual void f4()
	{
		cout << "DD::f4()" << endl;
	}
protected:
	int _d;
};
打印出类DD的虚函数,再结合它的内存:


        从图中可以看出,该类中有三个虚函数表,第一个是BB类的虚基表,BB类自己的虚函数存放在其中,DD类新定义的虚函数也存放在这个虚基表中;第二个是CC类的虚基表,存放的是CC类的虚函数;第三个是在最下面的虚函数表,存放的是AA、BB、CC、DD共同有的虚函数。

        它的内存对象模型是:


三、多态概念

多态就是多种形态,C++中的多态分为静态多态和静态多态。

  • 静态多态就是重载,因为是在编译期决议确定,所以称为静态多态。
  • 动态多态就是通过继承重写基类虚函数实现的多态,因为是在运行时决议确定,所以叫做动态多态。

通过查看汇编代码也可以理解它们的区别:

class AA
{
public:
	AA(int a = 1)
		:_a(a)
	{}
	virtual void f1()
	{
		cout << "AA::f1()" << endl;
	}
protected:
	int _a;
};
class BB : virtual public AA
{
public:
	BB(int b = 2)
		:_b(b)
	{}
	virtual void f1()
	{
		cout << "BB::f1()" << endl;
	}
	void f2()
	{}
	void f2(int a)
	{}
protected:
	int _b;
};
void func(BB& b)
{
	b.f1();
	b.f2(2);
	b.f2();
}
int main()
{
	BB b;
	func(b);
	system("pause");
	return 0;
}


猜你喜欢

转载自blog.csdn.net/weixin_40417029/article/details/80227907