欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

初夏小谈:C++之全面剖析多态(一)

程序员文章站 2024-03-15 17:36:54
...

一、什么是多态?

多态顾名思义就是多种状态,就是不同对象去做同一件任务,会产生多种状态。就是不同继承的类对象去调用同一函数,而执行结果却不相同。

例如:每当国庆节等节日时,在都去旅游这件事上,不同的人群会有不同的门票价格。比如去华山旅游,成人全价,学生半价,未成年人免费。一样。不同的人去购票会产生不同的结果。

再比如:更贴近学生本身,同一专业的学生,都在学习相同的知识。可是最后,每个人学的情况却是不尽相同。从而产生的多种状态就是多态。

二、多态实现的条件

1.条件:

(1).条件一. 使用某个函数想要产生多态。那么这个函数在基类中必须是虚函数。即在该函数前加virtual关键字。

2).条件二. 在子类中对该函数进行重写时,必须保证函数原型一样。即(返回值类型,函数名,参数列表都相同)。如果子类不再被继承,或者在后续继承的子类中不再期望使用该函数产生多态时则可以不加关键字virtual。

(3). 在调用该函数时必须是基类的引用/指针。

2.实例代码:

#include<iostream>
using namespace std;

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

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

void TestFunc(Base& b)
{
	b.Test1();
}
int main()
{
	Base b;
	children c;

	TestFunc(b);
	TestFunc(c);

	system("pause");
	return 0;
}

 

3. 运行结果:

初夏小谈:C++之全面剖析多态(一)

三、多态中两者特殊的情况

在实现多态时也不是绝对按照上述条件,有两种例外我将一一介绍:

1.协变

(1). 我们把在基类中的虚函数返回值类是基类的指针/引用,把在子类中重写该函数后返回值的类型设置为子类类型的指针/引用。时可以构成多态,把这种情况叫做协变。

(2)实例代码;

#include<iostream>
using namespace std;

class Base
{
public:
	//返回该类型的指针
	virtual Base* Test1()
	{
		cout << "Base::Test1()" << endl;
		return nullptr;
	}

	//返回该类型的引用
	virtual Base& Test2()
	{
		cout << "Base::Test1()" << endl;
		return *this;
	}
};

class children : public Base
{
public:
	//协变---返回值的类型不同
	virtual children* Test1()
	{
		cout << "children::Test1()" << endl;
		return nullptr;
	}

	virtual children& Test2()
	{
		cout << "children::Test1()" << endl;
		return *this;
	}
};

void TestFunc(Base& b)
{
	b.Test1();
	b.Test2();
}
int main()
{
	Base b;
	children c;

	TestFunc(b);
	TestFunc(c);

	system("pause");
	return 0;
}

(3). 运行结果:

初夏小谈:C++之全面剖析多态(一)

2.析构函数构成多态,给基类的析构函数前加上virtual关键字

(1)实例代码:

#include<iostream>
using namespace std;

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

class children : public Base
{
public:
	~children()
	{
		cout << "children::~children()" << endl;
	}
};

void TestFunc(Base& b)
{
	b.~Base();
}

int main()
{
	Base* Bptr = new children;
	delete Bptr;

	system("pause");
	return 0;
}

(2). 运行结果:

初夏小谈:C++之全面剖析多态(一)

(3)重要说明:

  • 在实例代码中有以下说明:
  •         1.在销毁对象前为什么会先调用子类的构造函数,再调基类的构造函数?在前面一篇继承中已经说过了,这是因为在处理子类资源时可能会调用基类的成员,所以先调用子类析构函数,在子类的所有资源清理工作完成后再调用基类析构函数。
  •         2.在创建这个对象时,在堆上申请空间时的类型是子类类型,而Bptr指针是Base类型。这样做是因为,基类中虚函数一定是公有,而子类不一定不限制。在后面说明。
  •         3.一旦对象在堆上创建,那么子类的析构函数必须给虚函数。即函数名前加上virtual关键字。这样才能在销毁对象时,调用子类的析构函数进行释放资源。否则在子类中在堆上申请的资源就会造成内存泄露。  当将基类析构函数不设置为虚函数时,不调用派生类的析构函数来释放资源。
  •         4.变量有两种类型:
  •                          静态类型:--->变量声明的类型如Bptr的Base*,在编译期间就可以确定
  •                          动态类型:--->实际指向的类型 如Bptr指向children类型,代码运行起来确定
  •            如果多态条件没有满足,使用对象的静态类型来调用虚函数 ----基类的虚函数。
  •            如果多态条件满足,就使用对象的动态类型来调用虚函数, ----先去调用当前对象所指向的类型的函数。

四、成员访问限定符对多态的影响

(1)当将重写的函数给成私有,可不可以?

  • 在基类中如果给成私有,就不可以,但是,如果将子类中的该虚函数给成私有。却可以。为什么?这时因为在编译期间发现是基类的调用,而基类的被重写函数是公有的。而在程序运行期间,进行子类调用时,发现是被重写的函数,则就会调自己的。
  • 多态中,成员访问权限可以不一样,基类必须公有,子类随便

五、重写(覆盖),重载,重定义(隐藏)的区别

(1)重写:

两个函数必须分别在基类和子类中,并且这两个函数的返回值,函数名,参数列表必须完全相同(协变例外),并且两个函数都是虚函数。(如果子类还要被继承)。

(2)重载:

一个大的前提:就是必须都在同一作用域下

函数名相同,但是参数列表不同,体现在参数个数可以不同,参数类型可以不同,参数的顺序可以不同。但是返回值不同不会构成函数重载。

(3)重定义:

两个函数分别在子类和基类中。函数名相同。列表可以不相同。在基类和子类的两个同名函数不构成重写就是重定义。

六、C++11中的final和override两个关键字

(1).override关键字(C++11)用来说明对该函数进行重写,防止重写时函数名写错等情况。

          只能放在派生类的虚函数后面

(2).final : 1.修饰类,说明该类不能被继承

                    2.修饰虚函数---说明在此类后面的子类中不能将这个函数再被重写

七、编译器不得不给出构造函数的四个场景

(1). 在A类中有缺省的构造函数    在B类中没有构造函数但是含有A类对象

(2). 继承:

                在A类中有缺省的构造函数    之后B类继承于A类,没有显示定义构造函数,

                当创建B类对象时,要使得B类对象创建完整就必须调用A类的构造函数来初始化A类的那一部分。

                初始化A类对象构造函数在B类的初始化列表处调用

(3). 虚拟继承:

                A类没有显示定义,但是B类虚拟继承于A,之后在构造B类的对象时,会创建两个参数一个是前四个字节是对象空间首地址,一个是1表示是虚拟继承.

                通过前四个字节所找到映射的虚基表,通过偏移量来找到基类对象的位置。

                对象模型:基类对象在下,子类对象在上。

(4). 带有虚函数的类:

               创建好对象,前四个字节存地址.

               每多一个虚函数,    前四个字节的地址所映射的表中就增加一个地址占四个字节--->虚表指针.

               基类:基类的虚函数按照其在类中的声明次序存放在虚表中。

八、抽象类

(1)抽象类就是在虚函数的后面跟上 = 0,就表明该函数是纯虚函数。包含纯虚函数的类就是抽象类。抽象类不能被实例化。如果继承了抽象类,并且没有在子类中没有重写纯虚函数,则子类也不能实例化。只有重写纯虚函数,才可被实例化。

(2)实例代码:

//抽象类
#include<iostream>
using namespace std;

class Abstract
{
public:
	virtual void AbstrS() = 0;

	void Print()
	{
		cout << "Abstract::AbstrS()" << endl;
	}
};

class children : public Abstract
{
public:
	virtual void AbstrS()
	{
		cout << "children::AbstrS()" << endl;
	}
};

int main()
{
	//抽象类不能被实例化
	//Abstract a;

	children c;
	c.AbstrS();
	c.Print();
	system("pause");
	return 0;
}

(3)运行结果:

初夏小谈:C++之全面剖析多态(一)

九、含有虚函数的类中的虚函数表

(1)虚函数表就是在实例化的对象的首地址的前四个字节所映射的虚表。按照声明顺序,存该类的所有虚函数的地址。

(2)示例代码:

#include<iostream>
using namespace std;

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

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

typedef void(*FuncPtr)();

void Print(Base& b)
{
	cout << sizeof(b) << endl;
	FuncPtr* ptr = (FuncPtr*)(*(int*)&b);
	while (*ptr)
	{
		(*ptr++)();
	}
}

int main()
{
	Base b;
	Print(b);

	system("pause");
	return 0;
}

(3)运行结果:

初夏小谈:C++之全面剖析多态(一)

十、单继承中的虚函数表

(1)在单继承的情况,事先将基类的所有虚函数先拷贝一份。然后这个虚表中先存储了拷贝的的基类的虚函数的地址,再存储它的所有虚函数的地址。

(2)为何是拷贝一份,而不是同一份?

         这是因为在对象的前四个字节即指向虚表的地址和基类对象的前四个字节内容不一样。

(3)如果子类对基类虚函数进行重写:

         先拷一份基类的 ---> 子类对那个基类虚函数重写,就用自己重写的虚函数替换虚表中相同偏移位置的位置。--->否则就继续用拷贝的基类的虚函数的地址。

         对于派生类自己新增加的虚函数跟在派生类虚表的最后(跟在继承的基类的虚表的后面)。

(4)示例代码:

#include<iostream>
using namespace std;

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

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


typedef void(*FuncPtr)();


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

	virtual void Test1()
	{
		cout << "children::Test1()" << endl;
	}

	virtual void Test5()
	{
		cout << "children::Test5()" << endl;
	}
private:
	int _b = 10;
};

void PrintVirtualFunc(Base& b)
{
	FuncPtr* ptr = (FuncPtr*)(*(int*)&b);
	while (*ptr)
	{
		cout << "ptr = " << ptr << endl;
		(*ptr++)();
	}
}

int main()
{
	cout << sizeof(children) << endl;

	Base b;
	b._b = 3;
	//FuncPtr* ptr = (FuncPtr*)(*(int*)&b);
	////第一个虚函数的地址
	//(*ptr)();
	//ptr++;
	////第二个虚函数的地址
	//(*ptr)();
	cout << "Base:" << endl;
	PrintVirtualFunc(b);

	children c;
	cout << "children:" << endl;
	PrintVirtualFunc(c);
	system("pause");
	return 0;
}

(5)运行结果验证上述结论:

初夏小谈:C++之全面剖析多态(一)

十一、多继承中的虚函数表

子类也会重新拷贝所有继承的基类的虚函数。

(1)在虚函数的多继承中。在子类实例化的对象中会为每一个基类含有所有虚函数去分配一个虚表。虚表的顺序按照继承的顺序排列。并且每多一个含有虚函数的类,就会使该子类大小多出四个字节。用来指向所继承的类所所有虚函数的地址即虚表。

(2)在多继承中,如果子类有自己的虚函数,就会将其地址放在第一张虚表中。

(3)如果子类对哪个基类的虚函数进行重写。就将对应那个类的虚表的对应修改的函数地址改变为重写后的函数的地址。

(4)实例代码:

#include<iostream>
using namespace std;

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

	virtual void TestFunc2()
	{
		cout << "Base1::TestFunc2()" << endl;
	}
public:
	int b1;
};

class Base2
{
public:
	virtual void TestFunc3()
	{
		cout << "Base2::TestFunc3()" << endl;
	}
	virtual void TestFunc4()
	{
		cout << "Base2::TestFunc4()" << endl;
	}
public:
	int b2;
};

class children : public Base1, public Base2
{
	virtual void TestFunc2()
	{
		cout << "children::TestFunc2()" << endl;
	}

	virtual void TestFunc3()
	{
		cout << "children::TestFunc3()" << endl;
	}

	virtual void TestFunc5()
	{
		cout << "children::TestFunc5()" << endl;
	}
public:
	int c;
};

typedef void(*FuncPtr)();

void PrintBase1(Base1& c)
{
	FuncPtr* FuncptrS = (FuncPtr*)(*(int*)&c);
	while (*FuncptrS)
	{
		cout << "FuncptrS = " << FuncptrS << " ";
		(*FuncptrS++)();

	}
}

void PrintBase2(Base2& c)
{
	FuncPtr* FuncptrS = (FuncPtr*)(*(int*)&c);
	while (*FuncptrS)
	{
		cout << "FuncptrS = " << FuncptrS << " ";
		(*FuncptrS++)();

	}
}

void Printchildren(children& c)
{
	FuncPtr* FuncptrS = (FuncPtr*)(*(int*)&c);
	while (*FuncptrS)
	{
		cout << "FuncptrS = " << FuncptrS << " ";
		(*FuncptrS++)();

	}
}
int main()
{
	cout << sizeof(children) << endl;//20
	children c;
	children c1;
	c.b1 = 1;
	c.b2 = 2;
	c.c = 3;

	cout << "c" << ":" << endl;
	cout << "PrintBase1(c) : " << endl;
	PrintBase1(c);
	cout << "PrintBase2(c) : " << endl;
	PrintBase2(c);
	//多继承时c中的虚拟函数放在第一张虚表的后面
	cout << "Printchildren(c1) : " << endl;
	Printchildren(c);
	cout << endl;

	cout << "c1" << ":" << endl;
	cout << "PrintBase1(c1) : " << endl;
	PrintBase1(c1);
	cout << "PrintBase2(c1) : " << endl;
	PrintBase2(c1);
	//多继承时c中的虚拟函数放在第一张虚表的后面
	cout << "Printchildren(c1) : " << endl;
	Printchildren(c1);

	system("pause");
	return 0;
}

(5)运行结果:

初夏小谈:C++之全面剖析多态(一)

(6)注意;

  • 注意的是,从结果中可以看到:虽然子类会拷贝基类的虚函数,但是创建多个子类对象却只用同一份。因为都是子类类型的不同对象,虚表中对应虚函数的地址一样。
  • 两张虚表不是挨在一起,因为调用子类的对象的前四个字节去找虚表的。并且上面结果也可以看到。
  • 第一张虚表的最后一个虚函数TestFunc5()的地址是FuncptrS = 003F9BBC,而第二张虚表对应的虚函数TestFunc4()的地址是:FuncptrS = 003F9BCC。
  • 这就导致了为什么调用子类对象的虚表打印的只是第一张虚表中的虚函数。

                                                                                                                                                                   珍&源码

上一篇: 指定区间进行链表翻转

下一篇: