C++多态技术在下面的这篇文章里描述得很清晰,但它忽略了技术细节,即多态是如何实现的?在看完第一章之后,虚函数和虚函数表是下一站。
第一章 C++多态技术
C++多态技术
作者:荣耀
提交者:eastvc 发布日期:2003-12-14 19:38:12
原文出处:http://www.royaloo.com/articles/articles_2003/PolymorphismInCpp_content.htm
摘要
本文描述了C++中的各种多态性。重点阐述了面向对象的动态多态和基于模板的静态多态,并初步探讨了两种技术的结合使用。
关键词
多态 继承 虚函数 模板 宏 函数重载 泛型编程 泛型模式
导言
多态(polymorphism)一词最初来源于希腊语polumorphos,含义是具有多种形式或形态的情形。在程序设计领域,一个广泛认可的定义是“一种将不同的特殊行为和单个泛化记号相关联的能力”。和纯粹的面向对象程序设计语言不同,C++中的多态有着更广泛的含义。除了常见的通过类继承和虚函数机制生效于运行期的动态多态(dynamic polymorphism)外,模板也允许将不同的特殊行为和单个泛化记号相关联,由于这种关联处理于编译期而非运行期,因此被称为静态多态(static polymorphism)。
事实上,带变量的宏和函数重载机制也允许将不同的特殊行为和单个泛化记号相关联。然而,习惯上我们并不将它们展现出来的行为称为多态(或静态多态)。今天,当我们谈及多态时,如果没有明确所指,默认就是动态多态,而静态多态则是指基于模板的多态。不过,在这篇以C++各种多态技术为主题的文章中,我们首先还是回顾一下C++社群争论已久的另一种“多态”:函数多态(function polymorphism),以及更不常提的“宏多态(macro polymorphism)”。
函数多态
也就是我们常说的函数重载(function overloading)。基于不同的参数列表,同一个函数名字可以指向不同的函数定义:
// overload_poly.cpp
#include <iostream>
#include <string>
// 定义两个重载函数
int my_add(int a, int b)
{
return a + b;
}
int my_add(int a, std::string b)
{
return a + atoi(b.c_str());
}
int main()
{
int i = my_add(1, 2); // 两个整数相加
int s = my_add(1, "2"); // 一个整数和一个字符串相加
std::cout << "i = " << i << "/n";
std::cout << "s = " << s << "/n";
}
根据参数列表的不同(类型、个数或兼而有之),my_add(1, 2)和my_add(1, "2")被分别编译为对my_add(int, int)和my_add(int, std::string)的调用。实现原理在于编译器根据不同的参数列表对同名函数进行名字重整,而后这些同名函数就变成了彼此不同的函数。比方说,也许某个编译器会将my_add()函数名字分别重整为my_add_int_int()和my_add_int_str()。
宏多态
带变量的宏可以实现一种初级形式的静态多态:
// macro_poly.cpp
#include <iostream>
#include <string>
// 定义泛化记号:宏ADD
#define ADD(A, B) (A) + (B);
int main()
{
int i1(1), i2(2);
std::string s1("Hello, "), s2("world!");
int i = ADD(i1, i2); // 两个整数相加
std::string s = ADD(s1, s2); // 两个字符串“相加”
std::cout << "i = " << i << "/n";
std::cout << "s = " << s << "/n";
}
当程序被编译时,表达式ADD(i1, i2)和ADD(s1, s2)分别被替换为两个整数相加和两个字符串相加的具体表达式。整数相加体现为求和,而字符串相加则体现为连接。程序的输出结果符合直觉:
1 + 2 = 3
Hello, + world! = Hello, world!
动态多态
这就是众所周知的的多态。现代面向对象语言对这个概念的定义是一致的。其技术基础在于继承机制和虚函数。例如,我们可以定义一个抽象基类Vehicle和两个派生于Vehicle的具体类Car和Airplane:
// dynamic_poly.h
#include <iostream>
// 公共抽象基类Vehicle
class Vehicle
{
public:
virtual void run() const = 0;
};
// 派生于Vehicle的具体类Car
class Car: public Vehicle
{
public:
virtual void run() const
{
std::cout << "run a car/n";
}
};
// 派生于Vehicle的具体类Airplane
class Airplane: public Vehicle
{
public:
virtual void run() const
{
std::cout << "run a airplane/n";
}
};
客户程序可以通过指向基类Vehicle的指针(或引用)来操纵具体对象。通过指向基类对象的指针(或引用)来调用一个虚函数,会导致对被指向的具体对象之相应成员的调用:
// dynamic_poly_1.cpp
#include <iostream>
#include <vector>
#include "dynamic_poly.h"
// 通过指针run任何vehicle
void run_vehicle(const Vehicle* vehicle)
{
vehicle->run(); // 根据vehicle的具体类型调用对应的run()
}
int main()
{
Car car;
Airplane airplane;
run_vehicle(&car); // 调用Car::run()
run_vehicle(&airplane); // 调用Airplane::run()
}
此例中,关键的多态接口元素为虚函数run()。由于run_vehicle()的参数为指向基类Vehicle的指针,因而无法在编译期决定使用哪一个版本的run()。在运行期,为了分派函数调用,虚函数被调用的那个对象的完整动态类型将被访问。这样一来,对一个Car对象调用run_vehicle(),实际上将调用Car::run(),而对于Airplane对象而言将调用Airplane::run()。
或许动态多态最吸引人之处在于处理异质对象集合的能力:
// dynamic_poly_2.cpp
#include <iostream>
#include <vector>
#include "dynamic_poly.h"
// run异质vehicles集合
void run_vehicles(const std::vector<Vehicle*>& vehicles)
{
for (unsigned int i = 0; i < vehicles.size(); ++i)
{
vehicles[i]->run(); // 根据具体vehicle的类型调用对应的run()
}
}
int main()
{
Car car;
Airplane airplane;
std::vector<Vehicle*> v; // 异质vehicles集合
v.push_back(&car);
v.push_back(&airplane);
run_vehicles(v); // run不同类型的vehicles
}
在run_vehicles()中,vehicles[i]->run()依据正被迭代的元素的类型而调用不同的成员函数。这从一个侧面体现了面向对象编程风格的优雅。
静态多态
如果说动态多态是通过虚函数来表达共同接口的话,那么静态多态则是通过“彼此单独定义但支持共同操作的具体类”来表达共同性,换句话说,必须存在必需的同名成员函数。
我们可以采用静态多态机制重写上一节的例子。这一次,我们不再定义vehicles类层次结构,相反,我们编写彼此无关的具体类Car和Airplane(它们都有一个run()成员函数):
// static_poly.h
#include <iostream>
//具体类Car
class Car
{
public:
void run() const
{
std::cout << "run a car/n";
}
};
//具体类Airplane
class Airplane
{
public:
void run() const
{
std::cout << "run a airplane/n";
}
};
run_vehicle()应用程序被改写如下:
// static_poly_1.cpp
#include <iostream>
#include <vector>
#include "static_poly.h"
// 通过引用而run任何vehicle
template <typename Vehicle>
void run_vehicle(const Vehicle& vehicle)
{
vehicle.run(); // 根据vehicle的具体类型调用对应的run()
}
int main()
{
Car car;
Airplane airplane;
run_vehicle(car); // 调用Car::run()
run_vehicle(airplane); // 调用Airplane::run()
}
现在Vehicle用作模板参数而非公共基类对象(事实上,这里的Vehicle只是一个符合直觉的记号而已,此外别无它意)。经过编译器处理后,我们最终会得到run_vehicle<Car>()和 run_vehicle<Airplane>()两个不同的函数。这和动态多态不同,动态多态凭借虚函数分派机制在运行期只有一个run_vehicle()函数。
我们无法再透明地处理异质对象集合了,因为所有类型都必须在编译期予以决定。不过,为不同的vehicles引入不同的集合只是举手之劳。由于无需再将集合元素局限于指针或引用,我们现在可以从执行性能和类型安全两方面获得好处:
// static_poly_2.cpp
#include <iostream>
#include <vector>
#include "static_poly.h"
// run同质vehicles集合
template <typename Vehicle>
void run_vehicles(const std::vector<Vehicle>& vehicles)
{
for (unsigned int i = 0; i < vehicles.size(); ++i)
{
vehicles[i].run(); // 根据vehicle的具体类型调用相应的run()
}
}
int main()
{
Car car1, car2;
Airplane airplane1, airplane2;
std::vector<Car> vc; // 同质cars集合
vc.push_back(car1);
vc.push_back(car2);
//vc.push_back(airplane1); // 错误:类型不匹配
run_vehicles(vc); // run cars
std::vector<Airplane> vs; // 同质airplanes集合
vs.push_back(airplane1);
vs.push_back(airplane2);
//vs.push_back(car1); // 错误:类型不匹配
run_vehicles(vs); // run airplanes
}
两种多态机制的结合使用
在一些高级C++应用中,我们可能需要结合使用动态多态和静态多态两种机制,以期达到对象操作的优雅、安全和高效。例如,我们既希望一致而优雅地处理vehicles的run问题,又希望“安全而高效”地完成给飞行器(飞机、飞艇等)进行“空中加油”这样的高难度动作。为此,我们首先将上面的vehicles类层次结构改写如下:
// dscombine_poly.h
#include <iostream>
#include <vector>
// 公共抽象基类Vehicle
class Vehicle
{
public:
virtual void run() const = 0;
};
// 派生于Vehicle的具体类Car
class Car: public Vehicle
{
public:
virtual void run() const
{
std::cout << "run a car/n";
}
};
// 派生于Vehicle的具体类Airplane
class Airplane: public Vehicle
{
public:
virtual void run() const
{
std::cout << "run a airplane/n";
}
void add_oil() const
{
std::cout << "add oil to airplane/n";
}
};
// 派生于Vehicle的具体类Airship
class Airship: public Vehicle
{
public:
virtual void run() const
{
std::cout << "run a airship/n";
}
void add_oil() const
{
std::cout << "add oil to airship/n";
}
};
我们理想中的应用程序可以编写如下:
// dscombine_poly.cpp
#include <iostream>
#include <vector>
#include "dscombine_poly.h"
// run异质vehicles集合
void run_vehicles(const std::vector<Vehicle*>& vehicles)
{
for (unsigned int i = 0; i < vehicles.size(); ++i)
{
vehicles[i]->run(); // 根据具体的vehicle类型调用对应的run()
}
}
// 为某种特定的aircrafts同质对象集合进行“空中加油”
template <typename Aircraft>
void add_oil_to_aircrafts_in_the_sky(const std::vector<Aircraft>& aircrafts)
{
for (unsigned int i = 0; i < aircrafts.size(); ++i)
{
aircrafts[i].add_oil();
}
}
int main()
{
Car car1, car2;
Airplane airplane1, airplane2;
Airship airship1, airship2;
std::vector<Vehicle*> v; // 异质vehicles集合
v.push_back(&car1);
v.push_back(&airplane1);
v.push_back(&airship1);
run_vehicles(v); // run不同种类的vehicles
std::vector<Airplane> vp; // 同质airplanes集合
vp.push_back(airplane1);
vp.push_back(airplane2);
add_oil_to_aircrafts_in_the_sky(vp); // 为airplanes进行“空中加油”
std::vector<Airship> vs; // 同质airships集合
vs.push_back(airship1);
vs.push_back(airship2);
add_oil_to_aircrafts_in_the_sky(vs); // 为airships进行“空中加油”
}
我们保留了类层次结构,目的是为了能够利用run_vehicles()一致而优雅地处理异质对象集合vehicles的run问题。同时,利用函数模板add_oil_to_aircrafts_in_the_sky<Aircraft>(),我们仍然可以处理特定种类的vehicles — aircrafts(包括airplanes和airships)的“空中加油”问题。其中,我们避开使用指针,从而在执行性能和类型安全两方面达到了预期目标。
结语
长期以来,C++社群对于多态的内涵和外延一直争论不休。在comp.object这样的网络论坛上,此类话题争论至今仍随处可见。曾经有人将动态多态(dynamic polymorphism)称为inclusion polymorphism,而将静态多态(static polymorphism)称为parametric polymorphism或parameterized polymorphism。
我注意到2003年斯坦福大学公开的一份C++ and Object-Oriented Programming教案中明确提到了函数多态概念:Function overloading is also referred to as function polymorphism as it involves one function having many forms。文后的“参考文献”单元给出了这个网页链接。
可能你是第一次看到宏多态(macro polymorphism)这个术语。不必讶异 — 也许我就是造出这个术语的“第一人”。显然,带变量的宏(或类似于函数的宏或伪函数宏)的替换机制除了免除小型函数的调用开销之外,也表现出了类似的多态性。在我们上面的例子中,字符串相加所表现出来的符合直觉的连接操作,事实上是由底部运算符重载机制(operator overloading)支持的。值得指出的是,C++社群中有人将运算符重载所表现出来的多态称为ad hoc polymorphism。
David Vandevoorde和Nicolai M. Josuttis在他们的著作C++ Templates: The Complete Guide一书中系统地阐述了静态多态和动态多态技术。因为认为“和其他语言机制关系不大”,这本书没有提及“宏多态”(以及“函数多态”)。(需要说明的是,笔者本人是这本书的繁体中文版译者之一,本文正是基于这本书的第14章The Polymorphic Power of Templates编写而成)
动态多态只需要一个多态函数,生成的可执行代码尺寸较小,静态多态必须针对不同的类型产生不同的模板实体,尺寸会大一些,但生成的代码会更快,因为无需通过指针进行间接操作。静态多态比动态多态更加类型安全,因为全部绑定都被检查于编译期。正如前面例子所示,你不可将一个错误的类型的对象插入到从一个模板实例化而来的容器之中。此外,正如你已经看到的那样,动态多态可以优雅地处理异质对象集合,而静态多态可以用来实现安全、高效的同质对象集合操作。
静态多态为C++带来了泛型编程(generic programming)的概念。泛型编程可以认为是“组件功能基于框架整体而设计”的模板编程。STL就是泛型编程的一个典范。STL是一个框架,它提供了大量的算法、容器和迭代器,全部以模板技术实现。从理论上讲,STL的功能当然可以使用动态多态来实现,不过这样一来其性能必将大打折扣。
静态多态还为C++社群带来了泛型模式(generic patterns)的概念。理论上,每一个需要通过虚函数和类继承而支持的设计模式都可以利用基于模板的静态多态技术(甚至可以结合使用动态多态和静态多态两种技术)而实现。正如你看到的那样,Andrei Alexandrescu的天才作品Modern C++ Design: Generic Programming and Design Patterns Applied(Addison-Wesley)和Loki程序库已经走在了我们的前面。
参考文献
1. David Vandevoorde, Nicolai M. Josuttis, C++ Templates: The Complete Guide, Addison Wesley, 2002.
2. Chris Neumann, CS193d (Summer 2003) C++ and Object-Oriented Programming, http://www.stanford.edu/class/cs193d/, 2003.
第二章 虚函数和虚函数表
事实上在侯杰的《深入浅出MFC》一书第二章,对虚函数和虚函数表有深入的剖析。
每个含有虚函数的类有一张虚函数表(vtbl),表中每一项指向一个虚函数的地址,实现上是一个函数指针的数组。
虚函数表既有继承性又有多态性。每个派生类的vtbl继承了它各个基类的vtbl,如果基类vtbl中包含某一项,则其派生类的vtbl中也将包含同样的一项,但是两项的值可能不同。如果派生类重载(override)了该项对应的虚函数,则派生类vtbl的该项指向重载后的虚函数,没有重载的话,则沿用基类的值。
在类对象的内存布局中,首先是该类的vtbl指针,然后才是对象数据。在通过对象指针调用一个虚函数时,编译器生成的代码将先获取对象类的vtbl指针,然后调用vtbl中对应的项。对于通过对象指针调用的情况,在编译期间无法确定指针指向的是基类对象还是派生类对象,或者是哪个派生类的对象。但是在运行期间执行到调用语句时,这一点已经确定,编译后的调用代码能够根据具体对象获取正确的vtbl,调用正确的虚函数,从而实现多态性。分析一下这里的思想所在,问题的实质是这样,对于发出虚函数调用的这个对象指针,在编译期间缺乏更多的信息,而在运行期间具备足够的信息,但那时已不再进行绑定了,怎么在二者之间作一个过渡呢?把绑定所需的信息用一种通用的数据结构记录下来,该数据结构可以同对象指针相联系,在编译时只需要使用这个数据结构进行抽象的绑定,而在运行期间将会得到真正的绑定。这个数据结构就是vtbl。可以看到,实现用户所需的抽象和多态需要进行后绑定,而编译器又是通过抽象和多态而实现后绑定的。
下面说一下多重继承。多重继承的两个基类如果继承了同一个类,则其派生类相当于继承了该类两次,vtbl也继承了两次。对象布局中,该类的数据有两份,vtbl指针有两个,分别指向两次被继承的vtbl。但派生类重载该类的虚函数时只能重载一次,那么重载后的函数地址将占据vtbl的哪个位置?通过写程序测试,我觉得应该是同时出现在所继承的两个vtbl的相应位置,有待进一步验证。
说到虚函数机制,对象指针的类型转换也是要弄清的,这里就不说了。还有一个this指针的问题,提一下。虚函数调用的时候也是需要传递this指针的,这没什么奇怪,但是这时的this指针就隐含着一个问题,它要和实际调用的虚函数相一致,即this指针也要实现多态性。