目录
概念
多态是在不同继承关系的类对象,去调用同一函数,产生不同的行为。
✳多态的构成条件
下面两条构成多态的条件,十分重要,重点记忆!
- 必须通过基类的指针或者引用调用虚函数。
- 被调用的函数必须是虚函数,并且派生类必须对基类的虚函数进行重写。
1.不满足多态:调用函数时看调用者的类型,调用该类型的成员函数。
2.满足多态:调用函数时看指针指向的对象的类型或引用对象的类型,调用该类型的成员函数。
//例:
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Student s;
Func(p);
Func(s);
return 0;
}
上面的例子中:
Func()函数参数为父类对象的引用,正常来说可以传入:1、父类对象;2、子类对象(切片);
当传入父类对象p时,调用父类的函数,传入子类对象s时,调用子类的函数。
不构成多态的时候:传入父类对象p,调用父类函数,传入子类对象s,因为切片,所以看作父类对象,还是调用父类的函数。
虚函数virtual
虚函数:被virtual修饰的类成员函数称为虚函数。
注:构造函数不能作虚函数,因为虚表指针是在初始化列表初始化的;
如果构造函数是虚函数,在调用的时候就需要去虚表中查找其地址,但是此时虚表指针还未初始化,无法查找。
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
虚函数的重写(覆盖)
- 重写/覆盖:子类中有一个跟父类完全相同的虚函数,满足:子类虚函数与父类虚函数的返回值、函数名、参数列表完全相同,此时子类虚函数重写了父类虚函数。
//BuyTicket构成虚函数的重写
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
//1.父类对象的引用或指针调用虚函数
//2.子类对父类虚函数构成重写
//满足两个条件,构成多态
void Func(Person& p)
{
p.BuyTicket();
}
构成虚函数的重写时:子类的函数可以不加virtual,父类必须要加virtual!
因为重写体现的是接口继承!(接口可以理解为函数的声明)
//子类不加virtual也构成虚函数的重写
class Person{
public:
virtual void BuyTicket(){
cout << "买票-全价" << endl;
}
};
class Student : public Person{
public:
void BuyTicket(){
cout << "买票-半价" << endl;
}
};
void Func(Person& p){
p.BuyTicket();
}
接口继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
接口继承-子类函数不用virtual修饰的原因:
这样继承的时候子类函数连同父类函数的virtual一同继承下来了,所以不用写。
虚函数重写的两个例外
- 协变:基类和派生类的函数返回值不相同
- 析构函数的重写:基类和派生类析构函数名不同。
1、协变
基类虚函数返回基类对象的指针或者引用;
派生类虚函数返回派生类对象的指针或者引用;
也可以构成重写:称之为协变。
返回值的父子类可以不是自己所在类的父子类,只要是父类返回父类、子类返回子类即可:
//协变
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;}//返回子类指针
};
2、析构函数重写
为什么函数名不同还能构成多态呢?
因为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
函数名就相同了。
class Person{
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person{
public:
virtual ~Student() { cout << "~Student()" << endl;}
};
虚函数重写特例的总结:
- 返回值可以不同,返回值必须是父子类的指针/引用,构成协变。
- 子类虚函数可以不用virtual修饰。
- 析构函数函数名可以不同。
override 和 final
- final:修饰虚函数,表示该虚函数不能再被重写。
- override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
用法:
1.final
class A
{
public:
virtual void AB() final {}//不能被继承
};
class B :public A
{
public:
virtual void AB() {}
};
2.override
class A{
public:
virtual void AB(){}
};
class B :public A {
public:
virtual void AB() override {}//没有构成重写就报错
};
函数重载、重写(覆盖)、重定义(隐藏)的区别
抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。
包含纯虚函数的类叫做抽象类(也叫接口类)。
- 抽象类不能实例化出对象
- 派生类继承后也不能实例化出对象
- 只有重写纯虚函数,派生类才能实例化出对象
纯虚函数规范了派生类必须重写。
//抽象类
class A
{
public:
virtual void AA() = 0;//纯虚函数
};
class B : public A{
public:
//继承了抽象类A,包含纯虚函数,所以B也是抽象类,也不能实例化对象
};
class C : public A{
public:
virtual void AA(){}//重写纯虚函数,C就可以实例化对象了
};
多态的原理
class B
{
public:
virtual void Func1(){
cout << "Func1()" << endl;
}
virtual void Func2(){}
private:
int _b = 1;
};
class C : public B
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _c = 1;
};
int main()
{
B b;
C c;
cout << sizeof(b) << endl;//8---虚表指针+_b
cout << sizeof(c) << endl;//12---虚表指针+_b+_c
return 0;
}
正常来说sizeof计算出来的结果应该是4,但是结算结果为什么是8呢?
——打开监视窗口,可以发现对象b、c中除了成员变量还有 _vfptr 指针放在对象的头部。
虚函数表(虚表)
这个指针是虚函数表指针
指向虚函数表(虚表),用来存储虚函数的地址,虚函数表是一个函数指针数组;
一个含有虚函数的类中都至少都有一个虚函数表指针;
不是虚函数,函数地址不存在虚表中;
监视:
由监视窗口可以看出:
基类的虚表中存储的是基类的虚函数指针
派生类的虚表中存储的是派生类重写的虚函数指针和继承基类的虚函数指针
重写体现在:子类与父类的虚表不同,子类重写了虚函数,虚表中存的地址也是重写后的子类虚函数地址,所以重写也叫覆盖,虚表中新的地址覆盖原来的虚函数地址。
所以可以实现使用给父类的指针或引用传参调用时,父类传入调用父类的虚函数,子类传入调用子类的虚函数
内存:
对象模型结构:
注:虚函数表中虚函数指针的排列顺序就是虚函数的声明顺序。
补充:
同类对象公用一张虚表:
B b1, b2, b3;
这三者的虚表指针指向同一个虚表,因为虚函数都是相同的,所以存相同的虚函数地址就行了。
为什么要使用基类指针或者引用才能构成多态?
//A为父类
void Func(A a)//若为父类对象,而不是引用或指针,就不行
{
a.Bash();
}
//或者
void Func(A* a)//若为父类指针或引用,可以构成多态
{
a.Bash();
}
解析:
- 若使用父类的指针或引用:
如果指向的是子类对象,则直接在原有子类中切片,指针指向子类中父类的部分,其中的虚表已经经过重写(覆盖),可以找到原来的虚函数。 - 若使用父类对象:
如果传入的是子类对象,会发生切片,子类中的内容切片拷贝到父类的形参中,而如果父类形参想要找到子类的虚函数,就需要将虚表也拷贝,但是虚表不能拷贝!拷贝时只拷贝成员,不拷贝虚表。因为如果拷贝了虚表,那这个父类形参对象的虚表就拷贝的是子类对象的虚表,这样就会出现一个问题:分不清楚这个父类形参的虚表到底是父类的还是子类的(因为不知道传入的是父类还是子类)。
派生类中定义的未重写虚函数在虚表中吗?
class B
{
public:
virtual void Func1() { cout << "B::Func1" << endl; }
virtual void Func2() { cout << "B::Func2" << endl; }
private:
int _b = 1;
};
class C : public B
{
public:
virtual void Func1() { cout << "C::Func1" << endl; }
virtual void Func3() { cout << "C::Func3" << endl; }
private:
int _c = 1;
};
int main()
{
B b;
C c;
return 0;
}
监视:
发现派生类C中虽然定义了一个虚函数Func3,但是却没有出现在监视窗口中的虚函数表中,这是为什么?——监视窗口做了特殊处理,不可信。
vs下的监视窗口会对虚函数表做处理,我们自己写一个函数来查看虚函数表更加合理。
注:在vs在虚表数组最后一位放了一个nullptr
typedef void (*Vfptr)();//函数指针
//打印虚函数表
void printVfptr(Vfptr table[])
{
//不知道虚表大小,因为虚表最后一位是nullptr,用以结束循环
for (int i = 0; table[i] != nullptr; ++i)
{
printf("[%d]:%p->", i, table[i]);
//Vfptr f = table[i];
//f();
table[i]();//和上面的两行效果一样
}
cout << "\n";
}
结果:
可以看出派生类中定义的未重写的虚函数Func3实际上也在虚函数表中。
虚表的存储位置
- 虚表在编译阶段就已经生成好了(函数指针数组);
- 虚表指针是在运行阶段,初始化列表初始化的;
- 虚表指针存储在对象中;
- 虚表存储在代码段中(常量区);
多继承的虚表
class A {
public:
virtual void Func1() { cout << "A::Func1" << endl; }
virtual void Func2() { cout << "A::Func2" << endl; }
private:
int _a = 1;
};
class B {
public:
virtual void Func1() { cout << "B::Func1" << endl; }
virtual void Func2() { cout << "B::Func2" << endl; }
private:
int _b = 1;
};
class C : public A, public B {
public:
virtual void Func1() { cout << "C::Func1" << endl; }
virtual void Func3() { cout << "C::Func3" << endl; }
private:
int _c = 1;
};
typedef void (*Vfptr)();
void printVfptr(Vfptr table[]) {
for (int i = 0; table[i] != nullptr; ++i) {
printf("[%d]:%p->", i, table[i]);
table[i]();
}
cout << "\n";
}
int main()
{
C c;
printVfptr((Vfptr*)(*(int*)&c));
B* ptrb = &c;
printVfptr((Vfptr*)(*(int*)ptrb));
return 0;
}
结果:
- 发现派生类C中单独定义的虚函数Func3只存在第一个虚表(继承自基类A)中,第二个虚表中没有。
- 同时,发现两个虚表中都完成了对Func1的重写,但是二者所存储的地址却不同,这是为什么?
——因为第一个可以直接跳转到虚函数地址进行访问,而第二个虚函数地址是对虚函数进行了包装,通过反汇编可以看到sub ecx, 8
,ecx中存储的是this指针:
为什么第一个虚表可以直接通过虚函数表进行函数运行,第二个虚表要进行封装呢?
解析:
(下图所示)因为ptra与ptrb都是父类的指针,如果通过父类指针调用虚函数Func1则构成多态,都会调用C中的Func1(),但是通过ptra调用时,其中ecx—this指针指向类的起始位置,位置正确;ptrb指向类的中间位置,ecx—this指针也指向类的中间位置,无法正常调用,就需要通过封装来让ecx-sizeof(A)
来修正this指针的位置。
因为以上原因,我们先继承的是A,所以B存在中间位置,通过基类指针B来调用的话就需要封装修正this指针位置了。
静态绑定、动态绑定
- 静态绑定:编译时确定函数行为,比如函数重载
(也叫早绑定、前期绑定、静态多态) - 动态绑定:运行时确定函数行为,比如运行时在虚表中查找使用哪个虚函数
(也叫晚绑定、后期绑定,动态多态)
菱形继承-虚继承
虚函数表(虚表)与虚基表的关系
class AA {
public:
virtual void Func1() { cout << "AA::Func1" << endl; }
virtual void Func2() { cout << "AA::Func2" << endl; }
private:
int _aa = 1;
};
class A : virtual public AA{
public:
virtual void Func1() { cout << "A::Func1" << endl; }
virtual void Func2() { cout << "A::Func2" << endl; }
private:
int _a = 2;
};
class B : virtual public AA{
public:
virtual void Func1() { cout << "B::Func1" << endl; }
virtual void Func2() { cout << "B::Func2" << endl; }
private:
int _b = 3;
};
class C : public A, public B {
public:
virtual void Func1() { cout << "C::Func1" << endl; }
virtual void Func2() { cout << "C::Func2" << endl; }
virtual void Func3() { cout << "C::Func3" << endl; }
private:
int _c = 4;
};
int main()
{
C c;
printVfptr((Vfptr*)(*(int*)&c));
return 0;
}
监视:
可以看到结构与之前的与没有多态的虚继承基本一致,只是在虚基表的前面加了虚表。
- 虚表(虚函数表)——>储存虚函数指针数组
- 虚基表——>储存偏移量
再使用监视窗口查看虚基表中的存储:
可以看到,虚基表有两行:
第一行:ff ff fc ff :-4 ——虚表偏移量,虚基表地址通过这个偏移量查找虚表:
虚基表地址 + (-4) == 虚表地址
第二行:00 00 00 18 :24 ——基类偏移量,A的地址通过这个可以找到AA:派生类A的地址 + 24 == 基类AA的地址