多态
多态字面理解就是有多种形式和形态,编程中专门指一种可以将不同的行为关联到一个泛型符号的能力。
多态是面向对象编程的基石之一,C++主要通过类的继承和虚函数来实现多态。
多态又可以分为动多态和静多态,主要的区别是多态的表现形式是在运行期处理,还是在编译期处理。
动多态#
动多态是在运行期处理多态行为,常常说的“多态”也大多指这种形式。
多态的设计思想在于:识别相关对象类型中的一组公共功能,并将其声明为公共基类中的虚函数接口。
struct Coord {
int x = 0;
int y = 0;
Coord operator-(const Coord& other) {
return { x - other.x, y - other.y };
}
Coord abs() {
return { std::abs(x), std::abs(y) };
}
};
class GeoObj {
public:
virtual void draw() const = 0;
virtual Coord centerOfOravity() const = 0;
virtual ~GeoObj() = default;
};
使用类的多态,需要将一些公共接口用虚基类抽象出来,比如上述抽象出了一个图形对象的虚基类,有两个方法,draw()
会画出该图形的形状,而centerOfGravity()
会返回图形的重心位置。
我们将继承这个虚基类,让各种图形实现自己对应的公共方法。
class Circle : public GeoObj {
public:
void draw() const override;
Coord centerOfGravity() const override;
};
class Line : public GeoObj {
public:
void draw() const override;
Coord centerOfGravity() const override;
};
class Rectangle : public GeoObj {
public:
void draw() const override;
Coord centerOfGravity() const override;
};
有三个图形继承了上面的虚基类,分别是圆、直线和矩形,不妨假设它们都已经实现了对应的公共虚函数。然后我们会通过多态的方式使用它们。
void myDraw(const GeoObj& obj) {
obj.draw();
}
Coord distance(const GeoObj& x1, const GeoObj& x2) {
Coord c = x1.centerOfGravity() - x2.centerOfGravity();
return c.abs();
}
void drawElements(const std::vector<GeoObj*>& elems) {
for(auto elem : elems) {
elem->draw();
}
}
int main() {
Line l;
Circle c1, c2;
myDraw(l);
myDraw(c1);
distance(c1, c2);
distance(l, c1);
std::vector<GeoObj*> coll;
coll.push_back(&l);
coll.push_back(&c1);
return 0;
}
我们又定义了几个函数,分别用来画出形状,计算图形重心间的距离和批量画出数组中的所有图形的形状。
可以看到,画出单个图形形状和计算重心距离的函数使用了虚基类GeoObj的引用作为入参,批量绘制图形形状的接口使用虚基类的指针作为动态数组vector的元素类型。
在main函数中,实例化了一个直线图形和两个圆的对象,它们可以直接作为我们定义的函数的入参,这就是多态。通过虚基类的引用或指针,可以访问派生类中重新实现的方法,而这些方法会根据实现的不同,表现出不同的行为。
多态最优秀的特点就是可以批量地处理异类对象集合的能力。
静多态#
模板也可以实现多态,而且模板并不依赖包含公共行为的虚基类,只需要调用的对象有着同名的函数即可。
template<typename GeoObj>
void myDraw(const GeoObj& obj) {
obj.draw();
}
使用这个模板函数,图形不再需要继承虚基类GeoObj,只需要在各自内部有名为draw
的成员函数即可。
换句话说,模板实际上天然具有了多态的特性,它会在编译期根据指定的模板参数类型去调用恰当的函数,所以也被叫做静多态。
不过,静多态不再能够处理void drawElements(const std::vector<GeoObj*>& elems)
,因为模板参数必须通过某种方式显式地指定,就不再能够处理异类对象集合这种情况。这是静多态静态特性所施加的约束,换取的是性能和类型安全方面的一些优势。
总结#
- 通过继承实现的多态是绑定和动态的:
绑定: 意味着参与多态行为的类型的接口是通过继承公共基类来获取的,而这个公共基类是事先设计好的。
动态: 意味着接口的绑定是在运行期(动态地)完成的。
- 通过模板实现的多态是非绑定的和静态的:
非绑定: 意味着参与多态行为的类型的接口并不是预先确定的。
静态:意味着接口的绑定是在编译期(静态地)完成的。
优缺点#
动多态具有的优点:
- 优雅地处理异类集合。
- 可执行代码的大小可能更小。
- 代码可以完全编译,不必发布任何实现源码(分发模板库通常需要分发模板实现的源码)。
静多态具有的优点:
- 内置类型的集合容易实现(不必继承公共基类)。
- 生成的代码效率更高(因为不需要通过指针来调用,可以更频繁地内联非虚函数)。
- 如果程序仅需要执行部分接口,仍可以只提供对应的部分接口。
动多态与静多态并非是非此即彼的关系,实际开发中往往是结合着使用的。通过合理地使用两者,可以得到一些更灵活和强大的代码实现。
使用概念#
静多态强大之处也往往是让人诟病的一点,更高的灵活性意味着没有公共的接口类设计,这样可能会造成一些难以理解的情况发生,甚至是一些能通过编译但是完全不符合预期的行为。
也就是说,模板太“无法无天”了,我们亟需一些限制来约束模板的行为。为此,C++17标准提出了概念(concept)关键字(C++20中已经成为了标准的一部分)。
template<typename T>
concept GeoObj = requires(T x) {
{ x.draw() } ->void;
{ x.centerOfGravity() } ->Coord;
};
这里使用关键字concept定义了GeoObj的概念,它将模板参数类型约束为具有适当返回类型的成员函数draw
和centerOfGravity
的类型。
于是我们可以使用这个概念来约束模板参数:
template<typename T>
requires GeoObj<T>
void myDraw(const T& obj) {
obj.draw();
}
template<typename T1, typename T2>
requires GeoObj<T1> && GeoObj<T2>
void distance(const T1& x1, const T2& x2) {
Coord c = x1.centerOfGravity() - x2.centerOfGravity();
return c.abs();
}
在实例化模板时,会判断模板参数在概念约束下的执行结果,只有通过了概念的约束,判断为true时才会被实例化。
新形式的设计模式#
传统的桥接模式:
传统的桥接模式通过继承来实现:在接口类中定义一个公共基类的指针,通过动多态在多个不同的实现之间进行切换。这就需要提前设计好一个公共虚基类,而采用模板的的方式来实现时,就可以跳过设计公共基类的过程。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通