通常讨论C++的多态,指的是通过基类指针/引用来使用其派生类,这叫做子类多态(subtype polymorphism)。但是实际上还有其他的多态,例如参数多态(parametric polymorphism),特设多态(ad-hoc polymorphism,我翻译的可能不太好,后面还是用ad-hoc原文),强制多态(coercion polymorphism)。
这些多态在C++中还有其他名字:
- 子类多态又称为运行时多态(runtime polymorphism)。
- 参数多态又称为编译期多态(compile-time polymorphism)。
- ad-hoc多态又称为重载(overloading)。
- 强制转换多态又称为类型转换(casting)。
在这篇文章中,我将通过C++语言的示例来阐述所有的多态性,并且解释它们为什么会有各种不同的名称。
当提到C++的多态时,默认指的是子类多态。它指的是通过基类指针/引用来使用其派生类的能力。
下面是一个例子。假设你有各种各样的猫科动物(felid)。由于它们都是猫科动物,并且它们都应该能够喵喵叫,因此它们可以表示为继承自Felid
基类并覆盖meow
纯虚函数的类,
// file cats.h
class Felid {
public:
virtual void meow() = 0;
};
class Cat : public Felid {
public:
void meow() { std::cout << "Meowing like a regular cat! meow!\n"; }
};
class Tiger : public Felid {
public:
void meow() { std::cout << "Meowing like a tiger! MREOWWW!\n"; }
};
class Ocelot : public Felid {
public:
void meow() { std::cout << "Meowing like an ocelot! mews!\n"; }
};
现在主程序可以通过Felid
(基类)指针来调用Cat
, Tiger
和 Ocelot
的方法。
#include <iostream>
#include "cats.h"
void do_meowing(Felid *cat) {
cat->meow();
}
int main() {
Cat cat;
Tiger tiger;
Ocelot ocelot;
do_meowing(&cat);
do_meowing(&tiger);
do_meowing(&ocelot);
}
在这个程序中,主程序将指向Cat
, Tiger
和 Ocelot
的指针传递给一个期望接收指向Felid
的指针的do_meowing
函数。由于它们都是Felid
的子类,程序会为每个Felid
调用正确的meow
函数,输出结果如下:
Meowing like a regular cat! meow!
Meowing like a tiger! MREOWWW!
Meowing like an ocelot! mews!
子类多态也被称为运行时多态,这个名称的由来有很充分的理由。多态函数调用的解析是在运行时通过虚表(virtual table)的间接引用来完成的。另一种解释是,编译器在编译时并不确定被调用函数的地址,而是在程序运行时,通过在虚表中解引用正确的指针来调用函数。
在类型理论中,它也被称为包含多态。
参数多态性提供了一种对任何类型执行相同代码的方法。在C++中,参数多态性是通过模板实现的。一个最简单的例子是一个泛型的max
函数,它寻找两个参数中的最大值。
#include <iostream>
#include <string>
template <class T>
T max(T a, T b) {
return a > b ? a : b;
}
int main() {
std::cout << ::max(9, 5) << std::endl; // 9
std::string foo("foo"), bar("bar");
std::cout << ::max(foo, bar) << std::endl; // "foo"
}
这里的max
函数在类型T
上是多态的。但是请注意,它不适用于指针类型,因为比较指针比较的是内存位置而不是内容。要让它为指针工作,你必须为指针类型专门化模板,这将不再是参数多态,而是特殊多态。
由于参数多态发生在编译时,因此也称为编译时多态。
[!NOTE]
关于模板,更多的内容可以参考模板。
ad-hoc多态允许具有相同名称的函数对每种类型有不同的行为。例如,对于两个int
,+
将他们相加;对于两个string
,+
将他们连接。这叫做重载。
这里是一个具体的例子,为int
和string
实现函数add
。
#include <iostream>
#include <string>
int add(int a, int b) {
return a + b;
}
std::string add(const char *a, const char *b) {
std::string result(a);
result += b;
return result;
}
int main() {
std::cout << add(5, 9) << std::endl;
std::cout << add("hello ", "world") << std::endl;
}
ad-hoc多态也出现在C++的模板特化中。还是以上面的max
模板为例,以下是如何为两个char *
编写max
template <>
const char *max(const char *a, const char *b) {
return strcmp(a, b) > 0 ? a : b;
}
现在你可以调用::max("foo", "bar")
来查找字符串“foo”和“bar”的最大值。
当一个对象或基本类型被强制转换为另一个对象类型或基本类型时,就会发生强制转换。例如:
float b = 6; // int gets promoted (cast) to float implicitly
int a = 9.99 // float gets demoted to int implicitly
如果类的构造函数不是explicit
,也会发生强制转换。
#include <iostream>
class A {
int foo;
public:
A(int ffoo) : foo(ffoo) {}
void giggidy() { std::cout << foo << std::endl; }
};
void moo(A a) {
a.giggidy();
}
int main() {
moo(55); // prints 55
}
如果你创建了explicit
修饰的构造函数,那就不可能了。推荐使用explicit
修饰构造函数以避免意外转换。
同样,如果一个类定义了类型T
的转换操作符,那么它可以在任何需要类型T
的地方使用。例如:
class CrazyInt {
int v;
public:
CrazyInt(int i) : v(i) {}
operator int() const { return v; } // conversion from CrazyInt to int
};
CrazyInt
定义了一个类型为int
的转换运算符。现在,如果我们有一个函数,比如说,print_int
接受int
作为参数,我们也可以向它传递一个CrazyInt
类型的对象,
#include <iostream>
void print_int(int a) {
std::cout << a << std::endl;
}
int main() {
CrazyInt b = 55;
print_int(999); // prints 999
print_int(b); // prints 55
}
前面讨论的子类型多态实际上也是强制转换多态,因为派生类被转换为基类类型。
祝您在学习这些关于多态性的新知识时玩得开心,下次再见!
原文链接:The Four Polymorphisms in C++。作者是一个大佬。