目录
一,概念
二,定义
2. 虚函数重写(覆盖)
虚函数3个特例
1. virtual 关键词
2. 重写析构函数
3.协变
3.面试题
4.接口继承与实现继承的区别
5. C++11 override & final
1. final:修饰虚函数,表示该虚函数不能再被重写(用的比较少)
2.override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
6. 重载,重写,重定义之间的区别
7. 抽象类
三,多态原理
1.概念
2. 实验
3. 验证(比较难)
4.补充动静态绑定
四,多继承多态
结语
程序中,同一个函数名字可以有多个不同的实现方式,根据函数的参数类型或个数的不同来决定具体调用哪个实现方式的特性。这种特性可以让程序在编译时期无需确定函数具体的实现方式,而是在运行时根据实际情况动态地确定调用哪个实现方式 。
那么在继承中要构成多态还有两个条件:
1. 必须通过基类的指针(或引用)调用虚函数。(硬性要求)
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
class Person
{
public:
virtual void BuyTicket()
// void BuyTicket()
8 { cout << "全价" << endl;}
9
10 ~Person(){ cout << "~Person" << endl; }
11 };
12
13 class Student: public Person
14 {
15 public:
16 virtual void BuyTicket()
// void BuyTicket()
17 { cout << "半价" << endl;}
18
19 ~Student(){cout << "~Student" << endl;}
20 };
21
22 void func(Person& p1)
23 {
24 p1.BuyTicket();
25 }
int main()
28 {
34 Person p;
35 func(p);
36 Student s;
37 func(s);
38 return 0;
39 }
当程序调用一个虚函数时,会根据指针或引用的实际类型来确定调用的是哪个类的虚函数。具体的调用过程如下:
class A {};
class B : public A {};
class Person
{
public:
virtual A* fc() { return new A; }
};
class Student : public Person {
public:
virtual B* fc() { return new B; }
// 省略virtual 也可以
};
如以上代码可见,返回的两个对象需要是父子关系。
class A
{
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <"<< val <test();
return 0;
}
解释一下:为啥结果是B->1???
这里就引出了
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种 接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
概念:
class A
{
virtual void func() = 0;
};
class B : public A
{
void func(){}
};
void main()
{B b;}
引出:
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
就是这次要讲的——虚函数表,而这多出来的4字节是,一个指针。
虚函数表是一个存储了虚函数地址的表格,每个对象都有一个指向虚函数表的指针。当调用一个虚函数时,编译器会通过对象的虚函数指针找到对应的虚函数表,并根据函数在虚函数表中的位置来确定调用哪个函数的实现。并且,在程序中使用基类指针或引用指向子类对象时,编译器只能根据指针或引用的类型来确定调用哪个函数,而无法确定实际指向的子类对象的类型。因此,编译器会将虚函数的调用转换为一个虚函数表(vtable)的查找过程,以在运行时动态地确定调用哪个虚函数的实现。(来自chatgpt)
观察下面的代码的运行监视窗口:
class A
{
public:
virtual void func() {
cout << "A func";
}
virtual void test(){
cout << "A test";
}
void KD()
{
cout << "KD";
}
int a;
char c;
};
class B : public A
{
virtual void func(){
cout << "B func";
}
// virtual void test(){}
virtual void one()
{
cout << "B one" << endl;
}
};
void main()
{
A a;
B b;
b.KD();
}
观察结果:
(1. 派生类对象b中也有一个虚表指针(且通过查看_vfptr不难发现子父类对象都有各自的虚表),b对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
a.先将基类中的虚表内容拷贝一份到派生类虚表中。b.如果派生类重写了基类中某个虚函数,用派生类自己的 虚函数指针覆盖虚表中基类的 虚函数指针。c.派生类自己新增加的虚函数按其在派生类中的声明次序增加派生类虚表的最后。
验证:子类新的虚函数,是否能出现在虚表里?
首先我们观察下面代码:
class A
{
public:
virtual void func() {cout << "A func";}
virtual void test(){cout << "A test";}
int a = 1;
char c;
};
class B : public A
{
virtual void func(){ cout << "B func";}
virtual void one(){ cout << "B one" << endl; }
};
void main()
{
A a;
B b;
}
补充一下函数指针的知识:函数指针类型的定义格式为:返回值类型 (*指针变量名)(参数类型列表)。其中,返回值类型是指被指向函数的返回值类型,参数类型列表是指被指向函数的参数类型列表。
例如:如果有一个函数指针类型为void (*FuncPtr)(),可以使用typedef关键字为该类型定义一个新的名称,如typedef void (*NewFuncPtr)()。然后,可以使用NewFuncPtr来声明函数指针变量,如NewFuncPtr ptr = func;,其中func是一个与函数指针类型匹配的函数。(来源chatgpt)
实验代码如下:
typedef void (*VFP)(); // 重命名函数指针类型,为VFP
// 制作一个便利虚表的函数,通过一般情况这个数组最后面放了一个nullptr,就可判断结束条件
void VFP_table(VFP* _vfp) // 本质上是一个二级指针
{
for (int i = 0; _vfp[i] != nullptr ; i++)
{
printf("VFP[%d]->%p 调用结果: ", i, _vfp[i]);
VFP instruction = _vfp[i];
instruction();
cout << endl;
}
cout << endl;
}
void main()
{
A a;
B b;
// 利用虚表存放在基类数据的前4个字节(或8字节)
VFP_table((VFP*)(*(int*)(&a)));//局限性x64地址有8位,这里的int得换成long long
VFP_table(*(VFP**)&b);// 比较简单的理解是,接口需要2级指针,这里有三级,解引用1级
}
感兴趣的同学可以自己画图。
结果呢?
结果:是我们访问到了那个在监视窗口中未曾可视的虚函数,监视窗口只是隐藏了。被virtual 的虚函数指针都会在虚表中。
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;
}
结果分析:
由上图可知:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
菱形继承,简单展示
具体想了解,请移步大佬文章:C++ 虚函数表解析 | 酷 壳 - CoolShell
这边建议,菱形虚拟继承与多态融合不用深入研究,容易走火入魔!
本小节就到这里了,感谢小伙伴的浏览,如果有什么建议,欢迎在评论区评论,如果给小伙伴带来一些收获请留下你的小赞,你的点赞和关注将会成为博主创作的动力