目录
一、多态的概念
多态的概念,通俗来说,就是多种形态,具体就是去完成某个行为,当不同的对象去完成时会产生不同的状态
我们先来演示一下多态
class person
{
public:
virtual void Tastefood()
{
cout << "吃火锅:全价" << endl;
}
};
class student :public person
{
public:
virtual void Tastefood()
{
cout << "吃火锅:半价" << endl;
}
};
void func(person& p)
{
p.Tastefood();
}
int main()
{
person p;
student s;
func(p);
func(s);
return 0;
}
func里面时父类对象不仅可以传父类也可以传子类,不同对象输出的结果是不同的,这就叫做多态
但是我们把func里面的引用去掉试试呢
居然又是另一种结果了,就不是多态了
二、多态的实现
C++多态有两个条件
1.虚函数重写
2.父类的指针或者引用去调用虚函数
那我们逐步分析
class person
{
public:
virtual void Tastefood()
{
cout << "吃火锅:全价" << endl;
}
};
在上一篇文章的继承我们写到了virtual,这二者有什么必然关系吗,答案是没有关系,一个是多态的条件,一个是解决菱形继承数据冗余的问题
那虚函数的重写需要什么条件呢
虚函数的重写要满足继承父子关系的两个虚函数:函数名相同,参数相同,返回值相同(三同)
但是三同有例外:协变->返回值可以不同,但是必须是父子类关系的指针或者引用
class person
{
public:
virtual person* Tastefood()
{
cout << "吃火锅:全价" << endl;
}
};
class student :public person
{
public:
virtual void Tastefood()
{
cout << "吃火锅:半价" << endl;
}
};
一般的返回值不同都不行
class person
{
public:
virtual person* Tastefood()
{
cout << "吃火锅:全价" << endl;
return nullptr;
}
};
class student :public person
{
public:
virtual student* Tastefood()
{
cout << "吃火锅:半价" << endl;
return nullptr;
}
};
注意:virtual只能修饰成员函数
但是派生类又可以不加virtual,因为派生类重写的是父类的实现,你可以认为把父类这个继承下来了,你父类是虚函数,派生类也是虚函数了
~person()
{
cout << "~person()" << endl;
}
~student()
{
cout << "~student()" << endl;
}
我们再把析构加进去
person p;
student s;
满足先子后父,子类析构结束后会去自动调用父类的析构函数
但是我们来看这里
person* p = new person;
delete p;
p = new student;
delete p;
父类对象去调父类的析构没问题,但是子类对象怎么也去调父类的析构
因为delete根据类型去调,但是我们第四行父类的指针有可能指向父类有可以指向子类
delete由于多态的原因是由p->destructor()+operator delete(p);封装构成的,所以派生类的析构和父类的析构构成隐藏关系,因为它们的函数名被处理成destructor
但是这里又是普通调用,普通调用看类型,两个都是person类型,但是我们期望这里是多态调用,指向父类调用父类,指向子类调用子类,这里我们满足了第一个条件:父类的指针或引用
剩下的我们只要满足虚函数重写就好了
virtual ~person()
{
cout << "~person()" << endl;
}
virtual ~student()
{
cout << "~student()" << endl;
}
这里就构成多态了,我们不写虚函数是不是就构成内存泄露了,前面父类加virtual子类不加也是在为这方面考虑,你父类写了你子类就可以不写了
注意:我们虚函数重写是继承父类的接口,重写父类的实现,所以缺省值一般使用的是父类的
重载、覆盖(重写)、隐藏(重定义)的对比
1.重载
两个函数在同一作用域
函数名/参数不同(类型,个数,顺序)
2.重写(覆盖)
两个函数分别在基类和派生类的作用域
函数名/参数/返回值都必须相同(协变例外)
两个函数必须是虚函数
3.重定义(隐藏)
两个函数分别在基类和派生类的作用域
函数名相同
两个基类和派生类的同名函数不构成重写就是重定义
关键字final
在上一篇文章继承里我们写到 final修饰类,不能被继承
final修饰虚函数,不能被重写
关键字override
override修饰派生类的虚函数
它是用来检查派生类是否重写
没有完成重写就会报错
三、抽象类
在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更能体现出了接口继承
class book
{
public:
virtual void reader() = 0;
};
int main()
{
book b;
return 0;
}
虽然无法定义出对象,但是可以定义出指针
class west :public book
{
public:
};
当然它的派生类也继承了纯虚函数,自然也无法实例化出对象
除非你把纯虚函数进行了重写
class west :public book
{
public:
virtual void reader() {
cout << "west :西游"<<endl;
}
};
class country :public book
{
public:
virtual void reader() {
cout << "country :三国"<<endl;
}
};
void func(book* b)
{
b->reader();
}
int main()
{
func(new west);
func(new country);
return 0;
}
指向哪个调哪个
它和override的区别在于:它间接强制去派生类重写,因为你不重写它就实例化不出对象。
那比如说人这个抽象类,根据这个类我们细分出各种职业比如医生,老师,律师等,但是人不是一个具体的职业,人没有具体的职业,自然无法实例化出对象
它这里的多态意味着它想在多个子类中实现
四、多态的原理
虚函数表
class A
{
public:
virtual void think()
{
cout << "class A" << endl;
}
private:
int _a;
char b;
};
int main()
{
cout << sizeof(A) << endl;
return 0;
}
看这段代码,熟悉内存对齐规则的人都会说这里为8
但是实际上是12,原因就在这里存了一张虚函数表
只要一个类有虚函数就要多一个指针,把虚函数的地址存到这张表上
class A
{
public:
virtual void think1()
{
cout << "think1" << endl;
}
virtual void think2()
{
cout << "think2" << endl;
}
void think3()
{
cout << "think3" << endl;
}
private:
int _a;
char b;
};
这个虚函数表存着两个虚函数的地址
那我们也可以从这张图里面看到,这个虚函数表的类型其实是一个虚函数指针数组
多态原理
class A
{
public:
virtual void think1()
{
cout << "think1" << endl;
}
virtual void think2()
{
cout << "think2" << endl;
}
void think3()
{
cout << "think3" << endl;
}
private:
int _a=1;
};
class B :public A
{
public:
virtual void think1()
{
cout << "B::think1" << endl;
}
virtual void think2()
{
cout << "think2" << endl;
}
private:
int _b = 2;
};
int main()
{
A aa;
B b;
return 0;
}
派生类里面只有一个虚表指针,派生类由两部分组成一个是父类的一个是自己的,但是派生类的虚表指针和父类的不是同一个,因为我们完成的是虚函数的重写,虚函数的重写也叫做覆盖,覆盖成新的虚函数
那么为什么指向父类调父类指向子类调子类呢
因为是父类的指针,我们传父类,这个func里面的指针根据父类的虚函数表去找父类的这个think1,传子类就切割或者切片出属于父类的那一部分,去找虚函数表,但是因为虚函数表子类的已经重写了所以调子类的虚函数
普通调用是在编译链接时,确定地址
多态调用时在运行时,去虚表里面找到函数地址,确定地址,再调用
那对象为什么不行呢
void func(A p)
{
p.think1();
}
就像我们A a=b,就是切割出子类对象中父类那一部分,成员拷贝给父类,但是不会拷贝虚函数表指针
那我们假设父类对象=子类对象会拷贝虚函数表指针
那么多态调用,指向父类,调用还是父类虚函数吗?不一定
因为你把子类的虚表拷过去,你调用父类的时候还是父类的虚函数吗,假设我子类的虚函数中间实现的时候有赋值改变什么的,这就完不成多态调用了
如果子类的虚表被拷过去,这里传父类的对象指向子类的虚表,父类的对象怎么能去调子类的虚函数,所以不会拷贝虚函数表指针,因为会造成逻辑紊乱
那如果我们子类不重写虚函数,父类和子类的虚表一不一样?
不一样,虚表本来也没多大的空间,没必要一样搞出其他麻烦
但是同一个类是共用一张虚表的
那么虚函数存在哪里的?虚函数表又是存在哪里的
虚函数和普通函数一样都是存在代码段的,同时把虚函数地址存了一份在虚函数表
虚函数表因为它不允许修改,又跟静态变量一般可以供一个类多个对象使用,所以应该存在代码段(常量区)
那么虚函数一定是放在虚表里面的吗
class A
{
public:
virtual void think1()
{
cout << "think1" << endl;
}
virtual void think2()
{
cout << "think2" << endl;
}
void think3()
{
cout << "think3" << endl;
}
private:
int _a=1;
};
class B :public A
{
public:
virtual void think1()
{
cout << "B::think1" << endl;
}
virtual void think4()
{
cout << "think4" << endl;
}
private:
int _b = 2;
};
void func(A p)
{
p.think1();
}
int main()
{
A a;
B b;
return 0;
}
看监视窗口我们可以发现b的虚函数表里面继承了think2,重写了think1,但是自身的虚函数think4不见了,难道虚函数真的不一定存在虚函数表里面吗,其实监视窗口是会骗人的,它给你看到的都是包装过的,但是内存不会骗人
但是b的虚函数表我们只能确定前两个,其他的我们只能做到怀疑但是做不到确定
我们在前面说了,虚函数表本质上是一个函数指针数组,那我们是不是可以通过函数指针数组的方式来打印函数指针,来看到呢
typedef void (*VFNC)();
void printf(VFNC* a)
{
for (size_t i = 0; a[i] != 0; i++)
{
printf("[%d]:%p\n", i, a[i]);
VFNC f=a[i];
f();
}cout << endl;
}
int main()
{
A a;
printf((VFNC*)(*((int*)&a)));
B b;
printf((VFNC*)(*((int*)&b)));
return 0;
}
接下来我们来逐步解析一下,首先是定义一个函数指针数组,(VFNC*)(*((int*)&a))这个我来给大家逐步拆分一下,我们知道虚函数表里面存的是一个指针,我们只要访问一个对象的头四个字节就能访问到虚函数表指针,这时候我们想到了int,但是int和指针是不相近的类型,但是指针和指针之间可以来回转换,但是由于我们解引用完是int,我们传过去的是要函数指针数组,所以我们强转成VFNC*
printf那里面我们就可以通过打印地址来发现了,但是我们还可以添加函数指针来验证是否构成多态来进一步验证,f就是虚函数的地址,我们知道函数名就是可以当作地址来访问,而且我们的参数为了方便访问弄成了无参的
不仅访问到了,还构成了多态
多继承的派生类有两张虚表,派生类的虚函数存在继承的第一个父类里面。
多态在这里写完了,多态涉及到很多涉及内存的东西,有时候监视窗口不起作用的时候,我们就需要往内存里面看,写的不好的地方欢迎大家指出,接下来要进入搜索二叉树了