[C++#28][多态] 两个条件 | 虚函数表 | 抽象类 | override 和 final | 重载 重写 重定义
目录
0.引入
1.虚函数
1. 虚函数的重写/覆盖
2. 特例1:不加 virtual 关键字的重写
3. 特例2:协变(了解)
2.多态的构成和细节
1. C++11 的 override 和 final
1. final 不可重写
2. override 报错检查
⭕2. 重载、覆盖(重写)和隐藏(重定义)的对比
3. 多态的使用
⭕多态的两个条件:
4.多态的原理
1. 虚函数表(vtable)
2. 动态绑定与静态绑定
5. 抽象类
1. 概念
2. 接口继承和实现继承
6. 单继承和多继承中的虚函数表
1. 单继承中的虚函数表
2. 多继承中的虚函数表
3. 菱形继承和菱形虚拟继承
0.引入
通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。如:比如买票这个行为
- 当普通人买票时,是全价买票;
- 学生买票时,是半价买票;
- 军人买票时是优先买票
1.虚函数
- 即被virtual修饰的类成员函数称为虚函数
class Person{public:virtual void BuyTicket() { cout << "买票-全价" << endl; }};
1. 虚函数的重写/覆盖
重写是指在派生类中对基类的虚函数重新实现,需满足以下“三同”条件:
- 函数名相同
- 参数相同
- 返回值相同
满足以上条件时,子类的虚函数重写了基类的虚函数,体现了接口继承与实现继承的关系。
- 虚函数重写是接口继承:即子类提供了基类虚函数的不同实现。
- 普通函数继承是实现继承:即子类直接使用基类中的普通函数。
若子类函数不符合重写的要求,则会形成隐藏关系,而非重写。
2. 特例1:不加 virtual
关键字的重写
在子类中,即便不加 virtual
关键字,虚函数依然构成重写关系。这是因为派生类继承了基类的虚函数属性,但从规范性和可读性考虑,建议显式加上 virtual
关键字。
析构函数的特殊处理:
- 如果基类的析构函数是虚函数,则派生类的析构函数只要定义,无论是否加
virtual
关键字,都会与基类析构函数构成重写。虽然析构函数的名称不同,但编译器会将其统一处理为destructor
。 - 建议:在继承中,将析构函数定义为虚函数,以保证正确的析构行为。
示例:
class Person
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};class Student : public Person
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用*//*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
虽然可以不加 virtual
,但规范上建议加上,以保持代码的可读性和一致性。
3. 特例2:协变(了解)
协变指的是当派生类重写基类的虚函数时,返回值类型可以不同,但要求遵守以下规则:
- 基类的虚函数返回基类对象的指针或引用。
- 派生类的虚函数返回派生类对象的指针或引用。
这种情况下,尽管返回值类型不同,依然构成重写。
示例:
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.多态的构成和细节
构成条件
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
- 必须通过基类的指针或者引用调用虚函数
1. C++11 的 override
和 final
1. final
不可重写
- 用途:用于修饰虚函数,表示该虚函数不能再被派生类重写。
- 示例:
class Car
{
public:virtual void Drive() final {}
};
使继承自 Car
的派生类都不能重写 Drive
函数。
2. override
报错检查
- 用途:用于检查派生类的虚函数是否正确地重写了基类中的某个虚函数。如果派生类的虚函数没有重写基类的虚函数(即函数签名不匹配),编译器会报错。
- 示例:
class Car
{
public:virtual void Drive() {}
};class Benz : public Car
{
public:virtual void Drive() override { cout << "Benz" << endl; }
};
如果 Drive
函数的签名与 Car
中的 Drive
函数不一致,编译器会报错。
⭕2. 重载、覆盖(重写)和隐藏(重定义)的对比
- 重载:函数名相同,参数列表不同,通常在同一个类中或在派生类中定义多个同名但参数不同的函数。重载与继承无关,可以在同一个作用域中实现。
- 覆盖(重写):派生类中重写基类的虚函数,要求(三同)函数名、参数列表、返回值类型必须与基类中的虚函数完全一致。覆盖是实现多态的关键。
- 隐藏(重定义):派生类定义了一个与基类中同名但非虚函数的函数。此时基类的函数在派生类中被隐藏,不能通过基类指针或引用调用。隐藏通常发生在非虚函数或静态成员函数中。
3. 多态的使用
代码示例:
#include <iostream>class Person {
public:
virtual void BuyTicket() {std::cout << "买票全价" << std::endl;
}
};class Student : public Person {
public:
void BuyTicket() override {std::cout << "买票半价" << std::endl;
}
};void Func(Person &people) {people.BuyTicket();
}void Test() {Person Mike;Func(Mike);Student Johnson;Func(Johnson);
}int main() {Test();return 0;
}
代码解释
- 定义了基类
Person
,其中有一个虚函数BuyTicket()
,输出 "买票全价"。 - 定义了派生类
Student
,继承自Person
并重写了BuyTicket()
函数,输出 "买票半价"。 - 定义了一个函数
Func()
,接收一个Person
类型的引用作为参数,并调用该参数对象的BuyTicket()
函数。 - 在
Test()
函数中,创建了一个Person
类的对象Mike
和一个Student
类的对象Johnson
,分别调用Func()
并传入对象。
- 观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike 对象指向的类的虚表中找到虚 函数是Person::BuyTicket
- 观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson 对象指向的类的虚表中 找到虚函数是Student::BuyTicket
这样就实现出了不同对象去完成同一行为时,展现出不同的形态
⭕多态的两个条件:
- 虚函数覆盖:基类的虚函数被派生类重写。
- 对象的指针或引用调用虚函数:通过基类的指针或引用来调用派生类中重写的虚函数。
两种调用:
- 多态调用:在程序运行时,通过对象的虚表查找对应的虚函数地址并进行调用。虚表(vtable)是由虚函数指针组成的指针数组,每个对象都有一个指向虚表的指针(vptr)。
- 普通函数调用:在编译期间确定函数的地址,运行时直接调用,效率高于多态调用。
4.多态的原理
1. 虚函数表(vtable)
- 虚函数表:每个含有虚函数的类都有一个虚函数表(vtable),其中存储了该类的所有虚函数的指针。每个对象都有一个指向虚函数表的指针(vptr)。
- 虚函数表的生成:
-
- 派生类的虚表首先拷贝基类的虚表。
- 如果派生类重写了基类的虚函数,虚表中相应的指针会被替换为派生类的函数指针。
- 派生类新增的虚函数会按声明顺序添加到虚表的末尾。
- 虚函数表存储位置:虚表指针存在对象中,而虚表本身存储在代码段(常量区)。
- 示例:
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() override{cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};int main()
{Base b;Derive d;return 0;
}
在上述示例中,Base
类和 Derive
类分别有自己的虚表,其中 Derive
类重写了 Func1
,所以它的虚表中存储的是 Derive::Func1()
的地址,而 Base
类的虚表中则存储 Base::Func1()
的地址。
测试看内存
发现:满足多态以后的函数调用,不是在编译时确定的,是运行 起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。
深入理解
在C++中,虚函数是用于实现多态的机制。每个包含虚函数的类都有一个虚函数表(虚表),它是一个包含了所有虚函数地址的数组。这个表存储在类的实例中,而不是函数体中。
- 虚函数和普通函数一样,它们的代码都存在于代码段中。虚表中的每个条目都是一个指向虚函数的指针。当通过基类指针或引用调用虚函数时,编译器会使用这个指针来找到实际的函数地址。
- 对象中存储的是指向虚表的指针,而不是虚表本身。这个指针通常称为“虚表指针”或“vptr”。虚表对象指针是类的一个特殊成员,它指向该类的虚表。
- 在Visual Studio 中,虚表是存储在代码段中的,而不是对象中。这是因为虚表的地址是静态的,对于一个给定的类,它不会随着对象实例的变化而变化。
- 同一个类型的所有对象共享同一个虚表。这意味着对于同一个类的所有实例,它们指向同一个虚表。
- 在Visual Studio中,子类的虚表和父类的虚表不是同一个。这是因为子类可能会添加新的虚函数,或者覆盖父类的虚函数,这些都会导致子类的虚表与父类的虚表不同。
总结:
- 虚函数存在于代码段中。
- 虚表存放在对象中,每个对象有一个指向虚表的指针。
- 虚表中存储的是虚函数的指针。再根据指针来找到地址,获取空间
- 同一个类型的所有对象共享同一个虚表。
- Visual Studio中,子类的虚表和父类的虚表是不同的。(拷贝使用)
- 对象指针-->虚表 ->中存的函数地址 ->调用函数
2. 动态绑定与静态绑定
- 静态绑定(早绑定):在编译期间确定函数调用,主要用于普通函数或函数重载,效率较高。
- 动态绑定(晚绑定):在运行期间根据对象的实际类型决定函数调用,主要用于虚函数,实现了多态,灵活性高。
5. 抽象类
1. 概念
- 抽象类:包含至少一个纯虚函数(即在函数后面写
= 0
的虚函数)的类称为抽象类,不能实例化对象。 - 派生类继承抽象类:派生类必须重写抽象类中的所有纯虚函数,才能实例化对象。如果没有重写纯虚函数,派生类也会成为抽象类。
- 示例:
class Car
{
public:virtual void Drive() = 0; // 纯虚函数
};class Benz : public Car
{
public:virtual void Drive() override{cout << "Benz-舒适" << endl;}
};class BMW : public Car
{
public:virtual void Drive() override{cout << "BMW-操控" << endl;}
};void Test()
{Car *pBenz = new Benz;pBenz->Drive(); // 输出: Benz-舒适Car *pBMW = new BMW;pBMW->Drive(); // 输出: BMW-操控
}
2. 接口继承和实现继承
- 接口继承:虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,主要目的是为了重写虚函数并实现多态。
- 实现继承:普通函数的继承是一种实现继承,派生类继承了基类的函数实现,可以直接使用这些函数。
- 建议:如果不需要实现多态,不要将函数定义为虚函数。
6. 单继承和多继承中的虚函数表
1. 单继承中的虚函数表
派生类的虚表继承自基类,派生类如果重写了基类的虚函数
- 虚表中的相应指针会被派生类的函数指针覆盖。
- 派生类新增的虚函数会添加到虚表的末尾。
class Base
{
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};class Derive : public Base
{
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }
private:int b;
};
可以发现 fun2()地址一样,fun1()实现了重写
实现如下:
2. 多继承中的虚函数表
- 多继承时,派生类会有多个虚表,分别对应继承的多个基类的虚函数。派生类会继承多个基类的虚表,并将未重写的虚函数保留在对应基类的虚表中。
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;
}
多继承原理:继承了两张虚函数表
观察下图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
在两张虚函数表中都对 fun1 进行了重写
3. 菱形继承和菱形虚拟继承
- 菱形继承:由多重继承引发的继承关系,其中基类被多个派生类继承,而这些派生类又被另一个类继承,形成菱形结构。菱形继承容易引发复杂性和性能问题,因此不推荐使用。
- 菱形虚拟继承:通过虚拟继承解决菱形继承中的重复继承问题,虽然避免了多次继承同一基类的副本,但仍可能引发复杂性和性能问题。