多态的相关知识学习

多态的基础

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

最典型的行为就是买票
函数:BuyTicket()
普通人
学生
军人
同样是买票,不同的类型对象去调用这个函数,会产生不一样的行为,形态结构是不一样的;

class Person {
public:
	Person(const char* name)
	:_name(name)
	{
		
	}
虚函数
 virtual void BuyTicket() { cout << "买票-全价" << endl; }

protect
string _name;
}class Student : public Person {
public:
✳️虚函数➕函数名/参数/返回值都相同----➡️构成重写/覆盖
所以Student和Soldier的两个虚函数,重写了父类的虚函数
为什么是重写/覆盖后面再说
 virtual void BuyTicket() { cout << _name << "买票-半价" << endl; }
 
虚函数➕函数名/参数/返回值都相同----➡️构成重写/覆盖
 class Soldier : public Person {
public:
 virtual void BuyTicket() { cout << _name << "优先买票-全价" << endl; }

void Pay(Person* ptr)
{
	ptr->BuyTicket();
}
✳️首先这里有一个赋值兼容的转换,Pay()函数参数是一个父类的指针,父类的指针是可以指向父类对象也能指向子类对象
所以对我们而言,到底是谁在买票呢?我也不知道。
所以继承做了一个铺垫,父类的指针/引用可以指向子类的对象,天然的!
你传不同的对象就会有不同的行为! 
怎么让不同的用户去调用不同的函数?父类的可以传给我,子类的也可以传给我
int main() 
{
	int option = 0;
	cout << "=======================================" << endl;
	do 
	{
		cout << "请选择身份:";
		cout << "1、普通人 2、学生 3、军人" << endl;
		cin >> option;
		cout << "请输入名字:";
		string name;
		cin >> name;
		switch (option)
		{
		case 1:
		{
				   
				  Person p(name.c_str());
				  //Pay(new Person(name.c_str()));---➡️也可以用匿名对象来写
				  Pay(&p);
				  break;
		}
		case 2:
		{
				  Student s(name.c_str());
				  Pay(&s);
				  break;
		}
		case 3:
		{
				  Soldier s(name.c_str());
				  Pay(&s);
				  break;
		}
		default:
			cout << "输入错误,请重新输入" << endl;
			break;
		}
		cout << "=======================================" << endl;
	} while (option != -1);

	return 0;
}

✳️多态两个要求:
1.子类虚函数重写的是父类虚函数(重写也有要求)
2.重写的要求:首先要是虚函数➕函数名/函数参数/返回值都必须相同
(三同➕虚函数! )
3.必须是父类的指针或引用去调用虚函数!

❓如果传对象会怎么样?
对象虽然可以传,但是就违反了多态的条件!因为必须是父类的指针或引用去调用虚函数!
我们发现传对象去调用虚函数,发现我是学生你给我的是Person对应的,我是军人你也是Person对应的。那么此时调用的都是父类的!(我们要学了虚函数表才能理解这里)

❓当我们军人虚函数参数不与父类相同时会怎么样virtual void BuyTicket(int)
当我们军人的虚函数参数不与父类虚函数参数一致的时候,即使我们传的是父类指针或引用去调用虚函数,发现最后都是匹配父类的!
因为不符合虚函数的要求!没有符合三同的要求。函数参数也得相同。

要想达到不同对象调用同一个虚函数达到不同的效果,那就必须得符合多态的要求!满足两个大条件!

✳️协变-----➡️返回值有关
返回不同也有特例:虚函数重写对返回值要求有一个例外:协变!必须是父子关系!并且只能是指针和引用,不能是对象!

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;}
};
这也是能够,构成多态的!

✳️子类的虚函数没有写virtural,f()函数依旧是虚函数 ,因为先继承了父类的函数接口声明。
然后我重写父类虚函数实现。
这也是能够构成多态的!

class A{};
class B : public A {};
class Person {
public:
  void f() {return;}--------➡️没➕virtual也构成多态!
};
class Student : public Person {
public:
  void f() {return;}

✳️做做面试题:
题一:关于接口继承这个点的题目

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

选项:
 A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
 首先p->test()没必要是A*,B*就行了,因为我又没说要构成多态呀!我就是子类指针去调用test()test()虽然是虚函数没有被重写,没有构成多态条件,但是我也是子类指针去调用,也不符合多态条件。但是为什么能被调用呢?是因为它被继承下来了;
 所以这里用的是继承属性!所以可以调用。
 this指针调用test()是A*去访问的还是B*去访问的?虽然是继承下来了,但也是A* this去访问的!
 继承调用:是用父类的指针类型去调用的!(但是后面多态调用:指针还是指向的子类对象)
 既然是A* this去访问那肯定就会有赋值兼容的转换!
 赋值兼容转换:我调用成员函数,那我就是要把我对象的地址传给你this指针,这里对象是B*类型的P,我传给你父类;
 那么就会有切片,相当于this还是指向new 出来的对象!
 所以this调用了test()后,this再去调用func()函数,那么此时符不符合多态的条件呢?
 符合多态的两个条件!:第一个条件,父类的指针调用符合!第二个条件func()函数符合virtual➕三同的条件,即虚函数的重写!
 虽然子类没有加virtual但也是可以构成多态的!重写是一个接口继承,我继承以后,是把你的壳给继承下来了,你的壳是虚函数,所以我不加virtual也可以是虚函数,返回值、参数、函数名都继承下来了,我重写的是你的实现。
 那么func()调用的父类还是子类的呢?很简单this指针指向谁那就调用谁!
 所以this指针是指向B类型对象那就是调用B的func()函数了!
 最恶心的一步来了:val的值是最不好确定的!val的值是1!
 
 ✳️子类继承重写父类的虚函数
 	1.接口继承(什么是接口继承?是函数的声明!声明主要包括写什么?返回值、函数参数、函数名,所以不加virtual也行)---所以B中func不写virtual也是虚函数,符合多态条件。
 	所以缺省也是用的A::func()1,说了我子类是接口继承,相当于我用的接口是virtual void func(int val = 1)。我重写的只是你的{}花括号里面的方法实现
 	所以这里的val是12.重写的是函数的实现

❓那如果我不p->test()而是用B类型指针去调用func()呢?
结果是B->0;
你看符不符合多态的条件?不符合!
因为没用父类的指针或引用去调用重写的虚函数,就是普通调用。只有多态才会出发接口继承!接口继承就是会去继承你的函数参数的缺省值。普通调用你父类和我一点关系都没。所以结果是B->0;
若改成A
p2 = new B;则在 p2->func();
那么就是多态调用:结果就是B->1

✳️析构函数:虚函数另一个重写的例外:

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 p;
 Student s;
 }

运行之后发现是:~Student
~Person()
~Person()
这里调用析构函数的顺序是对!因为栈是后进先出,s对象后定义所以后入栈,在析构时会被先析构。
那为什么会析构两次Peson呢?
因为我们前面说了,子类析构的时候,会先析构子类再析构父类。因为父类的一些成员会被先定义出来。所以子类的析构函数调用完了会再自动调用父类的析构函数,并且父类的析构函数我们不用去写!
因为多态的原因,他们的析构函数构成隐藏关系。因为他们的函数名会被处理成destruct

✳️常考的问题,好好记住这个场景
❓如果在父函数的析构函数前面➕virtual还是构成隐藏关系吗?
会从隐藏关系------>变成重写(覆盖)关系!
因为他们的函数名都是destructor!并且没有返回值和函数参数。那是不是符合三同,子类没有virtual是没关系的,因为我是先去继承你的属性,也就是会接口继承,然后才是去完成{}花括号内的重写功能。
在父类的析构函数加上没➕virtual就会出现不一样的点:
1.对于普通对象是没有影响的。
如:Person p;
Student s;
这样他会在return后正常析构不会说少析构谁
2.但若是这样:Person* ptr1 = new Person;
delete ptr1;
Person* ptr2 = new Student;
delete ptr2;
会发现打印的结果是:
~Person()
~Person()
我有一个父类的指针,有可能指向父类,也有可能指向子类。然后我用完了,不用了就去delete
delete ptr1—➡️会转换成ptr1->destructor() + operator delete(ptr1)
delete ptr2—➡️也是转换成ptr2r->destructor() + operator delete(ptr2)
如果不构成多态就是普通调用:我的类型是什么,是Person就去调用Person的析构函数。但是ptr2我希望你只去调Person的析构吗?我是子类对象Student,那你也应该去调子类的析构函数。但是调不到!
我们期望delete ptr调用析构函数是一个多态调用,不希望它是普通调用,希望它指针指向哪个对象就去调谁的 ,指向父类调父类,指向哪个子类就去调哪个子类的。
那么此时只要对父类的析构函数进行重写!便可以达成,前面没能如我们所愿就是因为只是普通调用。
所以➕了virtual之后就能正确的去析构了!

✳️如果你把基类给别人继承,那么你就应该将你的析构函数➕上virtual ,定义成虚函数!子类可以不加,加上更加规范。
这种问题很容易造成内存泄漏,真的是悄无声息的。

✳️再来看一道常考题:

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
 virtual void Func1()
 {
 cout << "Func1()" << endl;
 }
private:
 int _b = 1;
};
int main()
{
	cout<<< sizeof(Base) <<endl
}
答案是:8
里面的点是:有了虚函数以后,对象里面会有一个表,叫做虚表,简称虚函数表,虚函数是会放到虚函数里面的,对象里面会有一个虚表指针指向虚表,用来实现多态。
指针大小又要看平台了!4/8

✳️关键字“override”和“final”

class Car final-------➡️若在基类➕final则是不能被其他类继承的!
{
public:
 virtual void Drive() final {}----➡️➕final不让别人重写我的函数
};
class Benz :public Car
{
public:
 virtual void Drive() {cout << "Benz-舒适" << endl;}
}

final是用来干嘛呢,这两个关键字意义是相反的概念,比如说我这里有一个虚函数,我不想被别人重写,就是在函数的后面加一个final关键字。
我们以前也说过不能被继承的类的问题,我们说了把构造函数私有化,私有化后子类就不能用了,但是这不是一个特别好的方式,因为你不创建对象是不会报错的。C++11不这么间接,很直接的,你不创建对象它也能给你检查出来,就是用到了final!
final是可以写到基类的后面,别的类就不能被继承它。表达的意思是最终类。
所以final修饰虚函数不能被重写,修饰类不能被继承

✳️overrid
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

class Car{
public:
 virtual void Drive(){}
};
class Benz :public Car {
public:
 virtual void Drive() override {cout << "Benz-舒适" << endl;}
 ------➡️加在子类后面的,强制帮你检查是否完成重写
};

比如说你没有完成重写是会报错的。
overrid是写在子类中的,要求严格的检查是否完成重写,没有的话就报错!
比如它可以帮你很好的去做验证又没有完成重写,或者你怕别人改错了,最终想达到的目的就是重写。

final是放到父类,其实不能重写;而override是放到子类,目的是强制你重写。

✳️重载、覆盖/重写、隐藏的对比
重载:是两个函数,其必须得在同一作用域,然后函数名、函数参数不同(类型的顺序、参数个数、类型不同)

隐藏:两个函数分别在基类和派生类的作用域,然后函数名相同;其实还包含同名的成员变量也是一个构成隐藏关系

重写/覆盖:首先两个函数分别在基类和派生类的作用域,其实函数名、函数参数、返回值都必须相同(协变例外),最后两个函数都必须是虚函数(子类可不加virtual)
在这里插入图片描述

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

为什么叫抽象类呢?抽象的意思就是不具体吧,具象就是现实,抽象就是对应虚拟层面。我们虽然不懂艺术有个画派是抽象派。
在现实中一般没有具体对应的实体。比如说人不是具体的角色,具体的是学生、老师、保洁等,

class Car
{
public:
virtual void Drive() = 0;-----➡️没必要写实现,写了没有用
};
class Benz :public Car
{
public:
 virtual void Drive()
 {
 cout << "Benz-舒适" << endl;
 }
};

我们说了只要包含纯虚函数的类就是抽象类,派生类继承后也不能实例化。我Benz想要实例化出对象,就必须得重写基类的纯虚函数。
可不可以这样说,纯虚函数让基类不能实例化出对象,但也间接的强制派生类去重写纯虚函数!
override是检查有没有重写其放在子类。而这里的纯虚函数是放在基类的虚函数。

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

实现继承:我派生类继承的是基类的实现,我是可以用你里面的函数和成员变量的。
接口继承:虚函数的继承是接口继承,继承它虚函数属性,参数、返回值等等。我写一个虚函数,我的实现对子类就不重要了,虚函数若不重写,那么其就没有价值。

虚函数原理

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;
 }
  void Func3()
{
 	........;
}
private:
 int _d = 2;
};
int main()
{
 /*Base b;
 cout << sizeof(Base) << endl;*/
 
 Derive d;
 cout << sizeof(Base) << endl;

Base* p = &b;
p->Func1();
p->Func3();

p = &d;
p->Func1();
p->Func3();
 return 0; 
 }

我们运行起来看到Base b中的b中的变量有一个_vfptr指针,再展开这个指针看到里面有两个我们写的虚函数地址 。但我们说了监视窗口不是最真实的,是优化过的。看到Func3()并没有被放进去 ,说明表里面只会存虚函数 ,也不能说存虚函数,虚函数和普通函数编译后都是存在一个地方的,都是在代码段中。只是说不同的地方在哪儿呢?普通函数是只会进符号表。我们说过一个.cpp、.c编译完后会形成一个目标文件,目标文件里面有动静态库,它们不是完整的程序,但是他们提供了一些代码及其它里面定义的变量和函数,如果其他包了.h要用我,链接的时候可以来链一下。普通函数只是放入符号表方便去链接,因为他们都是编译时决议。
虚函数放进虚表里面去,是虚函数的地址放进虚表为后面运行时决议做准备。运行时决议待会儿说,心里面要有这么一个谱。
所以说我们可以这样理解,B对象呢 大概是这么个样子,有一个vfprt,还有一个自己的成员变量_b。
然后呢vfptr指向的是一张表,其实相当于数组,再准确点说是一个指针数组,里面存的是虚函数的指针。

我们再看看派生类又会是什么样子的,如果完成了重写,它会不会有虚表呢?它的虚表又会是什么样子的?
大家可以看上面代码,有一个Derive类,它继承了Base这个类。那么继承了Base以后我们来看看,我们来sizeof()一下,我们先看看有什么和多大。
如果正常情况下不看会是多大呢?8个字节两个int嘛,但是又了虚表指针呢是12.
我们打开了监视窗口,看看他和我们刚刚看的父类有什么区别。发现Func1()不一样,为什么Func1()不一样呢?因为派生类重写了Func1(),Func2()没有被重写。
那这样再来理解一个东西,虚函数重写—是语法层的概念,重写是我继承了你的接口。派生类对继承基类虚函数实现进行了重写
虚函数覆盖—原理层的概念—子类的虚表,拷贝父类的虚表进行了修改,覆盖了重写的那个虚函数。重写的虚函数我要覆盖掉。对基类的对象虚表存的是基类的虚函数,对于派生类的对象虚表存的是子类的修改后的虚函数和没有重写父类的虚函数。
在这里插入图片描述
东西铺垫好了,那我们来看看多态是怎么实现的。假设有一个父类的指针p:Base* p = &b,它指向父类对象,它就调用父类的虚函数;指向子类对象就调用子类的虚函数。
如何做到的?肯定是和上面说的虚表有关系。它到底要调用哪个函数说明,不是按类型去决定的,如果是按类型去定,这里的p = &d中p是父类指针应该是调用的父类的Func1()。那它这是怎么做的呢?它是去查表的!它到指定的对象去查表。其实他也不知道自己调的谁,它只知道到指定的对象去找。
我若指向的是父类的对象,那么我就是看到父类对象的虚表;我若指向的是子类对象,我通过切片,但我还是指向的是子类,所以我看到的是子类里面的虚表。它自己也分不清是调用的父类还是子类。
我看到的都是父类的对象,有可能本身就是父类,有可能是子类当中切片的父类对象。

所以这里多态的调用实现,依靠运行时,去指向对象的虚表中查调用的函数的地址,所以我指向谁就调用谁。指向谁就调用谁的前提是,父类的对象里面是父类的Fun1(),子类是重写的子类的Func1(),所以说我指向谁调用谁。
那么此时一定有一个非常重要的概念,叫做运行时决议!我运行的时候去查表。同样的代码我调用的Func3(),子类也是写了Func3()的,但他不是虚函数,也没有符合重写的条件。那请问调用的结果是什么?我都是用父类的指针去调用的,一个指向父类对象,一个指向子类对象。发现Func3()都是调用的父类的!子类虽然有Func3()但是调不到。
这里就有两个点:第一Func3()他不是虚函数,没有进入我们的虚表。第二,换个角度,从汇编的角度来看,我们有编译时决议,也有一个运行时决议。多态调用时运行时决议,普通函数调用时编译时决议。
说人话就是,运行时决议—是运行时确定调用函数的地址。怎么确定函数的地址?我指向谁,我就通过指令,我编译完后就是指令嘛,我就去对象的虚表里面去找虚函数的地址。我运行的时候才去找,找到了才去调。
普通调用时编译时决议–是编译的时确定调用函数地址。编译时怎么确定?不构成多态,所有的编译时确定我都是,我去看p是什么类型,和我p本身指向的对象没有关系,我不看我指向的对象。我自己指针是什么类型,我是Base类型,那我就去Base里面去找Func3(),在编译的时候就能确定Func3()的地址。、
所以有些地方提到编译时决议和运行时决议,其实就是确定某个函数的地址时段不同。一个是在运行的时候确定的,运行的时候如何确定的呢?就是去查虚函数表;另一个是编译时就确定了,我编译完了不就是有一个函数的地址嘛对不对,我符号表里面就有了。
来看看汇编:它们两种调用,编译的指令区别还是很大的
在这里插入图片描述
这是多态底层实现的原理。多态的条件是:要用父类的指针或引用。编译器也是去检查语法,若满足多态条件,它就去按多态调用的指令去执行,去虚表里面去找。若不满足多态条件,就按另一种指令去执行,直接编译的时候就去call地址。
这些都是实现多态的重要原理。
需要懂很多汇编吗?汇编常用的指令并不多,除了move就是call等还有cmp等没有很多指令。若想成为高手,还是要去学会看汇编的。 寄存器也不需要你看懂,寄存器没有他固定的价值,它就是来做临时的中转。

这里多态能实现,依赖的是什么?依赖的就是基础的虚表完成覆盖。父类对象的虚表里面,存的是父类的虚函数;子类对象里面存的是子类的虚函数。它在对象里面间接存的指针,它的指针指向这个表,这个表叫做虚函数表,简称虚表。
这和我们之前菱形虚拟继承那里不一样哈,那边虚表里面存的是偏移量。
这里的指针一般叫虚表指针,或准确一点叫虚函数表指针。

那现在问大家一个问题:指针能够实现,那么引用能不能实现?可以!因为引用和指针都一样,都能发生切片。你指针能够切片,我引用也能够切片。他和指针我们测试的结果都是一样的。它也是分编译时决议和运行时决议的。

我们现在回过头来反思为什么多态会有两个条件:第一,虚函数被完成重写(只有虚函数被满足完成重写了 。比如父类里面存的是父类的虚函数,子类里面会修改重写过的,这样才能达到指向谁调用谁的,指向谁就像谁的虚函数表里面找);第二个,只能是父类的指针或引用,因为只有指针或引用才能是子类对象的话通过切片从子类对象中切出父类。
子类赋值给父类对象也会切片,那么为什么实现不了多态?比如Base r2 = d;发现不是多态。为啥?
从两个层面看:第一从原理上,编译器很死板就是去检查你符不符合多态的条件,你符合多态的条件,那我就去找,那我就运行时去找。不符合的话我就编译时确定,那用什么来确定?就用类型来确定,我是什么类型的我就调用谁的。我是Base类型那我就Base里面的,这是从编译器实现角度来看。如果你是语言的设计者,你让指针和引用都支持了,你能不能让对象也支持?不可以!你想想如果对象也支持,那么大佬为什么会阻止呢?那我们再来看看三个切片不一样的点在哪儿?
第一种切片,对象切片: b = d
指针切片:Base* ptr = &d;
引用切片:Base& ref = d;
区别在于,对象切片我是把,我父类的那一部分弄出来,把值拷贝过去。那内置类型值拷贝,自定义类型调用它的拷贝构造函数。那我想问,那要不要拷虚表指针?不会拷贝!那为什么不拷呢?你想想,你拷了虚表指针就乱了呀!你待会儿这里的ptr指向子类调用Func1(),因为它指向子类,那就是调用子类的Func1()。那我如果在你Base b = d之后再有一个 Base* prt = &b,你如果普通对象拷贝了虚函数指针,那么我此时ptr指向的是Base对象,可是调用的就会是子类的对象虚表指针!那不就扯淡,彻底乱了吗?我指向子类调子类,我指向父类也调子类类。 所以现在我问你这里有一个父类指针,你分得清它调的是子类的还是父类的吗?那肯定分不清了呀。
所以对象切片的时候,子类只会拷贝成员给父类对象,不会拷贝虚表指针!因为拷贝了就混乱了,因为父类对象中到底是父类的虚表指针还是子类的虚表指针?都有可能。那么下面的调用的是父类虚函数还是子类虚函数,完全不能确定!
指针和引用就不会了。我虽然指向子类,但我是指向子类当中的一部分;我引用也是指向子类,但我引用的也是子类的一部分。不存在拷贝的问题!所以我指向父类调用的父类的,指向子类的调用的就是子类的。
所以单纯普通对象赋值是不能实现多态的!想实现也是不能的!不然会乱了套路。
在这里插入图片描述
✳️动态绑定和静态绑定
动态绑定也叫运行时决议。在C++里面,动态就是指的运行时,静态就是编译时。 普通调用都是静态绑定。
动态库和静态库,静态库就是链接的时候去链接;动态库就是程序运行起来后,才会去加载。

✳️什么是多态?
有的地方将多态分为两种,一种叫做
静态的多态:函数重载(使用的时候感觉好像是同一个函数,但是实际上不是,实际编译连接根据函数名修饰规则找到不同的函数)----编译时
动态多态:本节内容所讲的多态。-----运行时

✳️我们下面看一个难以控制的东西:

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;
 }
void Func3()
{
	.....
}
void Func4()
{
	//......
}
private:
 int _d = 2;
};

int main()
{
	Derive d;
}

这里有Func1()和Func2(),我们说了子类的虚表是哪儿来的?子类的虚表可以是把父类的虚表拷贝而来,然后把自己重写的虚函数覆盖。所以我们程序运行起来,打开监视窗口可以看到,子类的虚表里面有Func1()和Func2()。 Func2()是父类那边拷贝留下来的,Func1()会修改重写的。
那如果我子类单独增加一个虚函数Func4()呢?按理论而言虚函数都会进虚表。我们打开监视窗口,看子类的虚函数表,发现里面并没有Func4()!虚函数没有进虚表吗?
编译器规定了,虚函数都得进虚表。我们说了,子类的虚表是哪儿来的?子类虚表都是继承父类的,将父类的虚表拷贝下来,然后对重写的虚函数完成覆盖。Func4()没有进虚表?我们说了监视窗口是美化了,可能会骗人。
所我们打开内存来看看。我们先输入虚表指针的地址来查它里面的内容。
我们输入0071cc78,然后看到了对应的007114fb、007114f1、0071150a
我们猜了一下0071150a可能是我们增加的虚函数Func4(),但是我又不能确认是不是怎么办呢?
现在呢可以试试打印内存,然后呢拿着这个地址去调用一下。比如说想办法去内存的值,打印并调用,确认是否是Func4(),如果我们拿了地址去调用了,确认了是Func4(),那么我们就确认了是编译器优化了,它进入了虚表。但是取内存,打印调用很简单吗? 写程序怎么去玩呢?
我们想打印它虚表怎么去打?我们是有办法的。我们是可以取到定义对象里面虚表指针的地址,然后对地址解引用,就能去到里面的每一个值。你用指向成员函数的指针,你不能确认虚表的那个位置就是Func4()了。你想你成员函数的指针我们肯定是能调的,但是你不能确认虚表里面的那个就是Func4()。我们现在要确认的是Func4()在不在,在哪里。要有骚操作了。
你用指针能不能?你用对象的指针去打,不就是直接去调用这个Func4()函数吗?那你不是多态的话,还是编译时决议。这边想要构成多态构不成,因为没有虚函数的重写。也有人说再把这个Derive类被继承下去,重写一下这个,这是一个办法,但我们不用,我们就是想确认我们刚刚不确定的那个地址是不是就是虚函数Func4()的地址。
我们可以去打印对象的虚表。怎么去打印呢?在VS下呢,还是表好打印的,因为VS下虚表的后面它放了一个空指针。g++上面好像不会,它得写死,你知道有几个虚函数,就去打印几个。
我们假设已经有指向第一个虚函数的指针了,那我如何打印?也就是说我有虚表的指针了,就是vfptr。很简单。这里是一个指针数组,是一个函数指针数组,但是我们的函数很简单,没返回值也没参数,比较方便我们做这块的事情。定义函数指针数组还是有点恶心的。所以我们在这里typedef一下比较方便。
typedef void()() V_FUNC,这样定义是定义不出来的。有要求,用函数指针定义变量或者对他typedef都得放中间。得这样定义
typedef void(V_FUNC)();这样才对!这样说V_FUNC 就代表对是void()()这么一个类型,函数指针这么一个类型。typedef一下,会方便看好多 。不到万不得已不要用函数指针。当然要用的话最好typedef一下。我们写一个函数PrintVFTable(V_FUNC a[]) 或者PrintVFTable(V_FUNC
a),我是一个数组,我的每一个元素都是一个函数指针。数组在参数传参会退化。
如果我们知道有3个就i<3,有4个就 i < 4。但在VS下可以写的灵活一点,我们说了他会在最后一个位置设置为NULL。所以我们可以将判断条件写为a[i] != nullptr!
你是一个函数值的数组,我去你第i个数据就是a[i] 。然后依次打就可以打出来。
我们就打子类的吧,打子类怎么办?只要取到表的首元素地址就能打对不对。这虚表的地址在哪儿?在他的头四个字节!我们也看了VS平台都是放到头部的。
那我们取它头4个字节怎么取?这里有一块对象,对象空间就在这里了,怎么取呢?可不可以说我们将她转成int类型,如:int p = (int/int*)d,不能!因为不相近的这些类型,就算强转了也是不能转的,会报错。不能这样说,我想要你前四个字节,就把你用int就转了。不支持,不支持没有关联性的变量之间就直接转的。int 和 double可以,int和double等可以int是个指针,它也是编号,所以可以转成整数。但是一点关系都没有是不能直接转的。那我想取你头四个字节怎么取?我取到你头4个字节传给PrintVFTable函数,就是虚表的地址就可以打印了。
直接转不可以转,但是这样可以:PrintVFTable(
(int*)&d)指针之间可以互相转换的我取你头四个字节怎么办?任何一个指针都可以互相强转的。我是一个Derive*,把你强转成int* ,然后int解引用是多大?一个char解引用是1,int解引用是4。用char的话你解引用是一个字节,一指针解引用是看类型的大小。你➕4也就是只指向第四个字节,我要的是连续4个字节!所以取头4个字节int是一个好方式。但是PrintVFTable((int)&d)这样传,传不过去。我是Derive我解引用是一个Derive,我是int,我解引用是看一个int。我int是可以看头四个字节了,但我是一个int类型,与你PrintVFTable((int)&d)这个函数参数类型不匹配!
所以还要再做一件事情,你不是一个int嘛,我把你再转一下PrintVFTable((V_FUNC*)(int)&d),把你转回一个指针。你char*+=4也没用呀,因为你char解引用最多只能访问一个字节。
打印后发现,地址对得上!
再来整理一遍,我不能(int)d把你强转成int取你前四个字节,因为语法规定,如果完全没有关联的类型的是不能转的。至少要有一点关系,比如说指针和int,指针虽然是地址,它也是编号。但是指针之间随便类型都可以互相转!&d是一个Derive
,解引用看的是一个Derive,那如果我把你此时强转成int*,然后int解引用可以看到前四个字节。因为是作为int解引用的,是一个int类型,传参的话传不过去,因为你函数参数是一个函数指针数组的指针,所以我还要将你int强转成函数参数的类型。我取到头4个字节了,传给你,然后把虚表里面的内容都打印出来了。
但是还是不能证明最后一个是我们增加的Func4()呀。不要忘了,你有函数的地址是不是可以调用呀。那我们就V_FUNC f = a[i]。有函数的地址调用不就好了吗? 他就会去调函数。
所以终于能确定是Func4()了。

//打印虚表 
void PrintVFTable(V_FUNC* a)
{
	printf("vfptr:%p\n", a);

	for (size_t i = 0; a[i] != nullptr; ++i)
	{ 
		printf("[%d]:%p->", i, a[i]);
		V_FUNC f = a[i];
		f();
	}
}

结论:VS监视窗口看到的虚函数表不一定是真实的,可能被处理过。此处就是,我子类自己增加的虚函数也是进入了虚表的!

✳️
有人可能会问一个问题:会问虚表是存在哪个区域?这个问题即考了语言,也考了操作系统。我们来排一排区域:栈、堆、静态区(也可以叫数据段)、常量区(操作系统喜欢叫做代码段或者叫正文)
如果是我绝对不猜他在栈上,因为栈是用来建立栈桢的,栈桢运行结束就销毁了。难道你虚表一会儿销毁,一会儿又销毁吗?是不是不太好?
虚表是不是最好一个类型只有一个虚表,所以这个类型的对象都存这个虚表指针。那我们是不是希望虚表在一个能长期存储的区域。比如说我有个Base b1、Base b2、Base b3、Base b4,我这四个对象虚表是不是都一样的?
所以同一个类型的对象共用一个虚表!
虚表最好能够永久存储,你不能说存在栈上,这个对象销毁你就跟着走了。
其实按理来说,在编译的时候就建好了虚表。对象在构造的时候才初始化虚表,其实不是初始化虚表,是把这个类型的虚表找到,把这个地址放到对象的头四个字节上。你看初始化过程干嘛,应该说我这个虚表应该放到一个永久的区域,并且要很容易能够找。为什么要很容易能够找呢?因为我构造函数初始化的时候,其实是告诉大家是在初始化哪个阶段呢?是在构造函数初始化列表阶段会挨着给。那我是在这个阶段很容的位置找到这个虚表,把这个虚表的指针加到你头4个字节上。不能说某一个对象销毁,那虚表就没了。
基于他要永久存在,且要在很容易的位置找到,它应该在哪儿?堆上?也不太可能,堆是要动态申请,谁去申请?第一个实例化的对象去申请吗?好像可以,但是谁释放呢?最后一个走的释放吗?也不太符合永久存储吧。
那么只能猜剩下两个了。静态区是整个程序运行期间都在,常量区也是整个运行期间都在。那放在哪里比较好?是我的话会盲猜常量区更合理一点。
为什么常量区更合理呢?因为静态区是放的全局数据和静态数据。那么虚表是全局数据或者静态数据呢?这是个表,这个表是函数指针数组,放在静态区也不是不可以 ,就是差点意思,很膈应。
常量区就很好理解了,你看我虚表其实是一个数组,这个数组里面内容会不会改,不会。你不是说他会覆盖吗?覆盖只是形象的说法,子类呢弄的时候是把父类的拷贝过来,然后把重写的虚函数修改覆盖掉。你说实际当中,编译器生成,真的会说把父类的拿一个过来拷贝加修改再生成一个吗?不会,它就是看,哪一个位置要进行重写,就换成自己的那个。那只是一个形象的说法,帮助我们理解。但是怎么确认呢,我们也不知道。我们写一个程序反向验证一下。
我们定义一个全局变量、一个静态变量、一个局部变量、一个常量区、堆区

Base b1;
	Base b2;
	Base b3;
	Base b4;
	PrintVFTable((V_FUNC*)(*((int*)&b1)));
	PrintVFTable((V_FUNC*)(*((int*)&b2)));
	PrintVFTable((V_FUNC*)(*((int*)&b3)));
	PrintVFTable((V_FUNC*)(*((int*)&b4)));

	int a = 0;
	static int b = 1;
	const char* str = "hello world";
	int* p = new int[10];
	printf("栈:%p\n", &a);
	printf("静态区/数据段:%p\n", &b);
	printf("静态区/数据段:%p\n", &c);
	printf("常量区/代码段:%p\n", str);
	printf("堆:%p\n", p);
	printf("虚表:%p\n", (*((int*)&b4)));
	printf("函数地址:%p\n", &Derive::Func3);---➡️代码段,函数编译完了是一个指令,第一句指令的地址call是函数的地址
	printf("函数地址:%p\n", &Derive::Func2);
	printf("函数地址:%p\n", &Derive::Func1);

我们看看存的虚表指针和谁更接近一点,发现和常量区更加接近一点
在这里插入图片描述

✳️再来谈谈

class Person {
	public:
		virtual void BuyTicket() { cout << "买票-全价" << endl; }
	
		void Buy() { cout << "Person::Buy()" << endl; }
	};
	
	class Student : public Person {
	public:
		virtual void BuyTicket() { cout << "买票-半价" << endl; }
	
		void Buy() { cout << "Student::Buy()" << endl; }
	};
	
	void Func1(Person* p)
	{
		✳️ 跟对象有关,指向谁调用谁 -- 运行时确定函数地址
		p->BuyTicket();
		✳️ 跟类型有关,p类型是谁,调用就是谁的虚函数  -- 编译时确定函数地址
		p->Buy();
	}
	
	int main()
	{
		Person p;
		Student s;
	
		Func1(&p);
		Func1(&s);
	
		return 0;
	}
 

✳️多继承的多态
有一个Base1、Base2,它们都有Func1() 和 Func2()切都是虚函数,然后我Derive继承了Base1 和 Base2,并且对func1进行了重写。我自己有一个单独的虚函数func3()。我们先通过监视窗口来看看,模型应该是怎么样呢?
》想想多继承之后,应该是什么样。我们之前也看过多继承的监视窗口,那我Derive应该是有一个Base1放在前 和 Base2放在后。那我Derive里面应该有几张虚表?明显应该有2张虚表,不可能将Base1的虚表和Base2的虚表混在一起。
》通过看监视窗口也看到的确Base1、Base2下面各有一张虚表,并且各个虚表都有Derive覆盖重写的虚函数func1,然后各个存着没有被重写的func2。
》❓我现在困惑的是我Derive自己的虚函数func3()去哪里了呢?有两张虚表,我应该是放到了哪一张呢?是两张都放,还是放在其中一张呢?现在Derive的模型大概是这样的,下面给了图并且也告诉我们了虚函数func3()放在了Base1的表里面。
在这里插入图片描述
我们来验证一下。我们打Base1的虚表是不是很好打印,因为Base1就是在整个对象的头四个字节上。Base2在中间的位置 ,那怎么去打印Base2的虚表呢?
打印Base1很好打印,我只要PrintVTable((VFPTR*)((int)&d))这样就取到了头四个字节;
》打印Base2,我需要一个指针加到Base2->vTable的位置,怎么到了,我们是不是可以算呢?因为Base1的大小我们可以算是8个字节,但是不建议这样,万一不是int类型了呢?我们还有另一种方法,我们只要在打印Base1的传入参数上➕一个sizeof(Base1)不就好了吗?即PrintVTable((VFPTR*)((int)(&d+sizeof(Base1))));这样对不对?不对!因为你是&d是Derive指针,你➕1是跳过一个Derive对象,而我们只想跳过sizeof(Base1)的大小,所以我们将Derive强转成char*,这时候就对了即,PrintVTable((VFPTR*)((int)((char*)&d+sizeof(Base1))));这时候就跳到了Base2的位置来了
》通过我们打印的虚表可以看到虚函数func3()放到了Base1的虚表中。
》❓又有一个问题,那Base1* ptr1 = &d 和 Base2* ptr2 = &d 和 Derive* ptr3 = &d的3个指针的值是一样的吗?
》这里的Derive对象是不是能给这3种类型的指针呀。这三个指针的值是一样的吗?这三个看起来一样,但实际上是不一样的。我们将同一个对象的地址给他们了,但是他们不一样。
》因为这里要发生切片 ,切片以后呢赋值兼容,地址会不一样。ptr1指向Base1开始的位置,它的类型决定它看到哪里。ptr2,虽然你给的是Derve对象的地址,但是要发生切片,则ptr2要指向自己的那一部分,所以它会去指向Base2开始的地方。ptr3就是自己指向自己的类型,但和ptr1不一样在,它能看到整个Derive对象的大小。
在这里插入图片描述
》❓我还有一个问题就是被重写后的func1()放在Base1和Base2虚表里面的地址是不一样的,为什么呢?
》在Linux下目测两个地址应该是一样的。但为什么在windows下不一样呢?因为我们打印出来的地址不是真正函数的地址,或者说其中一个是,另一个是被封装后的地址。
》我们可以来看看func1()函数的地址到底是和哪张虚表里面的地址是一样的。所以可这样写printf(“%p\n”, &Derive::func1);然后我们打印后发现,func1()函数的地址和两张虚表内被重写的func1()的地址都不一样。
》应该不是出于什么保护机制,只不过就是嵌套了一层。比如说是这样的,这个地方是函数真正的地址,在它的外面再套了一层,它通过一个指令跳转到真正函数的地址。我们在打印虚表的时候,打印func1的地址,是不是都是调用了真正函数的地址才打印出来的呀。说明虽然虚表里面的地址和我Derive::func1()的真正地址不一样,但最后都是跳到了这个真正函数地址上面去。
》只能通过汇编看了。

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

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(*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()
{
	PrintVTable((VFPTR*)(*(int*)&d));
	PrintVTable((VFPTR*)(*(int*)((char*)&d+sizeof(Base1))));

	Base1* ptr1 = &d;
	Base2* ptr2 = &d;
	Derive* ptr3 = &d;
	cout << ptr1 << endl;
	cout << ptr2 << endl;
	cout << ptr3 << endl;

	Derive d;
	Base1* ptr1 = &d;
	Base2* ptr2 = &d;
	
	 调用的都是Derive::func1,但是是在两个虚表中找到的覆盖的func1
	ptr1->func1();
	ptr2->func1();

	return 0;
}

✳️看题

大家说说这里执行顺序,打印是什么样子的呢? 先执行谁?后执行谁?打印出来结果是什么呢?其实这个选项出的不够好,如果要更加迷惑人的话,有一个选项多打印几次class A。
》这个A呢只有一份,理论上而言A只会被初始化1次,凡事菱形虚拟继承对于A的初始化和覆盖,只会用自己的,所以B(s1,s2),C(s1,s3)它都不会去用,只会去用A(s1)。
》这里先初始化B、C呢还是A呢?也就是现在的初始化顺序是先B、C呢还是先A呢?你看A是最上面的基类,严格来说是最先继承A,你继承了两份,但是虚拟继承是不是解决了,所以应该是先初始化A 。虽然A(s1)在初始化列表最后位置,但是我们说了,初始化顺序与初始化列表的位置没有关系而与声明的顺序有关系所以选选项A。

class A{
public:
 A(char *s) { cout<<s<<endl; }
 ~A(){}
};
class B:virtual public A {
public:
 B(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
};
class C:virtual public A {
public:
 C(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
};
class D:public B,public C {
public:
 D(char *s1,char *s2,char *s3,char *s4):B(s1,s2),C(s1,s3),A(s1)
 { cout<<s4<<endl;}
};
int main() {
 D *p=new D("class A","class B","class C","class D");
 delete p;
 return 0; 
  }
  

A:class A class B class C class D B:class D class B class C class A
C:class D class C class B class A D:class A class C class B class D

✳️最后的问答题

  1. 什么是多态?答:参考本节课件内容
  2. 什么是重载、重写(覆盖)、重定义(隐藏)?答:参考本节课件内容
  3. 多态的实现原理?答:参考本节课件内容
  4. inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。(其实我们inline函数里面的代码过多编译器也都会去忽略内联属性,那他是虚函数肯定更加会忽略)其实我们也说了inline内联函数是没有地址的,当你的声明和定义分离是会报链接错误的,它是是不会进符号表的,因为内联函数直接在调用的地方展开,我根本需要地址。虚表呢,虚表里面放的是虚函数地址。如果你是多态调用,我就会去忽略你的内联属性。
    在这里插入图片描述
  5. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。静态函数不可能完成虚函数重写,也不可能完成运行时决议,因为静态成员没有this指针,并且静态成员函数是可以使用类型去调用,如果我以类型的方式去调用,即A::Func()这个样子,不用对象调那我如何去访问虚表呢?就算不以这样的方式去调用,以对象的方式去调用,但他没有this指针,不合乎情理。
  6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。你构造函数是虚函数,你能完成重写吗? 重写了那子类覆盖,那这个地方调用怎么调用。我们说了这几个默认成员函数是这样说的,子类要去调用父类的那个构造函数。你多态不是说要么调用父类要么调用子类吗?最大问题,你们知道虚表是在什么时候初始化的吗?是构造函数阶段才初始化的。那你在调用构造函数之前,虚表其实在编译阶段就已经生成了,但是虚表的初始化是在构造函数初始化列表阶段初始化的。你实现成虚函数是为了完成多态调用,多态调用要在虚表里面去找,我虚表都没有初始化我怎么在虚表里面去找构造函数。
  7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。因为一个基类的指针指向的是一个new的父类对象,也有可能指向new的派生类对象,这时候你要正确调用析构函数就要构成多态。
  8. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
  9. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。只是说虚表的初始化是在初始化对象的时候拿这个虚表的地址拿过来放对象头4个字节上。
  10. C++菱形继承的问题?虚继承的原理?答:参考继承课件。注意这里不要把虚函数表和虚基表搞混了。
  11. 什么是抽象类?抽象类的作用?答:参考(3.抽象类)。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值