多态的原理

前言:以下的内容均是在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.如果有虚函数和普通函数,通过指针或引用调用,不管构不构成多态,调用虚函数时都会到虚表里面去找,那么普通函数就快了

相关推荐
奋斗的小花生1 小时前
c++ 多态性
开发语言·c++
闲晨1 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
UestcXiye2 小时前
《TCP/IP网络编程》学习笔记 | Chapter 3:地址族与数据序列
c++·计算机网络·ip·tcp
霁月风4 小时前
设计模式——适配器模式
c++·适配器模式
jrrz08284 小时前
LeetCode 热题100(七)【链表】(1)
数据结构·c++·算法·leetcode·链表
咖啡里的茶i4 小时前
Vehicle友元Date多态Sedan和Truck
c++
海绵波波1074 小时前
Webserver(4.9)本地套接字的通信
c++
@小博的博客4 小时前
C++初阶学习第十弹——深入讲解vector的迭代器失效
数据结构·c++·学习
爱吃喵的鲤鱼5 小时前
linux进程的状态之环境变量
linux·运维·服务器·开发语言·c++
7年老菜鸡6 小时前
策略模式(C++)三分钟读懂
c++·qt·策略模式