本文为The Four Polymorphisms in C++这篇文章的翻译,非机器翻译,如有理解或术语错误望指正,谢谢~
C++中的四种多态(Polymorphisms)
当人们在谈论C++的多态的时候,他们通常指的是通过基类的指针或者引用,来使用派生类,这其实被称为子类型多态(subtype polymorphism),但是他们经常会忘掉其实C++里面还有各种其他的多态,比如参数多态(parametric polymorphism),重载多态(ad-hoc polymorphism)和强制多态(coercion polymorphism)。
这些多态在C++中也有其他不同的名称:
- 子类型多态也被称为运行时多态(runtime polymorphism)。
- 参数多态也被称为编译时多态(compile-time polymorphism)。
- 重载多态也被称为重载(overloading)。
- 强制多态也被称为**(显式的或隐式的)类型转换(casting)**。
我将在这篇文章里通过例子来说明C++中的所有多态,并告诉你为什么它们有这样的名字。
子类型多态(运行时多态)
子类型多态是人们在说C++多态时所能理解的那种,它可以通过基类的指针或者引用来使用派生类。
举个例子,假设你有各种的猫科动物,
由于他们都属于猫科生物家族,所以他们都可以发出类似喵的叫声(原文是they all should be able to meow),这样他们都可以通过继承猫科动物
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);
}
主程序向do_meowing
这个函数传递了指向cat
, tiger
和ocelot
的指针,而这个函数期望一个指向Felid
对象的指针。因为cat
, tiger
和ocelot
都属于Felid
,程序可以为每个猫科动物调用正确的meow
函数,并且输出如下:
Meowing like a regular cat! meow!
Meowing like a tiger! MREOWWW!
Meowing like an ocelot! mews!
子类型多态也被叫作运行时多态,因为,多态函数调用的解析发生在运行时,并通过虚表间接完成。另一种解释方式是,编译器不能在编译期定位函数被调用的地址,而在程序运行时,通过取消引用虚表的右指针来调用函数。
在类型理论中这也被成为包含多态性。
参数多态(编译时多态)
参数多态提供了一种可以对任何类型数据执行相同代码的方法。在C++里,参数多态通过模板(template)来实现。
一个最简单的例子就是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
上就是参数多态的,但是如果参数类型是指针那就不行了,因为比较指针相当于比较内存地址,而不是指针指向的内容。如果想要能比较指针指向的内容,就必须专门再设计一个指针类型的模板,那样的话其实就不再是参数多态了,而是重载多态。
因为参数多态发生在编译时,所以它也被称为编译时多态。
重载多态(重载)
重载多态可以允许函数拥有相同的函数名而对不同的类型的数据执行不同的动作。比如,给两个int
数据和+
号,加号会把他们加在一起。给两个字符串std::string
,加号会把他们连接在一起,这就叫重载。
下面给出一个实现对int类型和string类型相加的具体例子,
#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;
}
如果专门设定模板里的类型,那也是重载多态,回到我们之前的例子,下面的代码展示了如何写一个max
函数来比较两个char *
指针,
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 隐式地转换成 float
int a = 9.99 // float 隐式地转成 int
显示类型转换发生在你使用C风格的类型转换表达式比如(unsigned int *)
, (int)
或者C++的static_cast
, const_cast
, reinterpret_cast
或者dynamic_cast
。
强制多态也会发生在非显式调用类的构造函数时,例如,
#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
// 译者注:传进去了55数字,但是隐式地调用了A类的构造函数,发生了int-> A的类型转换
}
如果你显示调用了A的构造函数,那就不会发生强制多态了,显示的调用类的构造函数总是比较好的,可以避免意外的转换。
此外,如果一个类定义了对类型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
// 译者注:传进去b对象,隐式地转换成了int数字
}
前面讲到的子类型多态实际上也是强制多态,因为他是把派生类转换成了基类。