前言
先来看C++ Primer中的概述
OOP:概述
面向对象程序设计(object-oriented programming)的核心思想是数据抽象,继承和动态绑定(动态多态),通过使用数据抽象,我们可以将类的接口与实现分离,使用继承,可以定义相似的类型并对其相似关系建模,使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用他们的对象。
多态定义:在C++中,多态性(Polymorphism)是实现面向对象编程的一个重要概念,它允许我们通过基类的指针或引用调用派生类的函数。多态性主要有两种形式:编译时多态(也称为静态多态)和运行时多态(也称为动态多态)
编译时多态:指的是重载等,在编译时已经确定
运行时多态就是今天要学的动态绑定,它允许我们在运行时根据对象的实际类型来调用相应的方法,而不是在编译时确定,这种机制使得我们可以编写更加通用的代码,忽略相似类型之间的区别。
比如:在12306上买票,可能学生半价,成人全价,这就是一种多态,这节课的知识点讲解以动态绑定(动态多态)为主
!!!!!重点:1.知道多态是怎么写的
2.多态是怎么具体实现的
一、多态的写法
先给出例子,再来讲解条件:
条件(必须背下来的):
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数(只需要父类加virtual,子类可以写可以不写,但是建议写,增加代码可读性),且派生类必须对基类的虚函数进行重写
第一个条件很好理解,第二个条件中你可能会问啥是虚函数?啥是重写?注意:不要和继承中的virtual(菱形虚拟继承)和重定义(隐藏)联系起来,他俩没有任何关系
虚函数:
被virtual修饰的函数即为虚函数(类内)其他地方是编译错误的
啥是重写?重写又可以叫做覆盖,也是很形象的说法,派生类把基类的函数覆盖掉,要求三同:返回类型,函数名,形参类型都必须相同
注意:重写只是重写了函数内部的实现,并没有改变形参的值,相当于前半段用父类的,实现用子类的,粘在一块覆盖,你可能会懵,没事,看个例子就懂了
这里就很清晰的看出只重写了函数内部的实现
注意:重写有两个例外
第一个例外:协变:派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。这种也是重写,但是返回类型又不一样
不要求一定是本类的指针或者引用
是基类和派生类就可以
另外,加virtual并且函数名相同然后其他不同会报错!!!!
第二个例外:析构函数的重写(上个博客埋下的坑这节博客来讲)
问题来了:析构函数明明函数名不同怎么构成的重写?因为编译器统一处理成了destructor,为什么编译器要统一处理成destructor?因为要构成重写,为什么要构成重写?因为要满足多态的第二个条件,重点问题来了,为什么要构成多态?
请看下面的例子:
class Person {
public:
~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
~Student() { cout << "~Student()" << endl; }
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
这不对啊!new了一个student对象怎么没有析构啊!因为这里没有构成多态,所以p2会调用Person的析构而没有析构Student,造成了内存泄漏,所以:回到最上面的问题,这就是要构成多态的原因:能够正确的析构函数
二、override final 抽象类 重写,重定义,重载的对比
这里讲解一些小点,真正的大的在后面具体的多态的实现
1.override
作用:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写会编译报错。
这个报错真是。。。实际上是因为没有重写基类的虚函数报错
2.final
在父类中修饰虚函数,表示该虚函数不能再被重写,重写会报错
还有一个作用,建类时放在类的后面,让他无法被继承
3.抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
void Test()
{
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
}
4.重写(覆盖),重定义(隐藏),重载的区分
重写其实是更严格的重定义
plus:实现虚函数就是为了重写构成多态,所以普通继承就别带virtual了,浪费空间,另外析构函数涉及到父类和子类的析构问题要加virtual去构成多态
三、原理
。和菱形虚拟继承一样恶心,但是更重要
实现:通过虚函数表(和虚基表没有任何关系!!!!!)
注意:运行环境是VS2019 x86环境下,64位机的结果会大不相同!!!也推荐大家用x86环境,因为x86环境下指针的size是4,而64下是8,private放int类型的时候不会涉及结构体对齐问题
Base1中应该有个什么东西来指向虚函数表,没错:就是存了一个虚函数指针
vfptr:virtual function ptr 虚 功能 指针----虚函数指针
多态的情况呢?
我们利用这个代码来分析
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()
{
Derive d;
Base b;
return 0;
}
d中func1和b中不一样,这里就完成了重写
结论:
- 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
- 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
- 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
- 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
- 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
123都比较直观,好理解,我们来看一下45
4:
plus:地址没有正着倒着之分,数据存储比如4个字节的数据01 02 03 04,小端机下看到的是04 03 02 01,这里输入地址不要倒着输入地址了
可以看到确实之后是0
5:将Derive类中加入个虚函数再看看
欸?为什么监视里面没有,而内存里面确实放了个看着像func4的指针呢,到底是谁出问题了呢?我们可以来验证一下
思路:依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
这tm不是C语言的函数指针数组?
不知道为啥csdn一打星号字就变斜体了还不显示,统一用星号表示了
回忆一下函数指针是啥(为了配合这个类,返回值统一用void)void (型号) () 这样形式的
当时学习typedef 这个最特殊,需要将名字放在星号之后
typedef void()();
稍微回忆回忆,知道如何定义我们就要开始操作了
思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
1.先取b的地址,强转成一个int的指针
2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
4.虚表指针传递给PrintVTable进行打印虚表
5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了
(注意x86环境下指针是4个字节所以可以转成int的x64得搞成long的)
代码:
typedef void (*vft_ptr)();
void PrintVftable(vft_ptr * f)
{
for (int i = 0; f[i] != nullptr; ++i)
{
printf("第%d个虚函数地址 :%p\n", i + 1, f[i]);
f[i]();
}
cout << endl;
}
int main()
{
Derive d;
Base b;
vft_ptr* p1 = (vft_ptr*)(*(int*)&b);//取出地址->转成int* ->解引用->转成vft_ptr*
vft_ptr* p2 = (vft_ptr*)(*(int*)&d);
PrintVftable(p1);
PrintVftable(p2);
return 0;
}
结果:
验证了派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后这句话。
所以没有显示func3是vs的bug
上面都是一些底层分析没有具体多态的例子
回到最开始:
这个到底是怎么实现的?
指向父类在父类的虚函数表中找到对应的函数
指向子类在子类的虚函数表中找到对应的函数
这样就实现出了不同对象去完成同一行为时,展现出不同的形态。–多态
为什么是动态的?根据汇编(懒得看了,记住就行)多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中找的。所以是动态
而重载–编译时就确定好了–静态的
原理讲完了—就要思考一下多态的第一个条件,为什么是父类的引用或者指针?
父类比较好想,因为子可以给父,父无法给子,子类无法指向父类,
为什么是引用或者指针?直接给值不行?
void func(const Person p)?–这个为什么不行呢???
想想这两个的区别
class Person {
public:
virtual void BuyTicket()const { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket()const { cout << "买票-半价" << endl; }
};
void func(const Person&p)
{
p.BuyTicket();
}
int main()
{
Student s;
Person p;
p = s;
return 0;
}
答:为了防止切片,值的拷贝是要进行切片的,子类对象的派生部分会被切掉,基类成员直接赋值过去,那我把虚表也复制过去?不提成本,这样是一定不行的,如果我把虚表复制过去,那我原来Person里的就是半价还能改成全价了?如果是指针或者引用就不会有这个情况,不会进行切片,直接引用原对象,所以这就是原因。
还有一个问题虚表存在哪?
结论:代码段,验证给出一种代码验证思路
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
};
int main()
{
//栈区
int a = 1;
//堆区
int* _b = new int;
//静态区--数据
static int c = 2;
//常量区--代码段
const char *str = "hello world";
Derive d;
Base b;
printf("栈区地址:%p\n", &a);
printf("堆区地址:%p\n", _b);
printf("静态区地址:%p\n", &c);
printf("常量区地址:%p\n", str);
printf("虚表1地址:%p\n", *((int*)&d));
printf("虚表2地址:%p\n", *((int*)&b));
return 0;
}
由图可看出,虚表是放在常量区的
解释一下为什么*(int星)&d可以拿到虚表地址,要知道d本身有个地址,里面存储的vfptr的值是我们的虚表地址
我们就是想拿到vfptr的值,所以强转成int*再解引用就可以拿到值了。
终于写完原理了,好多。。。
四、多继承中的虚函数表
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()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
PrintVTable(vTableb2);
return 0;
}
得到结论:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
菱形继承、菱形虚拟继承中的多态。。。。这个就建议别碰,本来就不应该搞出菱形继承,现在你又来个菱形虚拟继承中多态。。。。恶心人呢
想看的自己看看。。。反正我是不想看
class Base {
virtual void func1() { cout << "Base::func1" << endl; }
};
class Base1 :public Base {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
private:
int b1;
};
class Base2 :public Base {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
private:
int d1;
};
int main()
{
Derive d;
d.func1();
return 0;
}
注意这样会报错
这样就好了
至于内存结构。。。想看的自己看看吧,有个虚函数指针还有个虚基表指针。。。相当复杂了
五、一些小的注意事项
1.inline函数可以是虚函数吗?可以,但是没用,虚函数要扔到虚表里,而内敛函数直接在代码中展开了,所以可以但是没用,
2.构造函数可以是虚函数吗,不可以,编译错误,为什么呢?虚函数表指针是在构造函数初始化列表才初始化的(自己可以调试去验证)那我调用构造的时候哪来的指针?
3.静态成员函数可以是虚函数吗?不可以
因为虚函数的唯一用处是多态,静态成员函数没有this指针,可以通过Base1::func()来访问,调用不到虚函数表,静态成员放不进去,另外,静态成员属于所有类来通用的,更不可能是虚函数了
4.虚函数表是在什么阶段生成的,存在哪的?
虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
5.对象访问普通函数快还是虚函数更快?
首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
总结
一天写两个博客累死我了
继承和多态的八股文较多,但是考的频率还不低,该背就得背下来!