多态的原理

前言:以下的内容均是在VS2019的环境中,32位平台下的

目录

1.多态的实现条件

虚函数重写的两个例外

一个题加深理解

总结

[重载 重写 重定义区别](#重载 重写 重定义区别)

2.多态的实现原理

单继承

多继承

动态多态和静态多态

多态的好问题


1.多态的实现条件

虚函数:被virtual修饰的**成员函数,**和虚拟继承没有一点关系

a.虚函数的重写(三同:函数名,返回值,参数类型)

b.父类的指针或引用调用虚函数

虚函数的重写(覆盖):子类继承了父类的虚函数的接口,对其函数主体进行重写,父类和子类具有相同的虚函数,相同是指三同:函数名,返回值,参数类型,函数主体不同。

cpp 复制代码
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
这样使用*/
/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
void Func(Person& p)
{ p.BuyTicket(); }
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}

构成多态:不同的对象去调用同一函数,呈现出不同的状态。

不构成多态:指针或引用类型是什么就调用谁的

虚函数重写的两个例外

协变

子类重写父类虚函数时可以返回值类型不同,但是子类 虚函数必须返回子类对象的指针或引用 (不一定是本身子类对象,可以是其他子类对象),父类 虚函数必须返回父类对象的指针或引用(不一定是本身父类对象,可以是其他父类对象)

cpp 复制代码
class A{};
class B : public A {};
class Person {
public:
virtual Person* f() {return new Person;}
};
class Student : public Person {
public:
virtual Student* f() {return new Student;}
};
cpp 复制代码
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关键字,都与父类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

cpp 复制代码
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;
}

一个题加深理解

cpp 复制代码
class A
  {
  public:
      virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
  virtual void test(){ func();}
  };

class B : public A
  {
  public:
      void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
  };
 
int main(int argc ,char* argv[])
  {
      B*p = new B;
      p->test();
      return 0;
  }

答案是什么?

解释:B

改了一下

cpp 复制代码
class A
  {
  public:
      virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}

  };

class B : public A
  {
  public:
      void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
      virtual void test(){ func();}
  };
 
int main(int argc ,char* argv[])
  {
      B*p = new B;
      p->test();
      return 0;
  }

又选什么?

解释:D

总结

多态就是硬套条件,符合多态就按多态走,不符号多态就按类型走

重载 重写 重定义区别

2.多态的实现原理

单继承

cpp 复制代码
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};

int main()
{
	Base B;
	cout << sizeof(Base) << endl;
	return 0;
}

运行发现Base的大小是8字节,为什么呢?

通过调试我们发现,Base对象除了有_b成员变量,还存在一个_vfptr变量, 这个变量是虚函数指针 简称虚表指针。 虚函数表 简称虚表:存着虚函数地址的函数指针数组,一个含有虚函数的类至少有一个虚表。

cpp 复制代码
class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

多态实现原理:父类的指针或者引用指向父类就通过虚表指针找到父类的虚表在调用相应的虚函数,指向子类通过虚表指针找到子类的虚表在调用相应的虚函数。

反思一下为啥满足多态的条件有一个是父类的指针或引用,不能是父类对象。

虚函数表的问题

1.VS系列编译器虚表最后面放的是nullptr,g++编译器没有

2.虚表是在编译阶段产生的

3.虚表指针是在构造函数初始化列表初始化

4.虚表存的是虚函数的地址,不是虚函数,虚函数和普通函数一样在代码段中,虚表也存在带码中

5.总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

打印虚表

虚表怎么打印呢?

思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr

1.先取b的地址,强转成一个int*的指针

2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针

3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。

4.虚表指针传递给PrintVTable进行打印虚表

5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。

cpp 复制代码
class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};
class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
};

typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :%p\n", i, vTable[i]);
	}
	cout << endl;
}
int main()
{
	Base b;
	Derive d;
	VFPTR * vTableb = (VFPTR*)(*(int*)&b);
	PrintVTable(vTableb);
	VFPTR* vTabled = (VFPTR*)(*(int*)&d);
	PrintVTable(vTabled);
	return 0;
}

多继承

菱形继承不做解释,太复杂了,实际也不会用。

动态多态和静态多态

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,如:函数重载

  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

多态的好问题

  1. inline函数可以是虚函数吗?

可以,内联函数是就地展开的,认为没有地址,也就不能放进虚表里,但是编译器会忽略inline属性,内联函数只是给编译器的一个建议,编译器可以不采取,成员函数要么是内联要么是虚函数,两者只能有一个

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

不可以,静态成员函数是所有类共享的,通过类型::成员函数来访问的,没有this指针,也就不能访问虚表,不能放进虚表里面

  1. 构造函数可以是虚函数吗?

不可以,虚表指针就是在构造函数的初始化列表初始化的,拷贝构造也不行,但是赋值重载是可以的,最好不要是

4.析构函数可以是虚函数吗?

可以,并且最好把基类的析构函数定义成虚函数

5.对象访问普通函数快还是虚函数更快?

a.如果有虚函数(不构成多态),普通调用,那么和普通函数一样快

b.如果有虚函数和普通函数,通过指针或引用调用,不管构不构成多态,调用虚函数时都会到虚表里面去找,那么普通函数就快了

相关推荐
szuzhan.gy1 小时前
DS查找—二叉树平衡因子
数据结构·c++·算法
火云洞红孩儿1 小时前
基于AI IDE 打造快速化的游戏LUA脚本的生成系统
c++·人工智能·inscode·游戏引擎·lua·游戏开发·脚本系统
FeboReigns2 小时前
C++简明教程(4)(Hello World)
c语言·c++
FeboReigns2 小时前
C++简明教程(10)(初识类)
c语言·开发语言·c++
zh路西法3 小时前
【C++决策和状态管理】从状态模式,有限状态机,行为树到决策树(二):从FSM开始的2D游戏角色操控底层源码编写
c++·游戏·unity·设计模式·状态模式
.Vcoistnt3 小时前
Codeforces Round 994 (Div. 2)(A-D)
数据结构·c++·算法·贪心算法·动态规划
小k_不小3 小时前
C++面试八股文:指针与引用的区别
c++·面试
沐泽Mu3 小时前
嵌入式学习-QT-Day07
c++·qt·学习·命令模式
ALISHENGYA3 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(实战训练三)
数据结构·c++·算法·图论
GOATLong4 小时前
c++智能指针
开发语言·c++