多态是什么?
多态(Polymorphism)是面向对象编程中的一个核心概念,它来源于希腊语,意为“多种形态”。
从字面意思理解,多态是指函数有多种形态(实现)。换句话说,运行阶段同一条函数调用语句可能会调用不同的函数实现。例如
struct Shape {
virtual float area() = 0;
};
struct Rectangle : Shape {
float area() { // 计算并返回长方形面积 }
};
struct Circle : Shape {
float area() { // 计算并返回圆形面积 }
};
float CalcRatio(Shape& shape) {
...
float area = shape.area();
...
}
在运行阶段,语句float area = shape.area();
调用到的函数实现可能是Rectangle::area()
,也可能是Circle::area()
,还可能是其他Shape
的子类实现的area()
。
多态有什么用?
多态的最终目的是复用代码(面向对象本身就是为了复用代码而出现的,而多态又是面向对象的一个关键概念)。多态允许我们在不关注实现细节的情况下编写更通用的流程和框架,从而达到复用代码的目的。例如,计算不同图形周长和面积的比值
struct Shape {
float perimeter() { ... }
float area() { ... }
};
struct Rectangle : Shape {
float perimeter() { // 计算并返回长方形周长 }
float area() { // 计算并返回长方形面积 }
};
struct Circle : Shape {
float perimeter() { // 计算并返回圆形周长 }
float area() { // 计算并返回圆形面积 }
};
// 同理,定义 Hexagon 和 Ellipse
float CalcRatio(Shape& shape) {
float ratio = 0.0f;
if (长方形) {
Rectangle realShape = (Rectangle)shape;
float perimeter = realShape.perimeter();
float area = realShape.area();
ratio = perimeter / area;
} else if (六边形) {
Hexagon realShape = (Hexagon)shape;
float perimeter = realShape.perimeter();
float area = realShape.area();
ratio = perimeter / area;
} else if (圆) {
Circle realShape = (Circle)shape;
float perimeter = realShape.perimeter();
float area = realShape.area();
ratio = perimeter / area;
} else if (椭圆) {
Ellipse realShape = (Ellipse)shape;
float perimeter = realShape.perimeter();
float area = realShape.area();
ratio = perimeter / area;
}
// ... 其他更多形状
return ratio;
}
不难发现,求每个图形比例系数的步骤都是一样的:1)求周长;2)求面积;3)计算周长和面积的比例。这个过程被重复编写,既不美观也不易维护。这些重复代码似乎写一遍就可以了,像这样
float CalcRatio(Shape& shape) {
float perimeter = shape.perimeter();
float area = shape.area();
float ratio = perimeter / area;
return ratio;
}
但是,在不使用多态的情况下shape.perimeter()
和shape.area()
调用的函数实现是固定的,每个图形都有对函数perimeter()
和area()
的实现,shape.perimeter()
和shape.area()
应该调用哪个函数实现?似乎调用哪个都不可行。
多态正是为解决这个问题而出现的。因此在使用了多态的情况下,代码可以简化成后面的形式。
当然多态也有其他的一些好处(实际上这些好处的最终目的还是复用代码):
- 提高灵活性和可扩展性。在不更改现有代码的情况下添加新的类或子类。让新的子类对象与现有代码一起工作。
- 提高可维护性。多态性促进了代码的模块化和分离。通过将公共功能放在父类中,并在子类中重写特定的功能,可以更容易地维护和更新代码。
- 支持设计模式。许多设计模式,如策略模式、工厂模式和观察者模式,都依赖于多态性。
个人理解:代码设计技术比如OOP、设计模式最终目的都是为了复用代码。
怎么使用多态?
c++使用虚函数提供多态能力,虚函数是指用关键字virtual
修饰的函数。具体有两个步骤:
- 在父类中声明虚函数。
- 在子类中重写这个虚函数。
注意子类覆写虚函数的时候需要确保函数的类型、名称、参数列表等与基类保持一致,否则无法使用多态。c++11引入的关键字
override
就是为了让编译器自动检查覆写的正确性,这个关键字是可选的。
比如前面的例子,如果要利用多态特性将CalcRatio()
改写成复用版本,就需要使用关键词virtual
修饰父类Shape
中的函数perimeter()
和area()
,例如
struct Shape {
virtual float perimeter() { ... }
virtual float area() { ... }
};
struct Rectangle : Shape {
float perimeter() override { // 计算并返回长方形周长 } // override 可选
float area() override { // 计算并返回长方形面积 } // override 可选
};
struct Circle : Shape {
float perimeter() override { // 计算并返回圆形周长 } // override 可选
float area() override { // 计算并返回圆形面积 } // override 可选
};
// 同理,定义 Hexagon 和 Ellipse
一些补充
- 虚函数一定是成员函数。
- 纯虚函数。纯虚是没有函数体的虚函数,它的定义类似
virtual float perimeter() = 0;
。注意包含纯虚函数的类不能实例化。 - 返回类型协变。返回类型协变是指在子类中重写基类的虚函数时,允许返回类型是基类函数返回类型的子类型。
多态的实现原理?
实现多态的一个关键技术是动态绑定。相对于静态绑定(在编译期间就能确定调用哪个函数实现)而言,动态绑定是指在运行阶段确定将调用哪个函数实现的过程。而c++的动态绑定能力是由虚函数提供的,所以我们要研究的实际上是虚函数的实现原理。
虚函数的实现原理
- 编译器会给有虚函数的类分配一个虚函数表,虚函数表里存储了这个类所有虚函数的函数指针。
- 编译器会给有虚函数的类的对象分配一个指向这个虚函数表的指针。
例如下面的代码
struct Shape {
int getId();
virtual float perimeter();
virtual float area();
};
struct Rectangle : Shape {
float perimeter();
float area();
};
struct Circle : Shape {
float perimeter();
float area();
};
// 定义getId、perimeter、area的函数体
float CalcRatio(Shape& shape) {
int id = shape.getId();
float perimeter = shape.perimeter();
float area = shape.area();
float ratio = perimeter / area;
return ratio;
}
对应的汇编代码为
Shape::getId():
...
ret
Shape::perimeter():
...
ret
Shape::area():
...
ret
Rectangle::perimeter():
...
ret
Rectangle::area():
...
ret
Circle::perimeter():
...
ret
Circle::area():
...
ret
CalcRatio(Shape&):
...
# int id = shape.getId();
mov rax, QWORD PTR [rbp-24]
mov rdi, rax
call Shape::getId()
mov DWORD PTR [rbp-4], eax
# float perimeter = shape.perimeter();
mov rax, QWORD PTR [rbp-24]
mov rax, QWORD PTR [rax]
mov rdx, QWORD PTR [rax]
mov rax, QWORD PTR [rbp-24]
mov rdi, rax
call rdx
movd eax, xmm0
mov DWORD PTR [rbp-8], eax
# float area = shape.area();
mov rax, QWORD PTR [rbp-24]
mov rax, QWORD PTR [rax]
add rax, 8
mov rdx, QWORD PTR [rax]
mov rax, QWORD PTR [rbp-24]
mov rdi, rax
call rdx
movd eax, xmm0
mov DWORD PTR [rbp-12], eax
...
ret
# Shape类的虚函数表
vtable for Shape:
.quad 0
.quad typeinfo for Shape
.quad Shape::perimeter()
.quad Shape::area()
# Circle类的虚函数表
vtable for Circle:
.quad 0
.quad typeinfo for Circle
.quad Circle::perimeter()
.quad Circle::area()
# Rectangle类的虚函数表
vtable for Rectangle:
.quad 0
.quad typeinfo for Rectangle
.quad Rectangle::perimeter()
.quad Rectangle::area()
...
通过汇编代码可以看到Shape
类、Rectangle
类和Circle
各有一张虚函数表,表内存放的是各自对perimeter()
和area()
两个函数的实现。
# Shape类的虚函数表
vtable for Shape:
.quad 0
.quad typeinfo for Shape
.quad Shape::perimeter()
.quad Shape::area()
# Circle类的虚函数表
vtable for Circle:
.quad 0
.quad typeinfo for Circle
.quad Circle::perimeter()
.quad Circle::area()
# Rectangle类的虚函数表
vtable for Rectangle:
.quad 0
.quad typeinfo for Rectangle
.quad Rectangle::perimeter()
.quad Rectangle::area()
三条函数调用语句编译后对应三个call
操作
函数调用过程释义如下图
- 第一个
call
操作是对非虚函数Shape::getId()
的直接调用。 - 后两个
call
操作是对虚函数的调用。虚函数调用被编译为两个查找操作和一个调用函数指针call rdx
操作(寄存器rdx
存放的是函数地址)。
可以看到,调用虚函数比调用非虚函数多了两个查找操作,c++虚函数实现原理的核心正是多出来的这两次查找操作。第一个查找操作是为了找到对象实际类型的虚函数表;第二个查找操作是为了在虚函数表中找到真正需要调用的函数实现。
由于
shape
对象的实际类型未知,所以第一次查找操作找到的虚函数表是不确定的;而虚函数表中注册的函数是确定的,所以只要能找到这个对象对应的虚函数表那么函数实现也就是确定的,因此多态实际发生在第一次查找操作。
看到这里,有些同学会发现:这TM不是函数注册表吗?没错,虚函数的原理实际上就是函数注册表,只不过建表和查表的过程由编译器代劳了。
用一个例子来结束本文
对于下面的代码
struct Shape {
virtual float perimeter() { ... }
virtual float area() { ... }
};
struct Rectangle : Shape {
float perimeter() { ... }
float area() { ... }
};
struct Circle : Shape {
float perimeter() { ... }
float area() { ... }
};
float CalcRatio(Shape& shape) {
float perimeter = shape.perimeter();
float area = shape.area();
float ratio = perimeter / area;
return ratio;
}
编译器会为类型Shape
及其子类Rectangle
、Circle
各分配一张虚函数表,表里存储了各自对虚函数perimeter()
和area()
的实现。
如果CalcRatio()
的入参shape
实际类型是Circle
,函数调用语句float perimeter = shape.perimeter();
会触发下面一系列操作
- 访问
shape
对象的虚函数指针,找到shape实际类型的虚函数表;这里shape
的真实类型为Circle
所以找到的是Circle
的虚函数表; - 访问虚函数表,查找函数
perimeter()
的实现;这里是在Circle
的虚函数表里查找,所以到的是Circle::perimeter()
; - 调用找到的函数;即调用
Circle::perimeter()
;
如果入参shape
的实际类型是Rectangle
,那么将会在是Rectangle
的虚函数表里查找perimeter()
的实现,最终调用的函数就是Rectangle::perimeter()
。