C++——多态概念及多态原理、虚表指针及虚函数表

多态的概念

多态按字面意思就是存在多种形态。当类之间存在层次结构,并且类之间是通过集成关联时,就会用到多态。C++多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。不同类型的对象调用同一个方法是达到的效果是不同的。

多态的应用场景

这里引用一篇文章里关于LOL的例子

class Champion
{
protected:
	int _currentHP;
	int _maxHP;
	int _damagePoint;
};

class 寒冰 : public Champion
{
public:
	void attack(盖伦* ptr)
	{
		// 攻击盖伦的行为
	}
};

class 盖伦 : public Champion
{
public:
	void attack(寒冰* ptr)
	{
		// 攻击寒冰的行为
	}
};

有一个父类是Champion,包含了所有英雄都应具有的属性如生命值、攻击力等。
有一个寒冰类和盖伦类,当寒冰要攻击盖伦,就要给attack传入盖伦的指针,但是如果有别的英雄类,如剑圣,就要给寒冰类里重载一个attack函数,参数是剑圣类的指针。这样就会导致代码十分冗余,且维护难度极大。
此时就可以用到多态。

class Champion
{
protected:
	int _currentHP;
	int _maxHP;
	int _damagePoint;
public:
	virtual void attack(Champion* ptr) = 0;
};

class 寒冰 : public Champion
{
public:
	void attack(Champion* ptr) override
	{
		// 攻击Champion的行为
	}
};

class 盖伦 : public Champion
{
public:
	void attack(Champion* ptr) override
	{
		// 攻击Champion的行为
	}
};

这时,寒冰需要攻击盖伦,就只要定义一个Champion的指针指向盖伦,然后把指针传入寒冰的attack函数,攻击其他英雄也是同样的步骤,这就体现了多态的价值。面对不同对象有不同的处理方式。

多态的条件

  1. 父类提供了虚函数接口,提供给子类重写
  2. 子类重写父类的虚函数,保证函数名、参数类型、返回值相同。
  3. 必须由父类的指针或者引用来调用该虚函数,不能是对象调用。

为什么必须是父类指针或引用来调用虚函数?
函数接口是父类提供的,并要求子类重写。传类型的指针过去,如果接收的参数是一个子类,那父类的那一部分数据就传不过去了,而如果传父类,会统一类型转换成父类传过去。
接下来介绍各个概念,第三点不能对象调用后面有提及。

虚函数

virtual修饰的类非静态成员函数称为虚函数。只有类的非静态成员函数可以加virtual,普通函数不能加virtual。

虚函数和虚继承共用同一个关键字。虚函数的virtual是为了实现多态,继承的virtual是为了解决菱形继承的数据二义性。

虚函数在类中声明后,类外定义不能带virtual,提示类声明外部的说明符无效。

class Entity
{
public:
	virtual void func1();
};

void Entity::func1()
{

}

虚函数重写

父类提供虚函数,子类将虚函数继承,保证函数名,参数,返回类型相同,并重新实现函数的内容,这种情况叫做重写或覆盖。重写是一种特殊的隐藏。

class Entity
{
protected:
	std::string _name;
public:
	Entity()
	{
		_name = "Entity";
	}
	virtual void func1()
	{
		cout << "Entity:func1" << endl;
	}

};

class Player : public Entity
{
public:
	Player()
	{
		_name = "Player";
	}
	virtual void func1()
	{
		cout << "Player:func1" << endl;
	}
};

void print(Entity* ptr)// 必须是父类的指针
{
	ptr->func1();
}

int main()
{
	Entity e1;
	Player p1;

	print(&e1);
	print(&p1);
	return 0;
}

Player继承了Entity的func1接口,保证函数名、参数、返回值相同,这就构成了重写。且子类的继承自父类的虚函数可以不带virtual,因为接口继承也继承了接口的声明,因此在子类中func1仍然保持虚函数属性。

重写的例外:协变

协变是重写的特殊情况,虚函数重写允许返回值不同,且返回值类型只能是父子关系的类的指针或引用。

接口继承和实现继承

接口继承:虚函数继承是继承接口,需要重写实现,子类继承了父类接口的声明。
实现继承:普通函数的继承是实现继承。子类继承了父类函数的实现,可以直接使用。
纯虚函数很好地体现了接口继承。

纯虚函数

如果想要在父类中定义虚函数,并强制所有子类都重新实现这个函数以便达到不同的效果,就可以用到纯虚函数。

class Virtual
{
protected:
	virtual void interface() = 0;
	virtual void interface1() = 0
	{
	}
};

上面的这种类型的函数就是纯虚函数,且要求继承这个类的子类必须重新实现这个函数。父类可以实现这个纯虚函数的内容,但是没有意义。

抽象类

包含纯虚函数的类称为抽象类,即上面的Virtual类。
抽象类的特点:不能在栈区或者堆区上创建对象。
但是可以定义抽象类指针,指向一个子类对象。

重载、重写、隐藏

重载:两个函数在同一个作用域。函数名、参数类型不同。
重写:两个函数所在的类直接构成子类和父类的关系。两个函数必须都是虚函数。函数名、返回类型、参数相同。
隐藏:两个函数所在的类直接构成子类和父类的关系。函数名相同。

final与override
final关键词表示该虚函数不能够再被子类重写,是最终版本。
override修饰子类中的虚函数,可以检查该函数是否严格重写了继承自父类的虚函数接口。

class Entity
{
protected:
	std::string _name;
public:
	Entity()
	{
		_name = "Entity";
	}
	virtual void func1()
	{
		cout << "Entity:func1" << endl;
	}
	virtual void func2() final
	{
		cout << "不能被重写了" << endl;
	}
};

class Player : public Entity
{
public:
	Player()
	{
		_name = "Player";
	}
	virtual void func1() override
	{
		cout << "Player:func1" << endl;
	}
};

构造函数和析构函数存在多态吗?
在构造函数和析构函数中调用虚函数,不构成多态。因为编译时即可确定,调用的函数是自己的类或基类中定义的函数,不会等到运行时才决定调用自己的还是派生类的函数。

虚拟析构函数 virtual destructor

虚拟析构函数可以实现父类指针析构子类对象。
场景:

class Entity
{
protected:
	std::string _name;
public:
	Entity()
	{
		_name = "Entity";
	}
	~Entity()
	{
		cout << "~Entity()" << endl;
	}
	void func1()
	{
		cout << "Entity:func1" << endl;
	}

};

class Player : public Entity
{
public:
	Player()
	{
		_name = "Player";
	}
	~Player()
	{
		cout << "~Player()" << endl;
	}
};

int main()
{
	Entity *e1 = new Entity();
	Player* p1 = new Player();
	delete e1;
	cout << endl;
	delete p1;
	
	return 0;
}
结果:
~Entity()

~Player()
~Entity()

对于普通的应用场景显然是没问题的。但是如果涉及多态的调用,需要用父类的指针指向新开辟的对象。

int main()
{
	Entity *e1 = new Entity();
	Player* p1 = new Player();
	
	delete e1;
	cout << endl;
	
	Entity* ptr = p1;
	// 这里用基类的指针指向子类对象
	delete ptr;
	
	return 0;
}
结果:
~Entity()

~Entity()

这样就出现了问题,父类指针指向的子类对象调用delete的时候,没有回收子类的资源,造成资源泄漏。我们期望delete是多态调用,因此要把析构函数用virtual修饰为虚函数。

如果父类的析构函数为虚函数,子类的析构函数就和父类的析构函数构成重写。父子类的析构函数名定义的时候显然是不同的,但是编译器将析构函数名统一处理成了destructor,所以能构成重写。

这也是继承中父子类的析构函数构成隐藏的原因!

多态的原理

class Entity
{
protected:
	std::string _name;
public:
	Entity()
	{
		_name = "Entity";
	}
	~Entity()
	{
		cout << "~Entity()" << endl;
	}
	virtual void func1()
	{
		cout << "virtual Entity:func1" << endl;
	}
	virtual void func2()
	{
		cout << "virtual Entity:func2" << endl;
	}
	void func3()
	{
		cout << "Entity:func3" << endl;
	}

};

class Player : public Entity
{
public:
	Player()
	{
		_name = "Player";
	}
	~Player()
	{
		cout << "~Player()" << endl;
	}
	virtual void func1()
	{
		cout << "virtual Player:func1" << endl;
	}

};

在这个场景中,Entity类中有三个函数,其中两个虚函数。Player类继承了Player类三个函数,其中两个虚函数,并且重写了fun1这个虚函数。
C++——多态概念及多态原理、虚表指针及虚函数表_第1张图片
可以看到e1和p1对象中不止有类内成员name,还有一个指针__vfptr,virtual function pointer,这是指向虚函数表的指针,指向一个函数指针数组,指针存放在对象内。虚函数表里保存了类各自的虚函数的地址。

看到由于func1被重写,所以e1和p1中的函数指针数组的0号位置各不相同,但是由于func2没有被重写,所以1号下标的内容相同。

可以得知,虚函数的重写就是基于这个原理。
虚函数重写:语法层概念,子类继承父类的虚函数接口,重写实现。
虚函数覆盖:原理层概念,子类中有一个父类对象,这个对象的虚函数表指针是原父类的深拷贝,并且可以修改。

整体逻辑是:父类中存在虚函数,子类继承了父类就把父类中指向虚函数表的指针也继承了下来,这个指针是一个深拷贝,指向的数组里存放着原来的虚函数和子类重写的虚函数的地址。

C++——多态概念及多态原理、虚表指针及虚函数表_第2张图片
父类指针或引用指向对象调用函数:就是通过解引用,找到这个对象的虚函数表指针,去虚函数里表找对应的函数。

多态的实现是运行时在指向的对象的虚函数表中查到要调用的函数的地址,然后调用。而普通的函数是在编译的时候就把函数名存在了符号表,调用函数的时候直接通过函数名映射找到对应函数的地址。

对象为什么不行?
子类对象可以赋值给父类类型的对象,同样可以切片。切片就是拿子类中属于父类的内容给父类对象拷贝构造,对于内置类型的拷贝构造是浅拷贝。
如果浅拷贝了虚函数表,Base存的是Derived的虚函数表,如果一个指针指向Base类,调用func1是调用derived中的func1,如果执行Derived类也是这个func1。这就乱套了,调用的时候就很混乱。而且既然不同类型的对象执行的函数都一样了,那还谈什么多态。

虚函数表

先理清一下关系:
对象中有一个指针,称为虚函数表指针,指向的是虚函数表。虚函数表是一个函数指针数组,数组的内容是虚函数的地址。虚函数和虚函数表都是存在于数据段。
虚表的特性是:一个类型一个虚表,所有该类对象都指向这个虚函数表,且生命周期是随进程的。虚表存放的区域是只读数据段、即常量区。

虚函数表指针(vptr):
虚函数表指针是一个指针,指向存储在对象中的虚函数表。在对象被创建时,会自动初始化vptr指针,将其指向正确的虚函数表。在调用虚函数时,程序会根据vptr指向的虚函数表找到相应的虚函数。

虚函数表(vtable):
虚函数表是一个数组,其中每个元素都是一个指向虚函数的指针。每个类都有自己的虚函数表,其中存储了该类中所有的虚函数。
虚函数表通常在编译时被创建,并存储在可执行文件的数据段中。在程序运行时,虚函数表被加载到内存中,每个类的对象都会有一个指向对应虚函数表的vptr指针。

虚函数(virtual function):
虚函数的地址存储在虚函数表中,当调用虚函数时,程序会根据对象的vptr指针找到相应的虚函数表,再根据函数在虚函数表中的位置调用对应的虚函数实现。

多继承的虚函数表

//基类1
class Base1
{
public:
	virtual void func1() { cout << "Base1::func1()" << endl; }
	virtual void func2() { cout << "Base1::func2()" << endl; }
private:
	int _b1;
};
//基类2
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;
};

现在有三个类,定义三个对象,分别打印对象中的虚函数表的内容。
如何打印?

typedef void(*VF_PTR)();

void printVirtualFuncAddr(VF_PTR* ptr)
{
	printf("虚表地址:%p\n", ptr);
	for (int i = 0; ptr[i] != nullptr; i++)
	{
		printf("ptr[%d]:%p-->", i, ptr[i]); 
		ptr[i](); 
	}
	printf("\n");
}

定义一个函数指针,typedef void* (VF_PTR); ,定义一个derive类对象,将对象的地址转换成int*类型以取前四个字节,就取到了虚函数表指针,再转换成函数指针类型的指针,就可以找到整个函数指针数组。

int main()
{
	Base1 b1;
	Base2 b2;
	Derive d1;
	printVirtualFuncAddr((VF_PTR*)(*(int*)&b1));
	printVirtualFuncAddr((VF_PTR*)(*(int*)&b2));

	printVirtualFuncAddr((VF_PTR*)(*(int*)&d1)); //打印d1的第一个虚表地址及其内容
	printVirtualFuncAddr((VF_PTR*)(*(int*)((char*)&d1 + sizeof(Base1)))); //打印d1的第二个虚表地址及其内容
	return 0;
}
虚表地址:00B8052C
ptr[0]:00B49924-->Base1::func1()
ptr[1]:00B49EBF-->Base1::func2()

虚表地址:00B80564
ptr[0]:00B4A3BF-->Base2::func1()
ptr[1]:00B493F7-->Base2::func2()

虚表地址:00B8059C
ptr[0]:00B49B86-->Derive::func1()
ptr[1]:00B49EBF-->Base1::func2()
ptr[2]:00B49B4F-->Derive::func3()

虚表地址:00B805B0
ptr[0]:00B4A14E-->Derive::func1()
ptr[1]:00B493F7-->Base2::func2()

可以得知,子类新创建的虚函数的地址会存在子类中继承的第一个父类的虚函数表中。

此外,还能结合菱形继承和虚拟继承搞出一堆乱七八糟的关系。设计的时候要尽量避免菱形继承。

你可能感兴趣的:(C/C++,c++,开发语言)