多态
多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
子类的函数覆盖\重写了这个函数。
多态的定义实现
多态的构成条件
✅继承中要构成多态还有两个条件缺一不可:
- 必须通过基类的指针或者引用调用虚函数。
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
虚函数的重写:三同(函数名、返回值、参数列表)即可重写父类虚函数。
例外(协变):返回值可以不同,但是必须是父子关系的指针或者引用
例外:子类的虚函数可以不加virtual
虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
虚函数重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
重写相当于使用父类的接口,自己写一个实现方法。包括缺省值都是使用父类的缺省值。(附录题1)
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person
{
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
//void BuyTicket() { cout << "买票-半价" << endl; }
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写
(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用*/
}
void Func(Person& p)
{ p.BuyTicket(); }
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
重写的例外
1.协变
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
class A{};
class B : public A {};
class Person
{
public:
virtual A* f() {return new A;}
};
class Student : public Person
{
public:
B* f() {return new B;}
};
2.析构函数重写
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。
虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}//destructor
};
class Student : public Person {
public:
~Student() { cout << "~Student()" << endl; }//destructor
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,
//才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写。 (因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。
因为子类继承父类的实现重写实现,所以不写也可以,不建议使用!!
C++11 override 和 final
final:修饰虚函数,表示该虚函数不能再被重写
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Car
{
public:
virtual void Drive(){}
};
class Benz :public Car
{
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
重载、重写、隐藏区别
✅虚函数就是为重写而生,重写就是为多态而生的!
抽象类
在虚函数的后面写上 =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();
}
纯虚函数其实就是强制你重写实现多态。而override是在子类中使用的,强制检查重写,而纯虚函数是在父类中,间接要求重写。
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
所以如果不实现多态,不要把函数定义成虚函数。!
多态的原理
一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。(题解3)
编译器也是严格按照多态的构成条件去对应的,为什么是要求指针或者引用调用函数呢?因为指针指向的值是是在虚函数表里,具体是谁不知道,但是如果是子类的话,也是子类通过切片之后覆盖掉父类指向的地址,所以重写又叫覆盖,并且要求指针或者引用调用。
虚函数表本质上就是一个虚函数指针数组.虚函数表在编译时确定!
✅“覆盖”虚函数时,覆盖掉的是虚函数表(vtable)中的函数指针,而不是虚函数表指针(vptr)本身。
✅单继承,一个类虚表就一张,但是虚函数表指针有很多,每个对象存一个自己虚函数表指针指向虚表,每个对象会覆盖掉自己父类的虚函数表中的指针,虚表中存储着很多虚函数的函数指针。这些地址指向的是类当前对应的实现函数,用于实现运行时多态。
步骤 | 操作方法 |
---|---|
类创建 | 编译器生成虚表(虚函数表在编译时确定!) |
对象创建 | 自动内嵌一个 vptr,指向所属类的虚表 |
函数调用 | 虚函数指针 → 虚函数表中的指针 → 函数指针 → 多态调用 |
❓那为什么对象不能实现多态呢?
对象的切片只拷贝成员,不拷贝虚表,父类是父类的虚表,子类是子类的虚表。对象本身不“实现”多态,而是“参与”多态,类通过继承、重写方法、接口等方式“实现多态”,对象通过“父类引用指向子类实例”来“触发多态”。
虚函数和普通函数一样都是放在代码段上,但是虚函数的地址会放在虚函数表。想知道一个对象在哪儿,关键是看你是怎么创建它的!
new 出来的就在堆上;局部变量一般在栈上;全局变量/静态变量在数据段。方法、类的代码永远不会跟对象放一起,它们在代码段
总结
- 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
- 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表 中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函 的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
- 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函 数,所以不会放进虚表。
- 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。(vs中)
- 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生 类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己
新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。- 这里还有一个很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的 呢?去验证一下会发现vs下是存在代码段的
问题与解答
多继承
多继承中派生类中自己的虚函数放在第一张虚表。
❓ 细心的朋友会发现多继承重写的func1 的地址不一样!?
✅ 多继承下虚函数地址不同的原因总结:
-
每个基类有独立的虚函数表(vtable)→ 多继承时,派生类有多个 vtable(每个基类一个)。
-
虚函数在不同基类的 vtable 中分别填入→ 即使是重写了同一个函数名,也会分别写入 A 和 B 的 vtable。
-
函数地址不同是因为编译器生成了不同的“跳板函数”(thunk)→ 用于调整 this 指针,保证调用时指向正确的对象内存区域。
-
实际最终调用的都是派生类的实现(如 C::func)→ 函数功能一致,但入口地址(调用路径)不同。
🎯 函数地址不同,不是因为调用了不同的函数,而是因为从不同基类视角进入同一个函数需要不同的跳板。 如下所示!
动态绑定和静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
拓展:虚基表中存储什么数据(see see 就好)
虚表、虚基表、变量没有规定具体的上下排序,是编译器规定的事情
附录
题解1
题解2
题解3
题解4
内联函数可以是虚函数吗?
编译器处理内联虚函数时会做两套准备:
- 生成常规的内联版本(用于直接调用)
- 在虚函数表中放置函数指针(用于多态调用)
- 如果是内联就直接展开,如果是多态就调用地址,使用多态的方式。
虚函数:实现运行时多态
内联:优化直接调用的性能(二选一)
是虚函数就不是内联,是内联就不是虚函数
静态成员函数是虚函数吗?
调用方式冲突
静态成员函数:通过类名直接调用(ClassName::staticFunc()),不依赖于对象实例
虚函数:必须通过对象实例调用(使用对象的虚函数表vtable)存储位置不同
静态成员函数:属于类级别,存储在代码区
虚函数:需要每个对象维护虚函数表指针(vptr)隐含的this指针
普通成员函数:隐含this指针参数
静态成员函数:没有this指针
虚函数:依赖this指针访问对象的虚函数表
构造函数能是虚函数吗
对象创建机制冲突
虚函数机制依赖于已存在的虚函数表(vtable)和虚指针(vptr)
构造函数的任务正是创建这些机制本身
这是一个"先有鸡还是先有蛋"的问题:需要虚表来调用虚构造函数,但虚表又需要构造函数来建立语言设计逻辑
虚函数用于实现运行时多态,即在对象已存在时决定调用哪个函数
构造函数的工作是创建对象,此时对象还不存在,谈不上多态编译器实现角度
虚函数调用需要通过vptr查找vtable
但在构造函数执行前,对象内存刚分配,vptr还未初始化
如果构造函数是虚的,将无法确定该调用哪个构造函数