C++多态(概念,多态的定义及实现,抽象类,override和final,多态的原理,单继承和多继承的虚函数表)

1. 多态的概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态

比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。

2. 多态的定义及实现

2.1多态定义的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价

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

  1. 调用函数的对象必须是指针或者引用。
  2. 被调用的函数必须是虚函数,且完成了虚函数的重写。

 什么是虚函数?

虚函数:就是在类成员函数的前面加virtual关键字

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票全价" << endl;
	}
};

什么是虚函数重写?

虚函数重写:派生类中有一个和基类完全相同的虚函数,我们就称子类的虚函数重写了基类的虚函数,完全相同是指:函数名,参数,返回值都相同。另外虚函数的重写也叫做虚函数的覆盖

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票全价" << endl;
	}
};
class Student :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票半价" << endl;
	}
};

void Func(Person &people)
{
	people.BuyTicket();
}

int main()
{
	Person p;
	Func(p);

	Student s;
	Func(s);

	return 0;
}

虚函数重写的例外:协变

虚函数重写有一个例外:重写的虚函数的返回值可以不同,但是必须分别是基类指针和派生类指针或者基类引用和派生类引用。

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;
	}
};

 不规范的重写行为

在派生类中重写的成员函数可以不加virtual关键字,也是构成重写的,因为继承后基类的虚函数被继承下来了在派生类一九保持虚函数特性,我们只是重写了它。但是这是非常不规范的,我们平时不要这样使用。

class Person 
{ 
public: 
	virtual void BuyTicket() 
	{ 
		cout << "买票-全价" << endl;
	}
};

class Student : public Person
{ 
public: 
	void BuyTicket() 
	{ 
		cout << "买票-半价" << endl; 
	} 
};

 析构函数的重写问题

基类中的析构函数如果是虚函数,那么派生类的析构函数就重写了基类的析构函数。这里他们的函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名字统一处理成destructor,这也说明的基类的析构函数最好写成虚函数。

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

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

// 只有派生类Student的析构函数重写了Person的析构函数,
//下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{   
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1; 
	delete p2;
	return 0; 
}

 接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义为虚函数。

重载,覆盖(重写),隐藏(重定义)的对比

3. 抽象类

在虚函数的后面写上 =0;则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。储蓄函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Car
{
public:
	virtual void Drive() = 0;
};

class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};
class BMW :public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};
int main()
{
	Car* pBenz = new Benz;
	pBenz->Drive();

	Car* pBMW = new BMW;
	pBMW->Drive();
}

4.C++11中的 override 和 final

C++11提供override和final来修饰虚函数。

实际中我们建议多使用纯虚函数+override的方式来强制重写虚函数,因为虚函数的意义就是实现多态,如果没有重写,虚函数就没有意义。

 

5. 多态的原理

5.1虚函数表

class Base
{
public:
	virtual void Fun1()
	{
		cout << "Fun1()" << endl;
	}
private:
	int _b = 1;
};

int main()
{
	cout << sizeof(Base) << endl;
	//输出结果为8
}

 上面程序运行的结构为8,除了_b成员,还多一个_vfptr放在对象的前面,对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中至少都有一个虚函数表的指针,因为虚函数的指针要被放到虚函数表中,虚函数表也简称虚表。

//1.增加了一个派生类Derive去继承Base
//2.Derive中重写Func1
//3.Base再增加一个虚函数Fun2和一个普通函数Fun3
class Base
{
public:
	virtual void Fun1()
	{
		cout << "Base::Fun1()" << endl;
	}
	virtual void Fun2()
	{
		cout << "Base::Fun2()" << endl;
	}
	void Fun3()
	{
		cout << "Base::Fun3()" << endl;
	}
private:
	int _b = 1;
};
class Derive :public Base
{
public:
	virtual void Fun1()
	{
		cout << "Derive::Fun1()" << endl;
	}
private:
	int _d = 2;
};

int main()
{
	Base b;
	Derive d;
	//cout << sizeof(Base) << endl;  //8
	//cout << sizeof(Derive) << endl; //12

}

我们发现:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚标指针也就是存在部分的另一部分是自己的成员。
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Fun1完成了重写,所以d的虚表中存的是重写的Derive::Fun1,所以虚函数的重写也叫覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Fun2继承下来后是虚函数,所以放进了虚表,Fun3也继承下来了,但不是虚函数,所以不会放进虚表
  4. 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
  5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚标中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  6. 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚标指针,虚表存在于数据段。

5.2多态的原理

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票全价" << endl;
	}
};
class Student :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票半价" << endl;
	}
};

void Func(Person &people)
{
	people.BuyTicket();
}

int main()
{
	Person p;
	Func(p);

	Student s;
	Func(s);

	return 0;
}

 

  1.  观察上图红色我们可以发现,people指向p时,people.BuyTicket在p的虚表中找到的虚函数是Person::BuyTicket.
  2. 观察上图蓝色我们可以发现,people指向s时,people.BuyTicket在s的虚表中找到的虚函数是Student::BuyTicket.
  3. 这样就实现除了不同对象去完成同一行为时,展示出不同形态
  4. 要达到多态,有两个条件,一个是虚函数的覆盖,一个是对象的指针会引用调用虚函数。
  5. 满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象中去找的。不满足多态的函数调用是编译时确认好的。

 5.3 动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态动态,比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也成为动态多态

6. 单继承和多继承关系中的虚函数表

6.1单继承中的虚函数表

class Base
{
public:
	virtual void Fun1()
	{
		cout << "Base::Fun1()" << endl;
	}
	virtual void Fun2()
	{
		cout << "Base::Fun2()" << endl;
	}
private:
	int a;
};
class Derive :public Base
{
public:
	virtual void Fun1()
	{
		cout << "Derive::Fun1()" << endl;
	}
	virtual void Fun3()
	{
		cout << "Derive::Fun3()" << endl;
	}
	virtual void Fun4()
	{
		cout << "Derive::Fun4()" << endl;
	}
private:
	int b;
};

int main()
{
	Base b;
	Derive d;

}

观察监视窗口我们发现看不见Fun3和Fun4.这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为他是一个小bug。

 我们可以打印出来虚表中的函数

class Base
{
public:
	virtual void Fun1()
	{
		cout << "Base::Fun1()" << endl;
	}
	virtual void Fun2()
	{
		cout << "Base::Fun2()" << endl;
	}
private:
	int a;
};
class Derive :public Base
{
public:
	virtual void Fun1()
	{
		cout << "Derive::Fun1()" << endl;
	}
	virtual void Fun3()
	{
		cout << "Derive::Fun3()" << endl;
	}
	virtual void Fun4()
	{
		cout << "Derive::Fun4()" << 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;
}

运行结果如下:

6.2多继承中的虚函数表

class Base1
{
public:
	virtual void Fun1()
	{
		cout << "Base1::Fun1()" << endl;
	}
	virtual void Fun2()
	{
		cout << "Base1::Fun2()" << endl;
	}
private:
	int b1;
};
class Base2
{
public:
	virtual void Fun1()
	{
		cout << "Base2::Fun1()" << endl;
	}
	virtual void Fun2()
	{
		cout << "Base2::Fun2()" << endl;
	}
private:
	int b2;
};
class Derive :public Base1,public Base2
{
public:
	virtual void Fun1()
	{
		cout << "Derive::Fun1()" << endl;
	}
	virtual void Fun3()
	{
		cout << "Derive::Fun3()" << endl;
	}
private:
	int d;
};
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()
{
	Derive d;
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);
	VFPTR* vTabled2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
	PrintVTable(vTabled2); 
	return 0;
}

运行结果如下:

可以发现:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。

猜你喜欢

转载自blog.csdn.net/Damn_Yang/article/details/86478094