被誤解的C++——漢尼拔
by 莫華楓
公元前216年8月2日,意大利東部平原,一個叫做坎尼的地方,兩支大軍擺開陣勢,準(zhǔn)備決一死戰(zhàn)。一方是由保羅斯和瓦羅兩位執(zhí)政官率領(lǐng)的羅馬人,另一方則是偉大的軍事天才漢尼拔*巴卡率領(lǐng)的迦太基軍隊(duì)及其同盟。羅馬人超過8萬,而迦太基僅有4萬余人。然而到了傍晚,羅馬人被徹底擊敗,7萬人被殺,僅有少數(shù)得以逃脫。這就是著名的坎尼會戰(zhàn)。經(jīng)此一役,(外加先前進(jìn)行的特利比亞和特拉西梅諾湖會戰(zhàn)),羅馬人元?dú)獯髠?,成年公民損失達(dá)五分之一。部分城邦背叛羅馬,西西里也發(fā)生起義。羅馬已經(jīng)到了搖搖欲墜的地步。
漢尼拔的這些勝利,完全得益于先前的一次異乎尋常的遠(yuǎn)征。公元前218年,漢尼拔率領(lǐng)軍隊(duì),從新迦太基城(西班牙)出發(fā),翻越比利牛斯山,進(jìn)入南高盧地域。在他面前有兩條路可走,翻越阿爾俾斯山,或者沿海岸進(jìn)入意大利。但是,當(dāng)時羅馬人已在沿海地區(qū)部署了兩支部隊(duì),準(zhǔn)備攔截漢尼拔。而且,羅馬人的海軍優(yōu)勢,使得他們可以在任何時候?qū)⒁恢Р筷?duì)登陸在他的背后。而翻越阿爾俾斯山,則是一條及其艱險的道路,更何況是在冬天。
漢尼拔選擇了阿爾俾斯山。他甩開了羅馬人,從小圣貝納德和日內(nèi)瓦山之間越過阿爾俾斯山,進(jìn)入意大利境內(nèi)。此時,羅馬人便失去了戰(zhàn)略縱深,一把尖刀已經(jīng)深深地插入他們的腹內(nèi)...
C++的發(fā)展史上,也有著如同漢尼拔翻越阿爾俾斯山遠(yuǎn)征。一切還得從C with Class時代說起。
Bjarne曾經(jīng)反復(fù)強(qiáng)調(diào),他創(chuàng)建C++為的是將Simular的抽象能力同C的性能結(jié)合起來。于是,在C語言的基礎(chǔ)上,誕生了一種擁有類、繼承、重載等等面向?qū)ο髾C(jī)制的語言。在這個階段,C++提供了兩個方面的抽象能力。一種是數(shù)據(jù)抽象,也就是將數(shù)據(jù)所要表達(dá)的含義通過類型以及依附于類型上的成員表述。另一種則是多態(tài),一種最原始的多態(tài)(重載)。
數(shù)據(jù)抽象,通過被稱為“抽象數(shù)據(jù)類型(ADT)”的技術(shù)實(shí)現(xiàn)。ADT的一種方案,就是類。類所提供的封裝性將一個數(shù)據(jù)實(shí)體的外在特征,或者說語義的表述形式,同具體的實(shí)現(xiàn),比如數(shù)據(jù)存儲形式,分離。這樣所增加的中間層將數(shù)據(jù)的使用者同數(shù)據(jù)的實(shí)現(xiàn)者隔離,使得他們使用共同的約定語義工作,不再相互了解彼此的細(xì)節(jié),從而使得兩者得以解耦。
多態(tài)則是更加基礎(chǔ)更加重要的一種特性。多態(tài)使得我們得以用同一種符號實(shí)現(xiàn)某種確定的語義。多態(tài)的精髓在于:以一種形式表達(dá)一種語義。在此之前,我們往往被迫使用不同的符號來代表同一種抽象語義,為的是適應(yīng)強(qiáng)類型系統(tǒng)所施加的約束。比如:
//代碼#1
int add_int(int lhs, int rhs);
float add_float(float lhs, float rhs);
很顯然,這兩個函數(shù)表達(dá)的語義分別是“把兩個int類型值加在一起”和“把兩個float類型值加在一起”。這兩個語義抽象起來都表達(dá)了一個意思:加。
我們在做算術(shù)題的時候是不會管被計(jì)算的數(shù)字是整數(shù)還是實(shí)數(shù)。同樣,如果能夠在編程的時候,不考慮算術(shù)操作對象的類型,只需關(guān)心誰和誰進(jìn)行什么操作,那么會方便得多。當(dāng)C++引入重載后,這種愿望便得以實(shí)現(xiàn):
//代碼#2
int add(int lhs, int rhs);
float add(float lhs, float rhs);
重載使得我們只需關(guān)心“加”這個語義,至于什么類型和什么類型相加,則由編譯器根據(jù)操作數(shù)的類型自動解析。
從某種意義上說,重載是被長期忽視,但卻極為重要的一個語言特性。在多數(shù)介紹OOP的書籍中,重載往往被作為OOP的附屬品,放在一些不起眼的地方。它的多態(tài)本質(zhì)也被動多態(tài)的人造光環(huán)所設(shè)遮蔽。然而,重載的重要作用卻在實(shí)踐中潛移默化地體現(xiàn)出來。重載差不多可以看作語言邁入現(xiàn)代抽象體系的第一步。它的實(shí)際效用甚至要超過被廣為關(guān)注的OOP,而不會像OOP那樣在獲得抽象的同時,伴隨著不小的副作用。
隨著虛函數(shù)的引入,C++開始具備了頗具爭議的動多態(tài)技術(shù)。虛函數(shù)是一種依附于類(OOP的類型基礎(chǔ))的多態(tài)技術(shù)。其技術(shù)基礎(chǔ)是后期綁定(late-binding)。當(dāng)一個類D繼承自類B時,它有兩種方法覆蓋(override)B上的某個函數(shù):
//代碼#3
class B
{
public:
void fun1();
virtual void fun2();
};
class D:public B
{
public:
void fun1();
void fun2();
};
當(dāng)繼承類D中聲明了同基類B中成員函數(shù)相同函數(shù)名、相同簽名的成員函數(shù),那么基類的成員函數(shù)將被覆蓋。對于基類的非虛成員函數(shù),繼承類會直接將其遮蔽。對于類型D的使用者,fun1代表了D中所賦予的語義。而類型D的實(shí)例,可以隱式地轉(zhuǎn)換成類型B的引用b,此時調(diào)用b的fun1,則執(zhí)行的是類B的fun1 定義,而非類D的fun1,盡管此時b實(shí)際指向一個D的實(shí)例。
但是,如果繼承類覆蓋了基類的虛函數(shù),那么將得到相反的結(jié)果:當(dāng)調(diào)用引用b的fun2,實(shí)際上卻是調(diào)用了D的fun2定義。這表明,覆蓋一個虛函數(shù),將會在繼承類和基類之間的所有層次上執(zhí)行覆蓋。這種徹底的、全方位的覆蓋行為,使得我們可以在繼承類上修飾或擴(kuò)展基類的功能或行為。這便是OOP擴(kuò)展機(jī)制的基礎(chǔ)。而這種技術(shù)被稱為動多態(tài),意思是基類引用所表達(dá)的語義并非取決于基類本身,而是來源于它所指向的實(shí)際對象,因此它是“多態(tài)”的。因?yàn)橐粋€引用所指向的對象可以在運(yùn)行時變換,所以它是“動”的。
隨著動多態(tài)而來的一個“副產(chǎn)品”,卻事實(shí)上成為了OOP的核心和支柱。虛函數(shù)的“動多態(tài)”特性將我們引向一個極端的情況:一個都是虛函數(shù)的類。更重要的,這個類上的虛函數(shù)都沒有實(shí)現(xiàn),每個虛函數(shù)都未曾指向一個實(shí)實(shí)在在的函數(shù)體。當(dāng)然,這樣的類是無法直接使用的。有趣的是,這種被稱為“抽象基類”的類,迫使我們繼承它,并“替它”實(shí)現(xiàn)那些沒有實(shí)現(xiàn)的虛函數(shù)。這樣,對于一個抽象基類的引用,多態(tài)地?fù)碛辛死^承類的行為。而反過來,抽象基類實(shí)際上起到了強(qiáng)迫繼承類實(shí)現(xiàn)某些特定功能的作用。因此,抽象基類扮演了接口的角色。接口具有兩重作用:一、約束繼承類(實(shí)現(xiàn)者)迫使其實(shí)現(xiàn)預(yù)定的成員函數(shù)(功能和行為);二、描述了繼承類必定擁有的成員函數(shù)(功能和行為)。這兩種作用促使接口成為了OOP設(shè)計(jì)體系的支柱。
C++在這方面的進(jìn)步,使其成為一個真正意義上具備現(xiàn)代抽象能力的語言。然而,這種進(jìn)步并非“翻越阿爾俾斯山”。充其量也只能算作“翻越比利牛斯山”。對于C++而言,真正艱苦的遠(yuǎn)征才剛開始,那令人生畏的“阿爾俾斯山”仍在遙遠(yuǎn)的前方。
同漢尼拔一樣,當(dāng)C++一腳邁入“現(xiàn)代抽象語言俱樂部”后,便面臨兩種選擇?;蛘咴谠谢A(chǔ)上修修補(bǔ)補(bǔ),成為一種OOP語言;或者繼續(xù)前進(jìn),翻越那座險峻的山峰。C++的漢尼拔——Bjarne Stroustrup——選擇了后者。
從D&E的描述中我們可以看到,在C++的原始設(shè)計(jì)中就已經(jīng)考慮“類型參數(shù)”的問題。但直到90年代初,才真正意義上地實(shí)現(xiàn)了模板。然而,模板只是第一步。諸如Ada等語言中都有類似的機(jī)制(泛型,generic),但并未對當(dāng)時的編程技術(shù)產(chǎn)生根本性的影響。
關(guān)鍵性的成果來源于Alex Stepanov的貢獻(xiàn)。Stepanov在后來被稱為stl的算法-容器庫上所做的努力,使得一種新興的編程技術(shù)——泛型編程(Generic Programming,GP)——進(jìn)入了人們的視野。stl的產(chǎn)生對C++的模板機(jī)制產(chǎn)生了極其重要的影響,促使了模板特化的誕生。模板特化表面上是模板的輔助特性,但是實(shí)際上它卻是比“類型參數(shù)”更加本質(zhì)的機(jī)能。
假設(shè)我們有一組函數(shù)執(zhí)行比較兩個對象大小的操作:
//代碼#4
int compare(int lhs, int rhs);
int compare(float lhs, float rhs);
int compare(string lhs, string rhs);
重載使得我們可以僅用compare一個函數(shù)名執(zhí)行不同類型的比較操作。但是這些函數(shù)具有一樣的實(shí)現(xiàn)代碼。模板的引入,使得我們可以消除這種重復(fù)代碼:
//代碼#5
template<typename T> int compare(T lhs, T rhs) {
if(lhs==rhs)
return 0;
if(lhs>rhs)
return 1;
if(lhs<rhs)
return -1;
}
這樣一個模板可以應(yīng)用于任何類型,不但用一個符號表達(dá)了一個語義,而且用一個實(shí)現(xiàn)代替了諸多重復(fù)代碼。這便是GP的基本作用。
接下來的變化,可以算作真正意義上的“登山”了。
如果有兩個指針,分別指向兩個相同類型的對象。此時如果我們采用上述compare函數(shù)模板,那么將無法得到所需的結(jié)果。因?yàn)榇藭r比較的是兩個指針的值,而不是所指向的對象本身。為了應(yīng)付這種特殊情況,我們需要對compare做“特別處理”:
//代碼#6
template<typename T> int compare(T* lhs, T* rhs) {
if(*lhs==*rhs)
return 0;
if(*lhs>*rhs)
return 1;
if(*lhs<*rhs)
return -1;
}
這個“特殊版本”的compare,對于任何類型的指針作出響應(yīng)。如果調(diào)用時的實(shí)參是一個指針,那么這個“指針版”的compare將會得到優(yōu)先匹配。如果我們將compare改成下面的實(shí)現(xiàn),那么就會出現(xiàn)非常有趣的行為:
//代碼#7
template<typename T>
struct comp_impl
{
int operator()(T lhs, T rhs) {
if(lhs==rhs)
return 0;
if(lhs>rhs)
return 1;
if(lhs<rhs)
return -1;
}
};
template<typename T>
struct comp_impl<T*>
{
int operator()(T* lhs, T* rhs) {
comp_impl<T>()(*lhs, *rhs);
}
};
template<typename T> int compare(T* lhs, T* rhs) {
comp_impl<T>()(*lhs, *rhs);
}
當(dāng)我們將指針的指針作為實(shí)參,調(diào)用compare時,神奇的事情發(fā)生了:
//代碼#8
double **x, **y;
compare(x, y);
compare居然成功地剝離了兩個指針,并且正確地比較了兩個對象的值。這個戲法充分利用了類模板的局部特化和特化解析規(guī)則。根據(jù)規(guī)則,越是特化的模板,越是優(yōu)先匹配。T*版的comp_impl比T版的更加“特化”,會得到優(yōu)先匹配。那么當(dāng)一個指針的指針實(shí)例化comp_impl,則會匹配T*版的 comp_impl,因?yàn)橹羔樀闹羔槪彩侵羔槨*版通過局部特化機(jī)制,剝離掉一級指針,然后用所得的類型實(shí)例化comp_impl。指針的指針剝離掉一級指針,那么還是一個指針,又會匹配T*版。T*版又會剝離掉一級指針,剩下的就是真正可以比較的類型——double。此時,double已無法與 T*版本匹配,只能匹配基礎(chǔ)模板,執(zhí)行真正的比較操作。
這種奇妙的手法是蘊(yùn)含在模板特化中一些更加本質(zhì)的機(jī)制的結(jié)果。這種意外獲得的“模板衍生產(chǎn)品”可以算作一種編譯時計(jì)算的能力,后來被一些“好事者”發(fā)展成獨(dú)立的“模板元編程”(Template Meta Programming,TMP)。
盡管TMP新奇而又奧妙,但終究只是一種輔助技術(shù),用來彌補(bǔ)C++的一些缺陷、做一些擴(kuò)展,“撿個漏”什么的。不過它為我們帶來了兩點(diǎn)重要的啟示:一、我們有可能通過語言本身的一些機(jī)制,進(jìn)行元編程;二、元編程在一定程度上可以同通用語言一起使用。這些啟示對編程語言的發(fā)展有很好的指導(dǎo)意義。
模板及特化規(guī)則是C++ GP的核心所在。這些語言特性的強(qiáng)大能力并非憑空而來。實(shí)際上有一只“幕后大手”在冥冥之中操縱著一切。
假設(shè)有一個類型系統(tǒng),包含n個類型:t1,...,tn,那么這些類型構(gòu)成了一個集合T={t1,...,tn}。在當(dāng)我們運(yùn)用重載技術(shù)時,實(shí)際上構(gòu)造了一組類型的tuple到函數(shù)實(shí)現(xiàn)的映射:<ti1,ti2,ti3,...> ->fj()。編譯器在重載解析的時候,就是按照這組映射尋找匹配的函數(shù)版本。當(dāng)我們編寫了形如代碼#5的模板,那么就相當(dāng)于構(gòu)建了映射:<T, T,...> ->f0()。
而代碼#6,以及代碼#7中的T*版模板,實(shí)際上是構(gòu)造了一個<Tp>->fp()的映射。這里Tp是T的一個子集:Tp={t'|t'=ti*, ti∈T}。換句話說,特化使泛型體系細(xì)化了。利用模板特化技術(shù),我們已經(jīng)能夠(笨拙地)分辨浮點(diǎn)數(shù)、整數(shù)、內(nèi)置類型、內(nèi)置數(shù)組、類、枚舉等等類型。具備為類型劃分的能力,也就是構(gòu)造不同的類型子集的能力。
現(xiàn)在,我們便可以構(gòu)造一個“泛型體系”:G={T} U T U Tp U Ta U Ti U Tf U Tc ...。其中,Tp是所有指針類型,Ta是數(shù)組,Ti是整數(shù),Tf是浮點(diǎn)數(shù),Tc是類等等。但是如果我們按照泛化程度,把G中的元素排列開:{T, Tp, Ta, Ti,...,t1,...,tn}。我們會發(fā)現(xiàn)這中間存在一些“斷層”。這些斷層位于T和Tp等之間,以及Tp等與ti等之間等等。這表明在C++98/03中,抽象體系不夠完整,存在缺陷。
所以,到目前為止,C++還沒有真正翻越阿爾俾斯山里那座最險峻的山峰。這正是C++0x正在努力做的,而且勝利在望。
在C++0x中,大牛們引入了first-class的concept支持。concept目前還沒有正式的法定描述(以及合理的中文翻譯)。通俗地講,concept描述了一個類型的(接口)特征。說具體的,一個concept描述了類型必須具備的公共成員函數(shù),必須具備的施加在該類型上的自由函數(shù)(和操作符),以及必須具備的其他特征(通過間接手段)。下面是一個典型的concept:
concept has_equal<T>
{
bool T::equal(T const& v);
};
這個concept告訴我們它所描述的類型必須有一個equal成員,以另一個同類型的對象為參數(shù)。當(dāng)我們將這個concept施加在一個函數(shù)模板上,并作為對類型參數(shù)的約束,那么就表明了這個模板對類型參數(shù)的要求:
template<has_equal T>bool is_equal(T& lhs, T const& rhs) {
return lhs.equal(rhs);
}
如果實(shí)參對象的類型沒有equal成員,那么is_equal將會拒絕編譯通過:這不是我要的!
concept是可以組合的,正式的術(shù)語叫做“refine”。我們可以通過refine進(jìn)一步構(gòu)造出約束更強(qiáng)的concept:
concept my_concept<T> : has_equal<T>, DefaultConstructable<T>, Swappable<T> {}
refine獲得的concept將會“繼承”那些“基concept”的所有約束。作為更細(xì)致的組合手段,concept還可以通過!操作符“去掉”某些內(nèi)涵的concept約束:
concept my_concept1<T> : has_equal<T>, !DefaultConstructable<T> {}
這個concept要求類型具備equal成員,但不能有默認(rèn)構(gòu)造函數(shù)。
通過這些手段,concept可以“無限細(xì)分”類型集合T。理論上,我們可以制造一連串只相差一個函數(shù)或者只相差一個參數(shù)的concept。
一個concept實(shí)際上就是構(gòu)成類型集合T的劃分的約束:Tx={ti| Cx(ti)==true, ti∈T}。其中Cx就是concept所構(gòu)造的約束。不同的concept有著不同范圍的約束。這樣,理論上我們可以運(yùn)用concept枚舉出類型集合 T的所有子集。而這些子集則正好填補(bǔ)了上述G中的那些斷層。換句話說,concept細(xì)化了類型劃分的粒度,或者說泛型的粒度。使得“離散”的泛型系統(tǒng)變成“連續(xù)”的。
當(dāng)我們運(yùn)用concept約束一個函數(shù)模板的類型參數(shù)時,相當(dāng)于用concept所描述的類型子集構(gòu)建一個映射:<Tx1,Tx2,...>->fx()。凡是符合tuple <Tx1,Tx2,...>的類型組合,對應(yīng)fx()。所以,從這個角度而言,函數(shù)模板的特化(包括concept)可以看作函數(shù)重載的一種擴(kuò)展。在concept的促進(jìn)下,我們便可以把函數(shù)模板特化和函數(shù)重載統(tǒng)一在一個體系下處理,使用共同的規(guī)則解析。
在目前階段,C++差不多已經(jīng)登上了“抽象阿爾俾斯山”的頂峰。但是就如同漢尼拔進(jìn)入意大利后,還需要面對強(qiáng)盛的羅馬共和國,與之作戰(zhàn)那樣。C++的面前還需要進(jìn)一步將優(yōu)勢化作勝利。要做的事還很多,其中最重要的,當(dāng)屬構(gòu)建Runtime GP。目前C++的GP是編譯時機(jī)制。對于運(yùn)行時決斷的任務(wù),還需要求助于OOP的動多態(tài)。但是C++領(lǐng)域的大牛們已經(jīng)著手在Runtime GP和Runtime Concept等方面展開努力。這方面的最新成果可以看這里、這里和這里。相信經(jīng)過若干年的努力后,GP將會完全的成熟,成為真正主流的編程技術(shù)。
坎尼會戰(zhàn)之后,漢尼拔已經(jīng)擁有了絕對的優(yōu)勢。羅馬人已經(jīng)戰(zhàn)敗,他同羅馬城之間已經(jīng)沒有任何強(qiáng)大的敵對力量,羅馬人也已經(jīng)聞風(fēng)喪膽,幾無斗志。但是,漢尼拔卻犯下了或許令他一生后悔的錯誤。他放過了羅馬城,轉(zhuǎn)而攻擊羅馬的南部城邦和同盟。他低估了羅馬人的意志,以及羅馬同盟的牢固程度。羅馬人很快結(jié)束了最初的混亂,任命了新的執(zhí)政官,采用了堅(jiān)壁清野、以柔克剛的新戰(zhàn)略。隨著時間的推移,漢尼拔和他的軍隊(duì)限于孤立無援的境地,被迫為了生存而作戰(zhàn)。盡管迫降并占領(lǐng)了幾個羅馬城市,但是終究無法再次獲得給予羅馬人致命一擊的機(jī)會。
漢尼拔的戰(zhàn)略錯誤實(shí)際上在從新迦太基城出發(fā)之前已經(jīng)注定。因?yàn)闈h尼拔對羅馬人的遠(yuǎn)征的根本目的并非擊潰并占領(lǐng)羅馬,而是通過打擊羅馬,削弱他們的勢力,瓦解他們的聯(lián)盟。以達(dá)到尋求簽訂和平協(xié)議的目的。然而這種有限戰(zhàn)略卻使導(dǎo)致了他的最終失敗。
不幸的是,C++或多或少地有著同漢尼拔一樣的戰(zhàn)略錯誤。C++最初的目的基本上僅僅局限于“更好的C”。并且全面兼容C。這在當(dāng)時似乎很合理,因?yàn)镃可以算作最成功的“底層高級語言”,擁有很高的性能和靈活性。但是,C的設(shè)計(jì)并未考慮將來會有一個叫做“C++”的語言來對其進(jìn)行擴(kuò)展。結(jié)果很多優(yōu)點(diǎn)對于C而言是優(yōu)點(diǎn),但卻成了C++的負(fù)擔(dān)。比如,C大量使用操作符表達(dá)語法結(jié)構(gòu),對于C而言顯得非常簡潔,但對于C++,則使其被迫大規(guī)模復(fù)用操作符,為其后出現(xiàn)的很多語法缺陷埋下了伏筆。這一點(diǎn)上,Ada做得相對成熟些。它從Pascal那里繼承了主要語法,但不考慮兼容。這使得Ada更加完整,易于發(fā)展。新語言就是新語言,過分的兼容是鐐銬,不是優(yōu)勢。而且,合理地繼承語法,同樣可以吸引眾多開發(fā)者。從經(jīng)驗(yàn)來看,程序員對于語法變化的承受能力還是很強(qiáng)的。他們更多地關(guān)心語言的功能和易用性。
另一方面,C++最初并未把目標(biāo)定在“創(chuàng)建一種高度抽象,又確保性能的語言”??v觀C++的發(fā)展,各種抽象機(jī)制并非在完整的規(guī)劃或路線圖的指導(dǎo)下加入語言。所有高級特性都是以“添油戰(zhàn)術(shù)”零打碎敲地加入語言。從某種程度上來看,C++更像是一種實(shí)驗(yàn)性語言,而非工業(yè)語言。C++的強(qiáng)大功能和優(yōu)點(diǎn)是長期積累獲得的,而它的諸多缺陷也是長期添加特性的結(jié)果。
漢尼拔和C++給予我們一個很好的教訓(xùn)。對于一個試圖在1、20年后依然健康成長的語言,那么就必須在最初便擁有明確的目標(biāo)和技術(shù)發(fā)展規(guī)劃。對于以往的語言特性應(yīng)當(dāng)擇優(yōu)而取,不能照單全收。并且在技術(shù)上擁有足夠的前瞻性。我們知道,技術(shù)前瞻性是很難做到的,畢竟技術(shù)發(fā)展太快。如果做不到,那就得有足夠的魄力對過去的東西加以取舍。所謂“舍小就大,棄子爭先”。
總體而言,C++在抽象機(jī)制的發(fā)展方面,還算是成功的。盡管伴隨著不少技術(shù)缺陷,但是C++的抽象能力在各種語言中可稱得上出類拔萃。而且C++還在發(fā)展,它未來將會發(fā)展成什么形態(tài),不得而知。但是,無論C++是繼續(xù)修修補(bǔ)補(bǔ),還是根本性地變革,它的抽象能力都會不折不扣地保留,并且不斷完善和增強(qiáng)。
坎尼會戰(zhàn)之后,漢尼拔又打過幾次小規(guī)模的勝仗。但經(jīng)過長期的作戰(zhàn),也得不到迦太基的支援,漢尼拔的力量越來越弱,只能在意大利半島上勉強(qiáng)生存。羅馬很快恢復(fù)了元?dú)猓母锪塑娛麦w系和作戰(zhàn)方式,重新掌握了戰(zhàn)略主動權(quán)。更重要的是,羅馬也有了自己的“漢尼拔”——(征服非洲的)普布利烏斯·科爾內(nèi)利烏斯·西庇阿(大西庇阿)。西庇阿被派往北非大陸,直接攻擊迦太基人的老巢。漢尼拔被召回,在扎馬與西庇阿擺開陣勢,展開一場決戰(zhàn)。最終,西庇阿運(yùn)用從漢尼拔那里學(xué)到的戰(zhàn)術(shù)擊潰了迦太基人,為羅馬人贏得了第二次布匿戰(zhàn)爭的勝利。
此后,漢尼拔在羅馬人的通緝之下,流亡于地中海沿岸,試圖尋求東山再起的機(jī)會。但最終未能如愿,被迫于公元前183年自盡,享年64歲。有趣的是,他的老對手,小他12歲的西庇阿也于同年去世。一個偉大的傳奇就此結(jié)束。