个人主页: 起名字真南的CSDN博客
个人专栏:
- 【数据结构初阶】 📘 基础数据结构
- 【C语言】 💻 C语言编程技巧
- 【C++】 🚀 进阶C++
- 【OJ题解】 📝 题解精讲
目录
- 📌 前言
- 📌1 多态
- ✨ 1.1 `多态`的概念
- 📌 2 `多态`的定义及实现
- ✨ 2.1 实现多态所需要的条件
- 🚀 2.1.1 实现多态的两个`重要条件`
- 🚀 2.1.2 `虚函数`
- 🚀 2.1.3 虚函数的`重写/覆盖`
- 🚀 2.1.4 多态场景的选择题
- 🚀 2.1.5 override和final关键字
- 🚀 2.1.6 重载/重写/隐藏的对比
- 📌 3 纯虚函数和抽象类
- ✨ 3.1 纯虚函数(Pure Virtual Function)
- ✨3.2 抽象类(Abstract Class)
- ✨总结
- 📌 4 多态的原理
- ✨ 4.1 函数表指针
- ✨ 4.2 多态的原理
- 🚀 4.2.1 多态是如何实现的
- 🚀 4.2.2 虚函数表
- 虚函数表(Virtual Table)
- 虚函数表的工作原理
- 结构示例
📌 前言
在C++编程中,多态是面向对象设计(OOP)的核心特性之一,也是提高代码灵活性和可扩展性。通过虚函数和动态绑定,多态可以是代码在运行时根据对象的不同调用实现各自的作用,适应更复杂的业务需求。
然而多态不仅限于简单的继承和重写,它涉及虚函数表,动态绑定,菱形继承,虚继承等。
📌1 多态
✨ 1.1 多态
的概念
多态的概念: 通俗来讲就是多种状态,多态编译分为运行时多态(动态绑定)和编译时多态(静态绑定)。是根据将实参传给形参的参数匹配分别是在编译时确定的和运行时确定的。
📌 2 多态
的定义及实现
✨ 2.1 实现多态所需要的条件
多态是继承关系下的类对象,在调用同一函数的时候所产生的不同的行为
🚀 2.1.1 实现多态的两个重要条件
-
必须指针或者引用调用函数
要实现多态效果,第一必须是基类的指针或者引用,因为只有基类的指针和引用才能即指向派生类对象,又指向基类对象。
-
被调用的函数必须是虚函数
第二派生类必须对基类的虚函数进行重写/覆盖,只有经过重写和覆盖,派生类才能有不同的函数。
🚀 2.1.2 虚函数
类的成员函数前面加上virtual修饰才能称为是虚函数,不是成员函数不能加virtual修饰
class Person
{
public:virtual void BuyTicket(){cout<<"买票-全价" << endl;}
};
🚀 2.1.3 虚函数的重写/覆盖
虚函数的重写/覆盖:派生类中有一个跟基类完全相同的虚函数,我们这里说的完全相同是指虚函数的函数名,参数类型,返回值类型三个完全相同。
class Person
{
public://多态实现的本质,调用虚函数//1、基类的指针或者引用调用虚函数//2、被调用的函数一定是虚函数virtual void BuyTicket(){cout << "买票-全价" << endl;}
};
class Student :public Person
{
public://返回类型,函数名,参数列表完全相同构成虚函数的重写//子类的virtual可以去掉virtual void BuyTicket(){cout << "买票-半价" << endl;}
};
//和Person这个类型没有关系
//传派生类会将基类的那一部分切片
void Func(Person* ptr)
{//虽然都是父类的指针在调用该函数,但是具体的实现是由ptr指向的对象实现的。ptr->BuyTicket();
}
void Func(Person& ptr)
{//虽然都是父类的指针在调用该函数,但是具体的实现是由ptr指向的对象实现的。ptr.BuyTicket();
}
int main()
{//满足多态指向谁调用谁,不满足多态就只和ptr的类型有关系.Person ps;Student st;Func(&ps);//子类隐式转换切割父类的那一部分。Func(&st);//传引用Func(ps);Func(st);return 0;
}
🚀 2.1.4 多态场景的选择题
以下程序输出的结果是什么()
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译报错
class A
{
public:virtual void func(int val = 1){cout << "A->" << val << endl;}virtual void test(){func();}
};class B : public A
{
public://重写只是重写的函数的实现,使用父类函数的声明部分加上派生类的实现部分//本质上时重写虚函数的实现,所以可以不加virtual;//实际上的重写/*virtual void func(int val = 1){cout << "B->" << val << endl;}*///绝不重新定义继承来的缺省值//virtual void func(int val = 0)virtual void func(int val = 1){cout << "B->" << val << endl;}
};int main()
{B* b = new B;//实际上时 A 中*this 来调用func所以构成多态b->test();//不构成多态//b->func();return 0;
}
正确答案是 B : B->1
分析:
**接下来我们很定会有这样的疑问:**首先我们看到 b
对象是一个指向 B
类型的一个指针,但是 B
是派生类,为什么还会构成多态呢? 构成多态的条件不是需要父类的指针吗?
**接下来我们带着疑问来解决上面的问题,**在 main
函数中 b
对象调用了 test
函数,并且通过 test
函数来调用 func
函数。我们可以发现 test
中并没有传入参数,但是我们忽略了作为一个成员函数总是有一个 this
指针,并且是父类的 this
指针,所以我们可以认为是 A
类型的 this
指针来调用的 func
函数。这样解决了第一个问题。
接下来我们来看 为什么继承的函数还是使用基类的缺省值。其实在继承虚函数时,我们不是将整个虚函数复制过来,而是通过虚函数表来调用它(虚函数表我们后面会详细讲解)。在派生类中不需要在继承的函数上重新加 virtual
,这是因为继承时我们继承的是函数的定义,而重写的部分仅是函数的实现(即函数体部分)。因此,最终输出的是 B
类型的函数体和 A
类的缺省值。
🚀 2.1.5 override和final关键字
C++11提供了override,可以帮助⽤⼾检测是否重写。如果我们不想让派⽣类重写这个虚函数,那么可以⽤final去修饰。
class Car
{
public://使用final不能被重写virtual void Drive() final{}
};class Benz : public Car
{
public://检查是否完成了重写,在编译时进行检查virtual void Drive() override{cout << "Benz" << endl;}
};
在这个代码中Drive函数被final修饰所以不能被重写/覆盖,同时在派生类中邪了override关键字用来检查函数时候被重写/覆盖,这样就会报错。
🚀 2.1.6 重载/重写/隐藏的对比
📌 3 纯虚函数和抽象类
在C++中,纯虚函数和抽象类是面向对象编程中的两个重要概念,主要用于定义接口和实现多态性。以下是对这两个概念的详细介绍:
✨ 3.1 纯虚函数(Pure Virtual Function)
- 定义:纯虚函数是没有实现的虚函数,只在类中声明但不提供具体实现。其声明格式为:
其中virtual void functionName() = 0;
= 0
表示该函数是纯虚函数。 - 目的:纯虚函数通常用于定义一个接口或规范,要求派生类必须提供该函数的具体实现。
- 特点:
- 纯虚函数没有函数体。
- 任何包含纯虚函数的类都无法直接实例化。
- 派生类继承该类时必须实现纯虚函数,否则派生类也会成为抽象类。
示例:
class Shape {
public:virtual void draw() = 0; // 纯虚函数
};class Circle : public Shape {
public:void draw() override {std::cout << "Drawing Circle" << std::endl;}
};class Square : public Shape {
public:void draw() override {std::cout << "Drawing Square" << std::endl;}
};int main() {Shape* s1 = new Circle();Shape* s2 = new Square();s1->draw(); // 输出: Drawing Circles2->draw(); // 输出: Drawing Squaredelete s1;delete s2;return 0;
}
在上面的例子中,Shape
类是一个基类,它定义了一个纯虚函数draw
。Circle
和Square
是派生类,它们实现了draw
函数,因此可以被实例化。
✨3.2 抽象类(Abstract Class)
- 定义:抽象类是包含一个或多个纯虚函数的类。由于包含纯虚函数,抽象类无法直接实例化。
- 目的:抽象类用于作为接口或基类,提供公共的接口规范,而不需要自己实现具体功能。
- 特点:
- 任何包含纯虚函数的类都是抽象类。
- 抽象类只能作为基类使用,不能直接创建对象。
- 派生类可以继承抽象类并实现其纯虚函数,从而可以实例化派生类对象。
- 抽象类可以包含已实现的普通成员函数,但这不会改变其抽象类的特性。
示例:
class Animal {
public:virtual void sound() = 0; // 纯虚函数void sleep() {std::cout << "Animal is sleeping" << std::endl;}
};class Dog : public Animal {
public:void sound() override {std::cout << "Woof!" << std::endl;}
};int main() {// Animal a; // 错误!无法实例化抽象类Dog d;d.sound(); // 输出: Woof!d.sleep(); // 输出: Animal is sleepingreturn 0;
}
在该例子中,Animal
是一个抽象类,它定义了一个纯虚函数sound
。派生类Dog
实现了sound
,因此可以实例化。同时,抽象类Animal
也可以包含一个普通函数sleep
,并且可以在派生类中使用。
✨总结
- 纯虚函数:只声明而没有定义的虚函数,用于要求派生类实现某些行为。
- 抽象类:包含纯虚函数的类,不能直接实例化,通常用作接口或基类。
📌 4 多态的原理
✨ 4.1 函数表指针
下面在32为系统下编译的结果为()
A: 编译报错 B:运行报错 C: 8 D:12
class Base{public:virtual void Func1(){cout << "Func1()" << endl;}protected:int _b = 1;char _ch = 'x';};int main(){Base b;cout << sizeof(b) << endl;return o;}
正确答案是12
分析:
在C++中,类对象的内存布局会受到成员变量和虚函数表指针的影响:
-
成员变量的大小和对齐:
int
类型的成员变量_b
占用4字节。char
类型的成员变量_ch
占用1字节。- 由于内存对齐,
_ch
后面会填充3字节,使得成员变量部分占用8字节。
-
虚函数表指针:
- 含有虚函数的类会在其对象中包含一个虚函数表指针(也称虚表指针),用于在运行时支持多态。
- 虚表指针占用4字节,指向虚函数表,表中存储了类的虚函数地址。
-
总内存占用:
- 成员变量8字节 + 虚表指针4字节 = 总共12字节。
重点
- 虚函数表指针:每个包含虚函数的类都会有一个虚表指针,占用4字节,用于在对象中指向虚函数表,支持运行时的多态。
- 内存对齐:成员变量按照对齐要求进行排列,使得结构体或类的内存大小可能大于简单的成员变量之和。
✨ 4.2 多态的原理
🚀 4.2.1 多态是如何实现的
class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
};class Derive : public Base
{
public:// 重写基类的func1virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3(){cout << "Derive::func1" << endl;}
};
int main()
{//同类型的虚函数表是一样的,防止数据冗余。Base b1;Base b2;Derive d;return 0;
}
此时我们进行的是动态绑定,即运行时到指向的对象的虚表中确定对应函数的地址然后再进行调用这样就实现了指针或引⽤指向基类就调⽤基类的虚函数,指向派⽣类就调⽤派⽣类对应的虚函数。
🚀 4.2.2 虚函数表
虚函数表(Virtual Table)
在C++中,虚函数表(vtable)是编译器用于支持运行时多态的一种机制。当一个类中包含虚函数时,编译器会自动为这个类生成一个虚函数表,用于存储类的虚函数地址。每个对象实例则包含一个指向虚函数表的指针(称为虚表指针,vptr)。
虚函数表的工作原理
-
创建虚函数表:在包含虚函数的类中,编译器会为该类生成一个虚函数表。虚函数表包含了所有虚函数的地址。对于基类和派生类,编译器会为每个类单独生成虚函数表。
-
虚表指针(vptr):每个类的对象会在内存中包含一个指向其类对应虚函数表的指针,称为虚表指针(vptr)。这个指针在对象创建时自动初始化,以指向该对象所属的类的虚函数表。
-
调用过程:当通过基类指针或引用调用虚函数时,编译器会先通过对象的虚表指针(vptr)找到虚函数表(vtable),然后在虚函数表中查找并调用对应的虚函数,实现多态。
结构示例
假设有如下类结构:
class Base {
public:virtual void func1() { std::cout << "Base::func1" << std::endl; }virtual void func2() { std::cout << "Base::func2" << std::endl; }
};class Derived : public Base {
public:void func1() override { std::cout << "Derived::func1" << std::endl; }virtual void func3() { std::cout << "Derived::func3" << std::endl; }
};
在这个例子中,虚函数表的布局如下:
-
Base类的虚函数表:
- 包含
func1
和func2
的地址。 func1
指向Base::func1
。func2
指向Base::func2
。
- 包含
-
Derived类的虚函数表:
- 继承自
Base
,会包含func1
和func2
,但func1
指向Derived::func1
(因为重写了该函数),func2
仍指向Base::func2
。 func3
是Derived
独有的虚函数,因此也会被添加到Derived
类的虚函数表中。
- 继承自
内存布局示例
假设创建一个Derived
类的对象d
,则d
的内存布局如下:
- 虚表指针:指向
Derived
类的虚函数表。 - 成员变量:包含类定义中的其他成员变量(例如
int a;
等)。 - 虚函数表:指向的虚函数表中包含
Derived::func1
、Base::func2
、Derived::func3
的地址。
虚函数表的优点
- 实现多态:虚函数表是C++实现运行时多态的核心,允许程序在运行时根据对象的实际类型选择函数。
- 性能较高:通过虚表指针查找虚函数地址,只需一次指针查找和一次跳转,效率相对较高。
重要注意点
- 虚表是类级别的:同一类的多个对象共享同一个虚函数表。
- 虚表指针是对象级别的:每个对象实例都有自己的虚表指针,用于指向所属类的虚表。
- 虚函数表只在有虚函数的类中存在:没有虚函数的类不会生成虚函数表。