【C++】:面向对象三大特性之多态

面向对象三大特性之多态

    • 1. 什么是多态?
    • 2.多态的两个构成条件
      • 2.1 虚函数
      • 2.2 虚函数的重写和协变
      • 2.3 一些不规范的重写行为
      • 2.4 析构函数的重写问题
      • 2.5 为什么建议将析构函数定义为虚函数?
      • 2.6 接口继承和实现继承
    • 3.重载、重写(覆盖)、隐藏(重定义)
    • 4.抽象类
    • 5.C++11新语法(override和final)
    • 6.多态的原理
      • 6.1 虚函数表
      • 6.2 多态原理
      • 6.3 动态绑定与静态绑定
    • 7.单继承和多继承的虚函数表
    • 8. 多态部分常见题目总结

1. 什么是多态?

多态指的是完成某一个具体的行为,使用不同的对象完成时会出现不同的状态例如:买火车票,学生买票是半价,成人买票是全价,军人买票优先。
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了PersonPerson对象买票全价,Student对象买票半价。

多态的分类:

  • 静态多态:指的是对象声明时的类型,在编译时确定(函数重载、泛型编程)
  • 动态多态:程序运行过程中才动态地确定操作所针对的对象(虚函数)

2.多态的两个构成条件

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

2.1 虚函数

虚函数:在类的普通成员函数前加上virtual关键字。

class Date
{
public:
	virtual void Print()
	{
		cout << _year << "-" << _month << "-" << _date <<endl;
	}
};

2.2 虚函数的重写和协变

虚函数的重写:派生类中有一个跟基类的完全相同虚函数,我们就称子类的虚函数重写了基类的虚函数。完全相同是指:函数名、参数、返回值都相同。另外虚函数的重写也叫作虚函数的覆盖。但是虚函数重写存在一个例外:协变

协变:重写的虚函数的返回值可以不同,但是必须分别是基类指针派生类指针或者基类引用派生类引用

class A{};
class B :public A{};
class Person
{
public:
	virtual A* Func()
	{
		return new A;
	}
};
class Stu :public Person
{
	virtual B* Func()
	{
		return new B;
	}
};

2.3 一些不规范的重写行为

class Person
{
public:
	virtual void Func()
	{
		cout << "全票" << endl;
	}
};
class Stu : public Person
{
public:
	void Func()
	{
		cout << "半票" << endl;
	}
};

在派生类中重写的成员函数可以不加virtual关键字,也是构成重写的,因为继承后基类的虚函数被继承下来 了在派生类依旧保持虚函数属性,我们只是重写了它。但是这样做是不规范的写法。

2.4 析构函数的重写问题

基类中的析构函数如果是虚函数,那么派生类的析构函数就重写了基类的析构函数。虽然他们的函数名不相同,看起来违背了重写的规则,其实并没有,因为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

2.5 为什么建议将析构函数定义为虚函数?

基类指针可以指向派生类的对象(多态性),如果删除该指针delete []p;就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。如果析构函数不被声明成虚函数,则编译器实施静态绑定(也就是说和类型有关),在删除基类指针时,会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。所以,将析构函数声明为虚函数是十分必要的。

2.6 接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以直接使用该函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,从而达成多态。总结:C++中实现继承时为了实现继承,而接口继承时为了实现多态。

3.重载、重写(覆盖)、隐藏(重定义)

  • 重载:在同一作用域、函数名相同、参数列表不同
  • 重写(覆盖):在基类和派生类两个作用域、两个函数必须是虚函数、函数名/参数/返回值必须相同(协变除外)
  • 重定义(隐藏):在基类和派生类两个不同的作用域、函数名相同

4.抽象类

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

使用抽象类实现一个简单的多态:

class Car
{
public:
	virtual void Func() = 0;//纯虚函数
};
class BMW:public Car
{
public:
	virtual void Func()//重写纯虚函数
	{
		cout << "舒适" << endl;
	}
};
class DaBen:public Car
{
public:
	virtual void Func()//重写纯虚函数
	{
		cout << "好看" << endl;
	}
};
void Test()
{
	Car* pb = new BMW;
	pb->Func();

	Car* pd = new DaBen();
	pd->Func();
}

5.C++11新语法(override和final)

override:override 修饰派生类虚函数强制完成重写。我们实际使用中可以使用纯虚函数+ override的方式来强制重写虚函数,因为虚函数的意义就是实现多态,如果 没有重写,虚函数就没有意义。
final:final修饰的类不能被继承final修饰的虚函数不能被重写。和Java中final功能类似。

6.多态的原理

6.1 虚函数表

class A
{
public:
	virtual void Func(){}
private:
	int a;
};
int main()
{
	cout << sizeof(A) << endl;//8,在32位平台下指针4个字节
	return 0;
}

上边的类中除了a成员,还多一个_vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表 function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中, 虚函数表也简称虚表

class A
{
public:
	virtual void Func1()
	{
		cout << "A::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "A::Func2()" << endl;
	}
	void Func3()
	{
		cout << "A::Func3" << endl;
	}
private:
	int a;
};
class B :public A
{
public:
	virtual void Func1()
	{
		cout << "B::Func1()" << endl;
	}
private:
	int b;
};
int main()
{
	A a;
	B b;
	return 0;
}

在下边的监视窗口中可以看到:

【C++】:面向对象三大特性之多态_第1张图片

  • 基类A对象a中有一个虚函数表指针,指向的这个虚函数表类似于一个函数指针数组,里边存放的是类A中的两个虚函数地址
  • 派生类B对象b中也有一个虚函数表指针,该对象由两部分构成,一部分是父类继承下来的成员,虚表指针存在从父类继承下来那部分。该虚函数表指针指向的两个虚函数,一个已经变成了派生类B重写的Func1函数
  • 基类A对象和派生类B对象的虚表是不一样的,Func1完成了重写,所以b的虚表中存的是重写的B::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  • 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
  • 基类A中的Func2是虚函数,继承在派生类B中,也在虚函数表中,而A::Func3不是虚函数,继承下来不在虚函数表中。

派生类虚函数表的生成过程:

  • 先将基类的虚表拷贝到派生类虚表中
  • 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
  • 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

虚函数存在哪里?虚函数表存在哪里?

虚函数和普通函数一样存放在代码段中,指向虚函数的指针存在虚函数表中,在每一个对象中,都存这一个虚表指针,而不是虚表。虚表存在哪里我们可以自行写代码验证,我在vs下验证发现虚表应该是存放在全局域(验证方法:可以将虚表的地址打印出来,和各个区域的指针比较可以发现大致在哪个区域)

6.2 多态原理

class Person
{
public:
	virtual void Ticket() = 0;//纯虚函数
};
class Parent :public Person
{
public:
	virtual void Ticket()
	{
		cout << "全票" << endl;
	}
};
class Stu :public Person
{
public:
	virtual void Ticket()
	{
		cout << "半票" << endl;
	}
};
void Func(Person& p)
{
	p.Ticket();
}
int main()
{
	Parent p;
	Stu s;
	Func(p);
	Func(s);
	return 0;
}
  • 当p是指向p(parent)对象时,p.Ticketp(parent)的虚表中找到虚函数是 Parent::Ticket
  • p是指向s对象时,p.Ticket在s的虚表中找到虚函数是Stu::Ticket
  • 这样就实现出了不同对象去完成同一行为时,展现出不同的形态

【C++】:面向对象三大特性之多态_第2张图片

  • 满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。
  • 多态的两个构成条件:一个是虚函数覆盖,一个是对象的指针或引用调用虚函数

6.3 动态绑定与静态绑定

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

7.单继承和多继承的虚函数表

  • 单继承的虚函数表
class A
{
public:
	virtual void Func1()
	{
		cout << "A::Func1" << endl;
	}
	virtual void Func2()
	{
		cout << "A::Func2" << endl;
	}
private:
	int a;
};
class B :public A
{
public:
	virtual void Func1()
	{
		cout << "B::Func1" << endl;
	}
private:
	int b;
};

在监视窗口很容易看出单继承的虚函数表:
【C++】:面向对象三大特性之多态_第3张图片

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2" << endl;
	}
private:
	int b;
};
class Child :public Base
{
public:
	virtual void Func1()
	{
		cout << "Child::Func1" << endl;
	}
	virtual void Func3()
	{
		cout << "Child::Func3" << endl;
	}
	virtual void Func4()
	{
		cout << "Child::Func4" << endl;
	}
private:
	int c;
};

但是vs的监视窗口只可以看到重写父类的的虚函数和继承于子类的虚函数,不能看到自己的虚函数。我们可以将各个虚函数的地址打印出来观察:

typedef void(*VFPTR)();
void PrintTable(VFPTR table[])
{
	cout << "虚表指针:" << table << endl;
	for (int i = 0; table[i] != nullptr; i++)
	{
		cout << "第" << i << "个虚函数地址:" << table[i] << "->";
		VFPTR f = table[i];
		f();
		cout << endl;
	}
}
int main()
{
	Base b;
	Child c;
	//拿出虚表指针赋值给pb
	VFPTR* pb = (VFPTR*)(*(int*)&b);
	PrintTable(pb);
	VFPTR* pc = (VFPTR*)(*(int*)&c);
	PrintTable(pc);
	return 0;
}

【C++】:面向对象三大特性之多态_第4张图片

在上图中可以很好的看出单继承中基类和派生类的虚函数表。

  • 多继承中的虚函数表
class B1 
{ 
public:    
	virtual void func1() { cout << "B1::func1" << endl; }    
	virtual void func2() { cout << "B1::func2" << endl; } 
private:    
	int b1; 
};

class B2 
{ 
public:    
	virtual void func1() { cout << "B2::func1" << endl; }    
	virtual void func2() { cout << "B2::func2" << endl; } 
private:    
	int b2; 
};

class C : public B1, public B2 { 
public:    
	virtual void func1() { cout << "C::func1" << endl; }    
	virtual void func3() { cout << "C::func3" << endl; } 
private:    
	int c; 
};
typedef void(*VFPTR)();
void PrintTable(VFPTR table[])
{
	cout << "虚表指针:" << table << endl;
	for (int i = 0; table[i] != nullptr; i++)
	{
		cout << "第" << i << "个虚函数地址:" << table[i] << "->";
		VFPTR f = table[i];
		f();
		cout << endl;
	}
}
int main()
{
	B1 b1;
	B2 b2;
	C c;
	//派生类存在两个虚表(继承于两个基类),所以存在两个虚表指针8+8+4=20
	cout << sizeof(C) << endl;

	VFPTR* v1 = (VFPTR*)(*(int*)&c);    
	PrintTable(v1);

	VFPTR* v2 = (VFPTR*)(*(int*)((char*)&c + sizeof(B1)));    
	PrintTable(v2);
	return 0;
}

虚表的打印结果:
【C++】:面向对象三大特性之多态_第5张图片

  • 子类C中的func1重写两个基类B1和B2中的func1
  • 派生类中的虚函数func3放在第一个虚函数表中,并且是放在最后的。

上述代码的对象模型:
【C++】:面向对象三大特性之多态_第6张图片
注:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。

8. 多态部分常见题目总结

  • 什么是多态?

答:多态就是使用不同的对象来完成同一种行为而体现出来的不同的形态

  • 重载、重写(覆盖)、重定义(隐藏)区别?

重载:在同一作用域、函数名相同、参数列表不同
重写(覆盖):在基类和派生类两个作用域、两个函数必须是虚函数、函数名/参数/返回值必须相同(协变除外)
重定义(隐藏):在基类和派生类两个不同的作用域、函数名相同即可构成隐藏

  • inline函数可以是虚函数吗?

答:内联函数在调用的时候直接展开,它是不存在地址的,所以不能放到虚函数表中,不能是虚函数。

  • 纯虚函数的作用是什么?

答:为了完成接口继承,强制要求子类对纯虚函数进行重写,为了复用是父类的声明而不是实现

  • 静态成员可以是虚函数吗?

答:静态成员如果是虚函数,要调用它就必须在虚函数表中查找,而虚函数表指针示在对象中的,而静态成员是没有this指针的,也就是说它找不到虚表指针,所以静态函数不能是虚函数

  • 构造函数可以是虚函数吗?

如果构造函数是虚函数,需要查找虚函数表调用,但是虚函数表示在构造时才创建的,所以它不能是虚函数。

  • 析构函数可以是虚函数吗?

析构函数建议是虚函数,这样子类重写父类的析构函数,构成多态。如果不构成多态,使用父类的对象接收子类的对象时,在析构时只会析构父类的对象,而不去调用子类的析构,如果子类的对象存在资源管理,就会导致资源泄露的问题。

  • 对象访问普通函数快还是虚函数快?

答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找存在调用开销,所以访问普通函数较快。

  • 虚函数表在什么阶段生成,存在哪里?

答:虚函数表是在编译阶段就生成的,一般情况下存在常量区的。

你可能感兴趣的:(C++)