淺談多態

靜態多態;動態多態;函數重載

多態(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
}
動態多態只需要一個多態函數,生成的可執行代碼尺寸較小,靜態多態必須針對不同的類型產生不同的模板實體,尺寸會大一些,但生成的代碼會更快,因爲無需通過指針進行間接操作。靜態多態比動態多態更加類型安全,因爲全部綁定都被檢查於編譯期。正如前面例子所示,你不可將一個錯誤的類型的對象插入到從一個模板實例化而來的容器之中。此外,正如你已經看到的那樣,動態多態可以優雅地處理異質對象集合,而靜態多態可以用來實現安全、高效的同質對象集合操作。  

發佈了26 篇原創文章 · 獲贊 3 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章