程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C++多態技術

C++多態技術

編輯:關於C++

摘要

本文描述了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. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved