明輝手游網(wǎng)中心:是一個(gè)免費(fèi)提供流行視頻軟件教程、在線學(xué)習(xí)分享的學(xué)習(xí)平臺(tái)!

什么是面向?qū)ο缶幊蹋?/h1>

[摘要]譯者序不要將本文簡(jiǎn)單地視為是對(duì)C++特征的一個(gè)介紹。它的意義在于,一方面介紹了編程風(fēng)格的演變,以及這種演變背后的動(dòng)機(jī)。另一個(gè)方面,它特別澄清了基于對(duì)象的(OB)和面向?qū)ο螅∣O)的異同,這是具有很大...
譯者序
不要將本文簡(jiǎn)單地視為是對(duì)C++特征的一個(gè)介紹。它的意義在于,一方面介紹了編程風(fēng)格的演變,以及這種演變背后的動(dòng)機(jī)
。另一個(gè)方面,它特別澄清了基于對(duì)象的(OB)和面向?qū)ο螅∣O)的異同,這是具有很大意義的。我們可以看到,
不管是OB還是OO,都不過(guò)是一種程序的組織形式。 這在很大程序上指出了OO著眼于解決什么樣的問(wèn)題
(程序如何組織才能有彈性,容易重用和理解),而不解決什么問(wèn)題(數(shù)據(jù)結(jié)構(gòu)的設(shè)計(jì),算法的設(shè)計(jì))等等。

摘要
“面向?qū)ο缶幊獭焙汀皵?shù)據(jù)抽象”已經(jīng)成為常用的編程術(shù)語(yǔ),然而,很少有人能夠就它們的含義取得一致的認(rèn)識(shí);本文以Ada,C++,Module 2,Simula和Smalltalk等語(yǔ)言為背景對(duì)此給出一個(gè)非正式的定義。基本的想法是將“支持?jǐn)?shù)據(jù)抽象”等同于定義和使用新數(shù)據(jù)類型的能力,而將“支持面向?qū)ο缶幊獭钡韧趯?duì)類層次的表達(dá)能力。同時(shí),還討論了通用編程語(yǔ)言為支持此種編程風(fēng)格而必須提供的機(jī)制。文中雖然采用C++來(lái)表述問(wèn)題,但其討論的范圍并不僅限于這個(gè)語(yǔ)言。

1 介紹
并不是所有的語(yǔ)言都是面向?qū)ο蟮摹R话阏J(rèn)為,APL,Ada,Clu,C++,LOOPS和Smalltalk是面向?qū)ο蟮模乙苍?jīng)聽(tīng)說(shuō)過(guò)關(guān)于使用C, Pascal,Module-2,和CHILL進(jìn)行面向?qū)ο笤O(shè)計(jì)的討論。那么是否可以嘗試使用Fortran和Cobol來(lái)進(jìn)行面向?qū)ο笤O(shè)計(jì)呢?我認(rèn)為那也一定是可行的。在很多圈子里,“面向?qū)ο蟆币呀?jīng)成為“優(yōu)秀”的高科技代名詞,在商業(yè)出版領(lǐng)域可以看到有以下的三段論:
Ada是優(yōu)秀的
面向?qū)ο笫莾?yōu)秀的
所以Ada是面向?qū)ο蟮?br>本文從通用編程語(yǔ)言的角度出發(fā)陳述了“面向?qū)ο蟆奔夹g(shù)的概貌:
第2節(jié)比較了數(shù)據(jù)抽象和面向?qū)ο笾g的異同,也將它們和其他的編程風(fēng)格做了區(qū)分;同時(shí),指出了為了支持不同的編程風(fēng)格所需的重要機(jī)制。
第3節(jié)陳述了為高效地支持?jǐn)?shù)據(jù)抽象所需的語(yǔ)言機(jī)制。
第4節(jié)討論了支持面向?qū)ο笏璧脑O(shè)施。
第5節(jié)陳述了傳統(tǒng)硬件體系結(jié)構(gòu)和操作系統(tǒng)對(duì)于數(shù)據(jù)抽象和面向?qū)ο缶幊淌┘拥南拗啤?br>
文中例子程序使用C++來(lái)書寫,這部分是出于介紹C++的目的,部分是因?yàn)镃++是少數(shù)幾個(gè)同時(shí)支持?jǐn)?shù)據(jù)抽象,面向?qū)ο蟪绦蛟O(shè)計(jì)和傳統(tǒng)編程風(fēng)格的語(yǔ)言。本文不討論為支持特定高層語(yǔ)言特性而涉及的并發(fā)性和特殊硬件支持。

2.編程風(fēng)格(Programming Paradigms)
面向?qū)ο缶幊淌且环N用來(lái)針對(duì)一類問(wèn)題編寫優(yōu)質(zhì)代碼的編程技術(shù)。一個(gè)語(yǔ)言稱為是“面向?qū)ο蟆钡娜绻С郑⊿upport)面向?qū)ο箫L(fēng)格的編程。
在這里存在一個(gè)重要的區(qū)別。一個(gè)語(yǔ)言稱為是“支持”某種風(fēng)格的編程技術(shù)的,如果它提供了便于實(shí)施(方便地,安全地和高效地)該種風(fēng)格編程的手段;反之,如果需要使用額外的技能和手段來(lái)獲得基于某種風(fēng)格的編碼,則這個(gè)語(yǔ)言就是不“支持”該種編程風(fēng)格的,我們只能說(shuō)這個(gè)語(yǔ)言“使能”(Enable)了某種編程風(fēng)格。舉例來(lái)說(shuō),人們可以使用Fortran編寫結(jié)構(gòu)化程序,使用C語(yǔ)言編寫類型安全的程序,在Module-2中使用數(shù)據(jù)抽象技術(shù),但是,這些任務(wù)都具有不必要的困難性,因?yàn)檫@些語(yǔ)言都不“支持”那些編程風(fēng)格。
對(duì)于某種編程風(fēng)格的支持不僅意味著語(yǔ)言提供明確的并且可以直接使用的編程手段,而且還意味著在編譯時(shí)間和運(yùn)行時(shí)間提供某種檢查,以防止代碼無(wú)意中偏離了該種風(fēng)格。類型檢查是一個(gè)特別明顯的例子,二義性檢查和運(yùn)行時(shí)間檢查也可以擴(kuò)充語(yǔ)言支持特定編程風(fēng)格的能力。同時(shí),象標(biāo)準(zhǔn)庫(kù)和編程環(huán)境等等都可以增強(qiáng)這種支持。
并不一定說(shuō)一個(gè)語(yǔ)言如果支持了某種特性,則它就一定優(yōu)于其他沒(méi)有支持該特性的語(yǔ)言。在這里存在著太多的反例。重要的不是一個(gè)語(yǔ)言具有多少特性,而是它具有的特性是否能夠在特定的領(lǐng)域內(nèi)足以支持特定的編程風(fēng)格。

1.所有的特性必須是清晰,優(yōu)雅地集成進(jìn)語(yǔ)言的。
2.通過(guò)組合使用這些特性必須足以獲得解決方案,而不再需要使用其他特性。
3.假冒的和“特殊目的”的特性必須盡可能的少。
4.所有的特性都不能在那些不使用它們的程序中強(qiáng)加上過(guò)多的開(kāi)銷。
5.用戶只需要了解那些在程序中被明確使用的特性所構(gòu)成的語(yǔ)言子集就可以編寫程序。

最后兩點(diǎn)可以概括為“程序員不會(huì)被他們不了解的東西傷害”。如果對(duì)于一個(gè)特性是否有用存在任何疑問(wèn),則該特性就最好被拋棄。在語(yǔ)言中加上一個(gè)特性要遠(yuǎn)比從中或者從其文獻(xiàn)中去掉一個(gè)容易得多。
以下將羅列一些編程風(fēng)格以及支持它們的核心語(yǔ)言機(jī)制,但對(duì)此并不打算討論得過(guò)于深入和繁瑣。

2.1 過(guò)程化編程
最初的(可能也是目前最常用的)編程風(fēng)格是:
決定需要那些過(guò)程
使用能夠得到的最好的算法
設(shè)計(jì)的重點(diǎn)在于處理過(guò)程和執(zhí)行運(yùn)算的算法,語(yǔ)言為此提供了將參數(shù)傳遞給函數(shù)以及從函數(shù)中返回值的機(jī)制。和這種思維方式相關(guān)的文獻(xiàn)集中討論了傳參的不同方式,區(qū)分不同參數(shù)的方式,以及各種不同的過(guò)程(過(guò)程,函數(shù),宏)等等。Fortran是最早的過(guò)程語(yǔ)言,Algol60,Algol68,C和Pascal是一些后繼的過(guò)程語(yǔ)言。
平方根函數(shù)是個(gè)典型的例子,它簡(jiǎn)單地產(chǎn)生傳入?yún)?shù)的平方根。為此,該函數(shù)執(zhí)行一個(gè)簡(jiǎn)單的數(shù)學(xué)運(yùn)算:
double sqrt(double arg)
{
//the code for calculting a square root
}

void some_function()
{
Double root2 = sqrt(2);
}
從程序結(jié)構(gòu)的角度來(lái)看,函數(shù)理清了算法之間的雜亂關(guān)系。

2.2 數(shù)據(jù)隱藏
隨著時(shí)間的推移,程序設(shè)計(jì)的重點(diǎn)從重于過(guò)程設(shè)計(jì)轉(zhuǎn)向重于對(duì)數(shù)據(jù)的組織,這反映了程序規(guī)模的增長(zhǎng)。數(shù)據(jù)和直接操作數(shù)據(jù)的一集函數(shù)合稱為一個(gè)模塊。程序設(shè)計(jì)的風(fēng)格變?yōu)椋?br> 決定需要那些模塊
分解程序,使得數(shù)據(jù)隱藏在不同的模塊之中
這種風(fēng)格被稱為“數(shù)據(jù)隱藏規(guī)則”。而在那些不必將數(shù)據(jù)和與它相關(guān)的過(guò)程綁定到一起的場(chǎng)合可以只使用過(guò)程程序設(shè)計(jì)風(fēng)格。特別地,那些用來(lái)設(shè)計(jì)“好的過(guò)程”的技術(shù)現(xiàn)在可以應(yīng)用到模塊之內(nèi)的每個(gè)過(guò)程之上。最常見(jiàn)的例子是定義一個(gè)堆棧模塊,設(shè)計(jì)時(shí)有以下問(wèn)題需要解決:
1.為堆棧模塊提供一個(gè)用戶接口(例如,函數(shù) push()和pop() )
2.保證堆棧的表示(例如,一個(gè)元素的陣列)只能通過(guò)模塊的接口來(lái)訪問(wèn)
3.保證堆棧在它第一次被訪問(wèn)之前執(zhí)行過(guò)初始化

以下是一個(gè)不甚嚴(yán)格的堆棧模塊的外部接口:

//declaration of the interface of module stack of charater
char pop();
void push(char);
const stack_size = 100;

假定這個(gè)外部定義保存在stack.h文件之中,而其模塊內(nèi)部表示如下:
#include "stack.h"
static char v[stack_size];
static char* p = v;
char pop()
{
//Check for underflow and pop
}

void push(char c)
{
//check for overflow and push
}

要將堆棧的表示修改為鏈表是很方便的,用戶不能訪問(wèn)堆棧的內(nèi)部表示(因?yàn)関 和p 已經(jīng)被聲明為static的,因此只能在聲明它們的模塊內(nèi)部引用它們)?梢韵筮@樣使用這個(gè)堆棧模塊:
#include "stack.h"
void some_function()
{
char c = pop(push('c'));
if( c != 'c' ) error( "impossible" );
}

Pascal沒(méi)有提供令人滿意的設(shè)施來(lái)實(shí)施這種綁定。將一個(gè)名字和程序的其它部分隔離開(kāi)來(lái)的唯一辦法是使它局部于一個(gè)過(guò)程之內(nèi),這導(dǎo)致了奇怪的過(guò)程嵌套以及對(duì)于全局?jǐn)?shù)據(jù)的過(guò)度依賴。
C語(yǔ)言的表現(xiàn)略好一些,在上面所述的例子之中,可以將數(shù)據(jù)和與它相關(guān)的過(guò)程保存在同一個(gè)文件之中以形成模塊,由此程序員可以控制哪些名字是全局可見(jiàn)的(被聲明為static的名字只在本模塊內(nèi)可見(jiàn))。由此,C語(yǔ)言可以在一定程度上支持模塊化;然而C缺乏使用這種機(jī)制的一般性框架,同時(shí),通過(guò)static控制名字訪問(wèn)顯得過(guò)于低級(jí)。
Pascal的一個(gè)后繼語(yǔ)言,Module-2,走得更遠(yuǎn)一些。它形式化了模塊這個(gè)概念,提供了一些基本的語(yǔ)言構(gòu)成,如良定義的模塊聲明,對(duì)于名字范圍的明確控制(import,export), 模塊的初始化機(jī)制,以及一組公認(rèn)的對(duì)這些機(jī)制的使用方式。
C和Module-2在這個(gè)領(lǐng)域內(nèi)的區(qū)別可以概括為,C只是“使能”了將程序分解為模塊,而Module-2則“支持”這種技術(shù)。

2.3數(shù)據(jù)抽象
模塊化編程發(fā)展成為將某種類型的數(shù)據(jù)集中置于一個(gè)類型管理模塊的控制之下的編程風(fēng)格。如果有人需要兩個(gè)stack,則他可能設(shè)計(jì)出一個(gè)具有如下接口的堆棧管理模塊:

class stack_id; //stack_id is a type
//no details about stacks or stack_ids are known here
stack_id create_stack(int size); //make a stack and return its identifier
destroy_stack(stack_id);
void push( stack_id,char)
char pop(stack_id)
相對(duì)于以往那些無(wú)結(jié)構(gòu)的混亂風(fēng)格,這當(dāng)然是一次重大的改進(jìn)。然而,通過(guò)這種方式實(shí)現(xiàn)的“類型”又明顯地和語(yǔ)言的內(nèi)建類型有區(qū)別。每一個(gè)類型管理模塊都必須分別定義自己的機(jī)制來(lái)生成自己的“變量”;這里沒(méi)有什么明確的方法可以賦予變量以標(biāo)識(shí)符,也不可能讓編譯器和編程環(huán)境了解變量的名字。同時(shí),沒(méi)有辦法讓這些變量服從常用的變量作用域規(guī)則和參數(shù)傳遞規(guī)則。
通過(guò)模塊機(jī)制建立起來(lái)的類型在很多重要的方面都和內(nèi)建類型存在區(qū)別,同時(shí),它獲得的支持也遠(yuǎn)比內(nèi)建類型獲得要低級(jí)得多。例如:
void f()
{
stack_id s1;
stack_id s2;

s1 = create_stack(200);
//Oops: forgot to create s2

shar c1 = pop(s1,push(s1,'a'));
if( c1!='c') error("impossible" );
char c2 = pop(s2,push(s2,'a'))
if( c2!= 'c') error( "impossible");

destroy(s2);
//Oops,forgot to destroy s1
}

換言之,支持?jǐn)?shù)據(jù)隱藏風(fēng)格的模塊概念只是使能了數(shù)據(jù)抽象,但它不支持這種風(fēng)格。

Ada, Clu和C++等語(yǔ)言通過(guò)允許用戶定義和內(nèi)建類型行為相似的“類型”來(lái)解決這個(gè)問(wèn)題。這種“類型”通常稱為“抽象數(shù)據(jù)類型”。于是,編程風(fēng)格變?yōu)椋?br> 決定需要那些類型
為每一個(gè)類型實(shí)現(xiàn)一組完整的操作
而在那些不需要為一個(gè)類型生成多個(gè)對(duì)象的場(chǎng)合可以只使用數(shù)據(jù)隱藏技術(shù)。有理數(shù)和復(fù)數(shù)等算術(shù)類型是抽象數(shù)據(jù)類型的常見(jiàn)例子:

class complex{
doube re, im;
public:
complex(double r, double i) { re =r ;im = i; }
complex( double r) { re=r; im = 0; } //float->complex conversion

friend complex operator+(complex,complex);
friend compelx operator-(complex,complex); //binary minus
firend complex opeator-(complex);//unary minus
friend compelx operator*(complex,complex);
friend complex operator/(complex,complex);
//...
}

類complex(用戶自定義類型)的聲明確定了一個(gè)復(fù)數(shù)的“表示”和一組和它相關(guān)的操作!氨硎尽笔撬接械模褪钦f(shuō),只能通過(guò)在complex類中聲明的函數(shù)才能訪問(wèn)re和im 。函數(shù)可以如下定義:
complex operator+(complex a1, complex a2)
{
return complex( a1.re + a2.re, a1.im + a2.im );
}
可以象這樣使用:
complex a = 2.3;
complex b = 1/a;
complex c = a-b*complex(1,2.3);
//...
c = -(a/b)+2;
大多數(shù)(但不是全部)模塊可以使用“類型”來(lái)獲得更好的表達(dá)。對(duì)于那些更加適合表達(dá)成為“模塊”的概念,程序員可以定義一個(gè)只生成單個(gè)對(duì)象的類型來(lái)作為替代。當(dāng)然,語(yǔ)言也可以在提供自定義類型機(jī)制之外再提供一個(gè)獨(dú)立的模塊機(jī)制。
2.4數(shù)據(jù)抽象的問(wèn)題
一個(gè)抽象數(shù)據(jù)類型定義了一類黑盒,一經(jīng)定義完成,則它和程序的其他部分不再發(fā)生交互。除非修改它的定義,否則很難將它用于新的用途?紤]為一個(gè)圖形系統(tǒng)定義一個(gè)類型shape。假定當(dāng)前系統(tǒng)支持圓,三角形和正方形,同時(shí)還有其他的一些相關(guān)類:
class point { /*...*/ };
class color{ /*...*/ };
shape類可能定義成這樣:
enum kind{ circle,triangle,squre};
class shape{
point center;
color col;
kind k;
//representation of shape
public:
point where() { return center; }
void move(point to) { center = to; draw(); }
void draw();
void rotate(int);
//more operation
};
為了允許draw,rotate知道當(dāng)前處理的是何種形狀,其中的類型域"k"必須存在(在類Pascal語(yǔ)言中,可使用帶標(biāo)記k的可變記錄 ),函數(shù)draw可以定義成這樣:
void shape::draw()
{
switch( k )
{
case circle:
//draw a circle;
break;
case triangle:
//draw a triangle;
break;
case square:
//draw a square;
break;
}
}
這是混亂的。象draw這樣的函數(shù)必須了解當(dāng)前存在的各種“形狀”,因此每當(dāng)系統(tǒng)新增一個(gè)新的“形狀”,這些函數(shù)就必須被改寫。為了定義一個(gè)新的“形狀”就必須檢查,同時(shí)也可能修改shape的所有操作。所以除非可以修改源碼,否則將不可能在系統(tǒng)中增加新的“形狀”。而既然增加一個(gè)新的“形狀”將導(dǎo)致修改shape所有重要的操作,這就意味著編程需要更高的技巧同時(shí)也可能為現(xiàn)存的其他“形狀”引入bug。同時(shí),建立在一般類型shape之上的應(yīng)用框架(或者其中的一部分)可能要求每一個(gè)具體的“形狀”必須具有定長(zhǎng)的表示,這會(huì)為如何表示具體的形狀帶來(lái)很大的限制。
2.5 面向?qū)ο缶幊?br>問(wèn)題在于沒(méi)有將各種形狀的一般性屬性(具有顏色,可以繪畫)和特定形狀的專有屬性(圓具有半徑,使用畫圓函數(shù)執(zhí)行繪畫)區(qū)分開(kāi)來(lái)。對(duì)這種區(qū)分的表達(dá)和利用形成了面向?qū)ο蟮木幊獭V挥锌梢杂脕?lái)直接表達(dá)這種區(qū)分的語(yǔ)言才是支持面向?qū)ο蟮,其他語(yǔ)言不是。
Simula的繼承機(jī)制提供了一個(gè)解決方案。首先,指定一個(gè)類來(lái)定義形狀的一般性的屬性:
class shape{
point center;
color col;
public:
point where(){ return center; }
void move(point to){ center = to; draw() }
virtual void draw();
virtual void rotate(int);
//.......
}
調(diào)用接口可以確定但實(shí)現(xiàn)尚不能確定的函數(shù)都被標(biāo)記成為“virtual”(在Simula和C++中意味著可以被某個(gè)子類重新定義)。給定了這些定義以后,我們可以寫出操作形狀的一般性函數(shù):
void rotate_all(shape* v, int size, int angle)
//rotate all members of vector "v" of size "size" "angle" degrees
{
for( int i = 0; i < size; i++) v[i].rotate(angle);
}
為了定義了一個(gè)特定的形狀,我們必須聲明這是一個(gè)“形狀”,同時(shí)指定它所有的屬性(包括虛函數(shù))
class circle : public shape{
int radius;
public:
void draw(){ /*...*/ }
void rotate(int){} //yes, the null function
}
在C++中,類circle稱為從類shape中派生,而類shape則稱為是類circle的基類。也可以使用子類(subclass)和超類(superclass)這兩個(gè)術(shù)語(yǔ)。
編程的風(fēng)格變?yōu)椋?br> 決定需要那些類
為每一個(gè)類提供完整的操作
使用繼承明確地獲得一般性
而在不需要表達(dá)一般性的場(chǎng)合可以只使用數(shù)據(jù)抽象。通過(guò)繼承和虛函數(shù)可以發(fā)掘出的類型之間的共性的多少是衡量面向?qū)ο缶幊碳夹g(shù)是否適用于特定應(yīng)用領(lǐng)域的核心標(biāo)準(zhǔn)。某些領(lǐng)域,例如交互式圖形系統(tǒng),特別適合應(yīng)用面向?qū)ο蠹夹g(shù);而另外一些領(lǐng)域,例如經(jīng)典的算術(shù)類型和基于它們的運(yùn)算系統(tǒng),則看來(lái)使用數(shù)據(jù)抽象就足夠了,面向?qū)ο蠹夹g(shù)在這里不一定是必要的。
在一個(gè)系統(tǒng)中的不同類型之間發(fā)掘一般性不是一個(gè)容易的過(guò)程,可以發(fā)掘出的一般性的多少取決于系統(tǒng)的設(shè)計(jì)方法。設(shè)計(jì)時(shí)必須積極地尋找一般性,一方面應(yīng)當(dāng)基于已經(jīng)存在的類型構(gòu)造新的類型,另一方面可以通過(guò)察看不同類型之間表現(xiàn)出的相似性決定是否可以歸納出一個(gè)基類。
文獻(xiàn) Nygarrd[13]和Kerr[9]嘗試了不基于特定語(yǔ)言解釋面向?qū)ο缶幊;文獻(xiàn)Cargill[4]是對(duì)面向?qū)ο缶幊痰陌咐芯俊?br>
3.對(duì)數(shù)據(jù)抽象的支持
為類型定義一組操作同時(shí)限制只允許這組操作訪問(wèn)類型的數(shù)據(jù)是對(duì)數(shù)據(jù)抽象編程的基本支持。隨后,程序員很快發(fā)現(xiàn)需要進(jìn)一步的語(yǔ)言機(jī)制來(lái)方便定義和使用這些新類型。操作符重載是一個(gè)很好的例子。

3.1初始化和清除
一旦類型的表示被隱藏了起來(lái),則必須提供一個(gè)機(jī)制來(lái)執(zhí)行對(duì)變量的初始化。一個(gè)簡(jiǎn)單的方案是要求用戶在使用一個(gè)變量之前先調(diào)用一個(gè)特定的函數(shù)來(lái)初始化它。例如:
class vector{
int sz;
int* v;
public:
void init(int size); // call init to initialize sz and v before the first use of a
//vector

//...
}

vector v;
//don't use v here
v.init(10);
//use v here
這容易導(dǎo)致錯(cuò)誤并且不夠優(yōu)雅。好一點(diǎn)的方案允許類型的設(shè)計(jì)者為初始化提供一個(gè)特別的函數(shù);給定了這個(gè)函數(shù),分配和初始化一個(gè)變量變成了同一個(gè)操作。這個(gè)特定的函數(shù)經(jīng)常被稱為構(gòu)造函數(shù)。在某些場(chǎng)合初始化一個(gè)對(duì)象可能并不是十分簡(jiǎn)單的,這樣就常常需要一個(gè)對(duì)等的操作來(lái)在對(duì)象被最后一次使用之后執(zhí)行清除。在C++中,這樣的一個(gè)清除函數(shù)稱為析構(gòu)函數(shù)?紤]一個(gè)vector類型:
class vector{
int sz;
int* v;
public:
vector(int); //constructor
~vector(); //destructor
int& operator[](int index);
};
vector的構(gòu)造函數(shù)可以定義為分配空間,象這樣:
vector::vector(int s)
{
if( s<=0 ) error("bad vector size' );
sz = s;
v = new int[s]; //allocate an array of "s" integers
}
vector的析構(gòu)函數(shù)釋放這部分空間
vector::~vector()
{
(譯注:此處最好是delete []v;)
delete v; //deallocate the memory pointed to by v
}
C++不支持垃圾收集,這種允許一個(gè)類型自己管理存儲(chǔ)空間而不需要用戶來(lái)干預(yù)的技術(shù)是一個(gè)補(bǔ)償。存儲(chǔ)管理是構(gòu)造/析構(gòu)函數(shù)經(jīng)常執(zhí)行的操作,但是它們也常常用來(lái)執(zhí)行與此無(wú)關(guān)的事情。

3.2賦值和初始化
對(duì)于很多類型而言,控制其初始化和清除過(guò)程就已經(jīng)足夠了,但并不是所有的類型都如此。有時(shí)候控制拷貝過(guò)程也是十分必要的,考慮vector:
vector v1[100];
vector v2 = v1; //make a new vector v2 initialized to v1
v1 = v2; //assign v2 to v1

在這里必須有機(jī)制來(lái)定義v2初始化和對(duì)v1賦值的含義,當(dāng)然也可以選擇提供機(jī)制來(lái)禁止這種拷貝。理想的情況是,這兩種機(jī)制都存在。例如:
class vector{
int *v;
int sz;
public:
//....
void operator=(vector&); //assignment
vector(vector&); //initialization
};
給出了用戶定義的操作來(lái)解釋vector的賦值和初始化。賦值可以象這樣定義:
( 譯注:由于在上文class vector中operator=(vector&a)聲明為void類型,所以這里的定義最好為
void vector::operator(vector&a) )
vector::operator=(vector&a) //check size and copy elements
{
if( sz != a.sz ) error( "bad vector size for = " );
for( int = 0; i<sz;i ++) v[i] = a.v[i];
}
雖然賦值操作可以依賴于一個(gè)“舊的 ”的vector對(duì)象,但初始化操作就必須有所不同,例如:
vector::vector(vector& a) // initialize a vector from another vector
{
sz = a.sz;
v = new int[sz];
for( int i = 0; i < sz; i++ ) v[i]=a.v[i]; //copy elements
}
在C++中,一個(gè)形如X(X&) 的構(gòu)造函數(shù)定義了從X的一個(gè)對(duì)象出發(fā)構(gòu)造X的另一個(gè)對(duì)象的初始化過(guò)程。除了明確地構(gòu)造X的對(duì)象之外,X(X&)也被用來(lái)處理傳值的傳參過(guò)程和函數(shù)的返回值。
在C++中,可以通過(guò)將賦值聲明為私有來(lái)禁止對(duì)于對(duì)象的賦值操作。
class X{
void operator=(X&); //only members of x can
X(X&); //copy an x
//...
public:
//...
}
Ada不支持構(gòu)造,析構(gòu),對(duì)賦值的重載和用戶定義的參數(shù)傳遞和返回機(jī)制,這嚴(yán)重限制了用戶自定義類型的種類,同時(shí)強(qiáng)迫程序員回到“數(shù)據(jù)隱藏”技術(shù),就是說(shuō),用戶必須設(shè)計(jì)和使用類型管理模塊而不是真正的類型。
3.3參數(shù)化類型
為什么我們要定義一個(gè)整數(shù)類型的vector呢?要知道,用戶常常需要一個(gè)對(duì)于vector的作者而言類型未知的vector。因此,vector應(yīng)當(dāng)采用一種可以將“類型”作為參數(shù)來(lái)引用的表達(dá)方式加以定義:
class vector<class T>{ //vector of elements of type T
T* v;
int sz;
public:
vector( int s)
{
if( s<= 0 ) error( "bad vector size" );
v = new T[sz = s ]; //allocate an array of "s" "T"s
}
T& opeartor[](int i);
int size() { return sz; }
//...
}
特定類型的vector可以象這樣定義和使用:
vector<int> v1(100); //v1 is a vector of 100 integers
vector<complex> v2(200); //v2 is a vector of 200 complex numbers

v2[ i ] = complex(v1[x], v1[y]);
Ada,Clu和ML支持參數(shù)化類型。不幸的是,C++不支持(譯注,現(xiàn)在的C++標(biāo)準(zhǔn)支持參數(shù)化類型,稱為模板);這里使用的記號(hào)只是為了演示;但在必要時(shí),可以使用宏來(lái)模擬參數(shù)化類型。和那些指定了所有類型的類比起來(lái)這樣做并沒(méi)有在運(yùn)行時(shí)引入更多的開(kāi)銷。
一般來(lái)說(shuō),一個(gè)參數(shù)化類型總會(huì)依賴于參數(shù)類型的某些方面。例如,vector的有些操作假定參數(shù)類型定義了賦值操作。那么人們?nèi)绾伪WC這一點(diǎn)呢?一種方案是要求參數(shù)化類型的設(shè)計(jì)者表明這種依賴關(guān)系。例如,“T必須是一種定義了賦值操作的類型”。另一個(gè)好一點(diǎn)的辦法讓參數(shù)化類型的規(guī)格和參數(shù)類型的規(guī)格彼此獨(dú)立,編譯器可以檢測(cè)到對(duì)不存在操作的調(diào)用,并且可以給出相應(yīng)的錯(cuò)誤提示。例如:
cannot define vector(non_copy)::operator[](non_copy&) :
type non_copy does not have operator=
這種技術(shù)使得我們可以在“操作”這個(gè)級(jí)別上處理參數(shù)類型和參數(shù)化類型之間的依賴性。例如,我們可能定義一個(gè)具有排序功能的vector,排序操作可能用到參數(shù)類型的<,<= 和=操作。然而,只要不調(diào)用vector的排序功能,我們還是可以使用一個(gè)沒(méi)有<操作的類型來(lái)參數(shù)化vector。
從參數(shù)化類型中生成的每一個(gè)類型之間是彼此獨(dú)立的,這是一個(gè)問(wèn)題。例如,vector<char>和vector<complex>之間完全無(wú)關(guān)。理想的情況是,人們可以表達(dá)并且利用從同一個(gè)參數(shù)化類型中生成的各個(gè)類型之間具有的共性,例如,vector<char>和vector<complex>都具有一個(gè)和類型無(wú)關(guān)的size()操作。從vector的定義中推導(dǎo)出size可以被實(shí)例類型共用是可能的,但其過(guò)程并不簡(jiǎn)單。解釋型的語(yǔ)言或者同時(shí)支持參數(shù)化類型和繼承機(jī)制的語(yǔ)言在這個(gè)方面具有優(yōu)勢(shì)。

3.4 異常處理
隨著程序規(guī)模的增長(zhǎng),特別是當(dāng)程序庫(kù)對(duì)外發(fā)布后,提供一個(gè)處理錯(cuò)誤(或者更一般地說(shuō),“異常情況”)的標(biāo)準(zhǔn)機(jī)制是重要的。Ada,Algol68和Clu各自支持一套處理異常的標(biāo)準(zhǔn)機(jī)制。不幸的是,C++不直接支持異常處理(譯注,現(xiàn)在的C++標(biāo)準(zhǔn)已經(jīng)支持異常處理),而必須使用函數(shù)指針,“異常對(duì)象”,“錯(cuò)誤狀態(tài)”和C的庫(kù)函數(shù)signal和longjump等機(jī)制來(lái)偽造。這些機(jī)制不夠一般,同時(shí)也不能提供一個(gè)處理錯(cuò)誤的標(biāo)準(zhǔn)框架。
重新考慮一下vector的例子。當(dāng)一個(gè)越界的索引值被傳遞給索引(subscribe)操作時(shí),會(huì)發(fā)生什么?vector的設(shè)計(jì)者應(yīng)該可以為此指定一個(gè)缺省行為:
class vector {
...
except vector_range{
//define an exception called vector_range
//and specify default code for handling it
error("global,vector range error" );
exit( 99 );
}
}
vector::opeartor[]()可以觸發(fā)異常處理代碼而不是調(diào)用出錯(cuò)函數(shù):
int& vector::operator[](int i)
{
if( 0 < i sz <= i ) raise vector_ranger;
return v[ i ];
}
這導(dǎo)致堆;鼐,直到發(fā)現(xiàn)一個(gè)能夠處理vector_range異常的句柄為止。然后執(zhí)行該異常處理句柄。
可以針對(duì)一個(gè)特定的代碼塊來(lái)定義異常句柄:
void f() {
vector v(10);
try { //errors here are handled by the local
//exception handler defined below
//...
int i = g(); //g might cause a range error using some vector
v[ i ] = 7;
}
except {
vector::vector_ranger:
error( "f() vector ranger error" );
return;
}
//error here are handled by the global
//exception hander defined in vector
int i = g();
v[ i ] = 7; ://g might cause a range error using some vector
//potential range error
}
可以有很多種方式來(lái)定義異常以及異常處理句柄的行為。這里列出的異常機(jī)制概貌是從Clu和Module-2+中變化而來(lái)的。這種風(fēng)格的異常處理可以實(shí)現(xiàn)為,直到拋出異常時(shí)才執(zhí)行異常處理代碼。也可以容易地使用C的setjmp和longjup模擬出來(lái)。
那么,象上文定義的異常處理的語(yǔ)義在C++中是否可以完全偽造出來(lái)呢?很不幸,不能。問(wèn)題在于,當(dāng)異常發(fā)生時(shí),運(yùn)行棧必須被回卷到安裝異常處理句柄的位置,在C++中,這涉及到調(diào)用在回卷過(guò)程中被銷毀對(duì)象的析構(gòu)函數(shù)。使用C的longjmp函數(shù)是做不到這一點(diǎn)的;一般地說(shuō),用戶自身也不能做到這一點(diǎn)。

3.5強(qiáng)制
已經(jīng)證明,用戶自定義的強(qiáng)制是非常有用的技術(shù),例如,構(gòu)造函數(shù)complex(double)隱含著一個(gè)從double到complex的強(qiáng)制。程序員可以明確地指出強(qiáng)制,或者在必要時(shí),如果沒(méi)有二義性,編譯器也可以暗中引入它:
complex a = complex(1);
complex b = 1; //implicit: 1->complex(1)
a = b + complex(2);
a = b + 2 //implicit: 2->complex(2)

C++引入用戶定義的強(qiáng)制的原因是,在支持算術(shù)運(yùn)算的語(yǔ)言中混合模式的算術(shù)表達(dá)式是很常見(jiàn)的;同時(shí),參加運(yùn)算的用戶自定義類型(例如,矩陣,字符串,機(jī)器地址等)也大多可以很自然地相互映射。
從程序組織的角度來(lái)看,有一種類型的強(qiáng)制可以證明是格外有效的:
complex a = 2;
complex b = a+2; //interpereted as operator+(a,complex)
b = 2+a; //interpereted as operator+(complex(2),a)
在解釋‘+’操作時(shí)只需要一個(gè)函數(shù),并且對(duì)于類型系統(tǒng)而言,兩個(gè)操作數(shù)是被同等看待的。進(jìn)一步,我們看到,可以在不對(duì)整數(shù)概念做出任何調(diào)整的前提下只通過(guò)實(shí)現(xiàn)類complex就可以將這兩個(gè)概念平滑地集成到一起。這和“純面向?qū)ο笙到y(tǒng)”截然不同,在那里這些操作會(huì)被如下解釋:
a+2; ://a.opeartor+(2)
2+a; ://2.operator(a)
這樣就必須修改類integer來(lái)使得2.operator(a)合法化。當(dāng)在一個(gè)系統(tǒng)中加入新的功能時(shí),修改已有的代碼是必須盡量避免的,一般地說(shuō),面向?qū)ο蟮木幊碳夹g(shù)能夠很好地支持這個(gè)目標(biāo),但在這里,數(shù)據(jù)抽象技術(shù)提供了更好的解決方案。

3.6迭代器(Iterators)
一般認(rèn)為,支持?jǐn)?shù)據(jù)抽象的語(yǔ)言必須提供定義控制結(jié)構(gòu)的手段。特別是,常常需要一個(gè)允許用戶循環(huán)訪問(wèn)一個(gè)容器類型中所含元素的機(jī)制,同時(shí)又不能迫使用戶依賴于容器類型的實(shí)現(xiàn)細(xì)節(jié)。如果有一個(gè)定義類型的強(qiáng)大機(jī)制,同時(shí)又能夠重載操作符,則就可以在不引入獨(dú)立的定義控制結(jié)構(gòu)的機(jī)制的前提下實(shí)現(xiàn)這一目標(biāo)。
對(duì)于vector,用戶可以通過(guò)下標(biāo)來(lái)確定其順序,所以可以不必定義迭代器。然而我還是定義了一個(gè)來(lái)演示這個(gè)技術(shù)。迭代器可以有很多種風(fēng)格,我比較喜歡的是通過(guò)重載函數(shù)操作符:
class vector_iterator{
vector & v;
int i;
public:
vector_iterator(vector& r) { i = 0; v = r; }
int operator()() { return i<v.size() ? v.elem(i++) : 0; }
};
現(xiàn)在我們可以象這樣聲明和使用迭代器:
vector v(sz);
vector_iterator next(v);
int i;
while( i = next() ) print( i );
在同一個(gè)時(shí)刻一個(gè)對(duì)象可以激活多個(gè)迭代器對(duì)象;同時(shí),一個(gè)類型可以定義多種不同類型的迭代器以便執(zhí)行不同的循環(huán)操作。迭代器是一種相當(dāng)簡(jiǎn)單的控制結(jié)構(gòu),也可以定義更加一般的控制機(jī)制,例如C++標(biāo)準(zhǔn)庫(kù)提供了co-routine類[15]。
對(duì)于很多容器類型,例如vector,可以將迭代機(jī)制作為類型自身的一部分來(lái)定義以避免引入獨(dú)立的迭代器。可以將vector定義為具有一個(gè)“當(dāng)前狀態(tài)”:
class vector {
int* v;
int sz;
int current;
public:
//...
int next() { return (current++<sz) ? v[current] : 0; }
int prev() { return ( 0 < --current ) ? v[current] : 0; }
};
于是可以象這樣操作:
vector v(sz);
int i;
while( i = v.next() ) print(i);
和迭代器比起來(lái),這樣的方案不夠一般;但是在一種重要的特殊情況下它減少了開(kāi)銷:可能我們只需要一種類型的迭代器,并且在同一時(shí)刻只會(huì)有一個(gè)迭代器對(duì)象在活動(dòng)。如果必要,也可以在這個(gè)簡(jiǎn)單的方案之上加上更一般的機(jī)制。請(qǐng)注意,使用這種簡(jiǎn)單的解決方案比起使用迭代器來(lái)需要更多的設(shè)計(jì)遠(yuǎn)見(jiàn)。迭代器技術(shù)也可以設(shè)計(jì)為同一個(gè)迭代器類型能夠綁定到不同的容器類型,這樣通過(guò)一個(gè)迭代器就可以訪問(wèn)不同的容器類型。

3.7 實(shí)現(xiàn)問(wèn)題
對(duì)數(shù)據(jù)抽象的支持大多定義為語(yǔ)言特征并且由編譯器來(lái)實(shí)現(xiàn)。但參數(shù)化類型最好能夠通過(guò)一個(gè)對(duì)于語(yǔ)言的語(yǔ)義有更多理解的連接器來(lái)支持;同時(shí)異常處理需要運(yùn)行環(huán)境的支持。他們都可以在不犧牲一般性和易用性的前提下獲得很好的編譯速度和效率。
隨著定義類型的能力的增長(zhǎng),程序開(kāi)始更多地依賴來(lái)自一些庫(kù)中的類型(并不僅限于那些在語(yǔ)言的手冊(cè)中描述的內(nèi)容)。這很自然地需要工具來(lái)表達(dá)程序中哪些部分被插入了庫(kù)中,而哪些部分是從庫(kù)中抽取出來(lái)的;也需要工具來(lái)找出庫(kù)包含了哪些東西和庫(kù)中的哪些部分是實(shí)際被程序使用了的等等。
對(duì)于編譯型語(yǔ)言,能夠使得代碼在修改以后盡量減少編譯工作的工具是非常重要的。同時(shí),連接器/加載器能夠在加載執(zhí)行代碼時(shí)盡量不加載大量的無(wú)關(guān)和無(wú)用代碼的能力也是非常關(guān)鍵的。特別要指出來(lái),如果一個(gè)類型只有少數(shù)幾個(gè)操作被調(diào)用,而庫(kù)/連接器/加載器卻將該類型的所有操作都加載入內(nèi)存的行為是特別糟糕的。

4. 對(duì)面向?qū)ο蟮闹С?br>有兩個(gè)機(jī)制在支持面向?qū)ο缶幊讨衅鹆嘶镜淖饔,第一個(gè)是類的繼承機(jī)制;第二個(gè)是,當(dāng)在編譯時(shí)無(wú)法確定一個(gè)對(duì)象的實(shí)際類型時(shí),應(yīng)當(dāng)能夠在運(yùn)行時(shí)基于對(duì)象的實(shí)際類型來(lái)決定調(diào)用的具體方法。其中,對(duì)于方法調(diào)用機(jī)制的設(shè)計(jì)是關(guān)鍵。同時(shí),如上文所述的對(duì)數(shù)據(jù)抽象的支持技術(shù)對(duì)于支持面向?qū)ο笠餐瑯邮侵匾,因(yàn)閿?shù)據(jù)抽象的觀點(diǎn),以及為了在語(yǔ)言中優(yōu)雅地支持它而作的努力在面向?qū)ο蠹夹g(shù)中同樣也有效。這兩種技術(shù)的成功都取決于對(duì)類型的設(shè)計(jì)以及能夠高效,方便和靈活地使用這些類型。相對(duì)于數(shù)據(jù)抽象而言,面向?qū)ο蠹夹g(shù)能夠設(shè)計(jì)出更加一般,更加靈活的數(shù)據(jù)類型。

4.1調(diào)用機(jī)制
支持面向?qū)ο蟮年P(guān)鍵語(yǔ)言特征是針對(duì)一個(gè)給定的對(duì)象如何調(diào)用它的方法。例如,給定指針p,如何處理調(diào)用p->f(arg)呢?在這里存在一系列的選擇。
在C++和Simula這樣廣泛應(yīng)用靜態(tài)類型檢查的語(yǔ)言中,可以借助于類型系統(tǒng)來(lái)在不同的調(diào)用方式之間作出選擇。在C++中,有兩種函數(shù)調(diào)用的方式:
【1】普通的方法調(diào)用:具體調(diào)用那個(gè)方法在編譯時(shí)間就可以決定(通過(guò)查找編譯器的符號(hào)表),同時(shí)在使用標(biāo)準(zhǔn)過(guò)程調(diào)用機(jī)制基礎(chǔ)上增加一個(gè)表示對(duì)象身份的指針。如果在某些場(chǎng)合標(biāo)準(zhǔn)過(guò)程調(diào)用顯得效率不夠高,則可以將函數(shù)聲明成為內(nèi)聯(lián)(inline)的,編譯器就嘗試在調(diào)用的位置展開(kāi)函數(shù)體。通過(guò)這種方式,人們可以既獲得類似宏的展開(kāi)機(jī)制又不犧牲標(biāo)準(zhǔn)函數(shù)的語(yǔ)義。這樣的優(yōu)化措施對(duì)于數(shù)據(jù)抽象也同樣是有價(jià)值的。
【2】虛函數(shù)調(diào)用:函數(shù)調(diào)用依賴于對(duì)象的實(shí)際類型,一般地說(shuō),對(duì)象的實(shí)際類型只能在運(yùn)行時(shí)間才能確定。典型的情況是,指針p具有某個(gè)基類B的類型,而p指向的對(duì)象是B的某個(gè)子類D的(就想上文例子中的shape和circle)。調(diào)用機(jī)制必須察看編譯器為對(duì)象指定的一個(gè)表來(lái)決定調(diào)用那個(gè)函數(shù)。一旦找到了函數(shù),譬如說(shuō)是D::f,則可以使用上文所述的方式來(lái)調(diào)用。在編譯時(shí)間,名字f轉(zhuǎn)換成為函數(shù)指針表的一個(gè)索引。這樣的調(diào)用機(jī)制幾乎和普通的方法調(diào)用一樣有效,在標(biāo)準(zhǔn)C++中,只需要額外地多做5次內(nèi)存引用。
弱類型語(yǔ)言需要采用更加復(fù)雜的機(jī)制。在象Smalltalk之類的語(yǔ)言中,必須保存類的所有方法的名字以備在運(yùn)行時(shí)間執(zhí)行查找:
【3】方法觸發(fā):首先通過(guò)p指向的對(duì)象找到正確的方法名字的表,然后在這個(gè)表中查找是否有一個(gè)方法“f”,如果存在則調(diào)用之;否則出錯(cuò)。和靜態(tài)類型語(yǔ)言在編譯時(shí)間執(zhí)行函數(shù)名查找不同,在這里方法名查找是針對(duì)一個(gè)實(shí)際的對(duì)象執(zhí)行的。
和虛函數(shù)調(diào)用比起來(lái),方法觸發(fā)是低效的,但是它更加靈活。由于此時(shí)通常無(wú)法執(zhí)行靜態(tài)參數(shù)類型檢查,所以這種調(diào)用方式必須和動(dòng)態(tài)類型檢查一起使用。

4.2 類型檢查
上文所述的“形狀”這個(gè)例子顯示了虛函數(shù)的威力。那么方法觸發(fā)為我們提供了些什么呢?答案是,我們現(xiàn)在可以在任意對(duì)象上嘗試調(diào)用任意方法!
在任意對(duì)象上觸發(fā)任意方法的能力使得類庫(kù)的設(shè)計(jì)者可以將正確處理數(shù)據(jù)類型的責(zé)任轉(zhuǎn)嫁到用戶的頭上,當(dāng)然,這簡(jiǎn)化了庫(kù)的設(shè)計(jì)。例如:
class stack { ://assume class any has a member next
any* v;
void push(any* p )
{
p->next = v;
v = p;
}
any* pop()
{
if( v == 0 ) return error_obj;
any* r = v;
v = v->next;
return r;
}
};
這樣,用戶就必須有責(zé)任保證避免以下的類型匹配錯(cuò)誤:
stack<any*> cs;

cs.push( new Saab900 );
cs.push( new Saab37B );

plane* p = (plane*)cs.pop();
p->takeoff();

p = (plane*)cs.pop();
p->tackoff(); //Oops! Run time error: a Saab 900 is a car
// a car does not have a takeoff method;
消息處理機(jī)制可以檢測(cè)到程序試圖將一部汽車當(dāng)作一架飛機(jī),于是觸發(fā)了一個(gè)錯(cuò)誤。但是,只有當(dāng)用戶是程序員本人時(shí)這樣的錯(cuò)誤提示才可能有一些安慰性的價(jià)值;由于缺少靜態(tài)類型檢查,我們很難保證在最終發(fā)布的程序中不包含這樣的錯(cuò)誤。自然地,在一個(gè)沒(méi)有靜態(tài)類型的,基于方法的語(yǔ)言中這樣的矛盾會(huì)更加突出。
將參數(shù)化類型和虛函數(shù)結(jié)合起來(lái)使用可以在庫(kù)設(shè)計(jì)的靈活性,簡(jiǎn)單性,以及庫(kù)使用的方便性上近似于方法觸發(fā),而同時(shí)又不需要放棄靜態(tài)類型或在空間和時(shí)間上引入開(kāi)銷,例如:
stack<plane*> cs;
cs.push( new Saab900 ); // compile time error:
// type mismatch: car* passed,plane* expected
cs.push( new Saab37B);

plane* p = cs.pop();
p->takeoff(); //fine: a Saab 37B is a plane.

p = cs.pop();
p->takeoff;

基于靜態(tài)類型檢查/虛函數(shù)調(diào)用的程序風(fēng)格和基于動(dòng)態(tài)類型檢查/方法觸發(fā)的程序風(fēng)格存在某些差異。例如,在Simula和C++中一個(gè)類為其所有子類的對(duì)象指定了一個(gè)確定的接口,而Smalltalk中的類則為其所有子類的對(duì)象指定了一個(gè)初始接口。換言之,Smalltalk中的類是一個(gè)最小的接口規(guī)范,用戶可以任意嘗試其他所有在接口中未指定的方法;而C++類則是一個(gè)確定的接口規(guī)范,用戶可以確信,只有調(diào)用那些在接口中有定義的方法才能通過(guò)編譯。
4.3 繼承
考慮一個(gè)支持方法查找但不支持繼承的語(yǔ)言,這個(gè)語(yǔ)言可以被稱為支持面向?qū)ο髥?我認(rèn)為不。很明顯,我們可以利用方法查找技術(shù)來(lái)使得對(duì)象適應(yīng)具體的應(yīng)用環(huán)境,并因此能完成很多有趣的事情。然而,為了避免混亂,我們還是需要一個(gè)系統(tǒng)的手段來(lái)將對(duì)象的方法和作為對(duì)象表示的數(shù)據(jù)結(jié)構(gòu)結(jié)合在一起。同時(shí),為了使得對(duì)象的用戶可以了解到對(duì)象的行為方式,又必須有一個(gè)系統(tǒng)的手段來(lái)表示對(duì)象不同的行為之間的共性。“這個(gè)系統(tǒng)而標(biāo)準(zhǔn)的手段”就是繼承。
考慮一個(gè)支持繼承但是不支持虛函數(shù)或者方法查找的語(yǔ)言,這個(gè)語(yǔ)言可以被稱為是支持面向?qū)ο蟮膯?我認(rèn)為不:上文所述的“形狀”這個(gè)例子在這個(gè)語(yǔ)言中沒(méi)有很好的解決方案。然而,相對(duì)于只支持?jǐn)?shù)據(jù)抽象的語(yǔ)言,這樣的語(yǔ)言要強(qiáng)大很多。這個(gè)觀點(diǎn)來(lái)自于這樣的觀察:很多基于Simula和C++的程序都使用繼承來(lái)組織其結(jié)構(gòu),但在其中沒(méi)有使用虛函數(shù)。表達(dá)共性的能力是一個(gè)特別強(qiáng)大的工具,例如,不使用聯(lián)合我們就可以解決各種“形狀”需要一個(gè)公用表示的問(wèn)題。但是,由于缺少虛函數(shù),人們必須借助于一個(gè)“類型域”來(lái)表達(dá)對(duì)象的具體類型,這導(dǎo)致代碼的組織缺少模塊性。
類繼承是一種特別有效的編程工具, 繼承不僅可以用來(lái)支持面向?qū)ο缶幊,而且還有著更廣泛的應(yīng)用。在面向?qū)ο蟮木幊虒?shí)踐中,我們可以使用基類來(lái)表達(dá)一般的概念,而使用子類來(lái)表達(dá)各種特例。這樣的方式只是部分展示了繼承的強(qiáng)大功能,然而那些所有函數(shù)都是虛函數(shù)的(或者都是基于方法查找的)語(yǔ)言很強(qiáng)調(diào)這樣的觀點(diǎn)。如果能夠正確地控制從基類中繼承而來(lái)的內(nèi)容,那么類繼承可以是一個(gè)從已有類型中產(chǎn)生新類型的強(qiáng)大工具。子類和基類之間的關(guān)系并不總能概括為特例和一般,“分解”(factoring)能夠更加準(zhǔn)確地表達(dá)子類和基類之間的關(guān)系。
繼承是又一個(gè)我們無(wú)法簡(jiǎn)單地預(yù)言應(yīng)該如何使用才算是合理的編程工具,并且在今天(即便Simula誕生已經(jīng)超過(guò)20年)要簡(jiǎn)單地說(shuō)斷定說(shuō)哪些使用方式是誤用也還為時(shí)過(guò)早。

4.4多繼承
假設(shè)A是B的基類,則B繼承了A的所有屬性;就是說(shuō),除了自身特有的一些屬性之外,B就是一個(gè)A。在這樣的解釋之下,很明顯, 讓B同時(shí)繼承兩個(gè)基類A1和A2很可能是有用的。這稱為多繼承。
舉一個(gè)經(jīng)典的例子。假定類庫(kù)提供了兩個(gè)類displayed和task,分別用來(lái)表示顯示管理對(duì)象和調(diào)度管理對(duì)象,則程序員就可以產(chǎn)生象這樣的類:
class my_displayed_task : public displayed, public task{
// my stuff
};

class my_task : public task { //not displayed
// my stuff;
}

class my_displayed : public displayed { // not a task
// my stuff;
};
如果只使用單繼承,那么程序員就只能使用這三個(gè)選擇中的兩個(gè)。這或者將導(dǎo)致代碼的重復(fù),或者將導(dǎo)致缺少?gòu)椥裕蛘叱3烧呒嬗。在C++中,這樣的例子可以使用多繼承解決,并且相對(duì)于單繼承不引人明顯開(kāi)銷(時(shí)間和空間),也不犧牲靜態(tài)類型檢查。
二義性可以在編譯時(shí)間解決:
class A{ public : f(); ... }
class B{ public : f(); ... }
class C: public A, public, B{ ... };

void g() {
C* p;
p->f(); //error:ambiguous
}
在這一點(diǎn)上,C++有別于LISP的一個(gè)支持多繼承的面向?qū)ο蠓窖。LISP通過(guò)依賴聲明的順序來(lái)解決名字沖突,或者將來(lái)自不同基類的同名的方法看作是等價(jià)的,又或者將基類中的同名方法組合成為一個(gè)最高層類中的一個(gè)更復(fù)雜的方法。
而在C++中,我們可以通過(guò)一個(gè)額外的功能來(lái)解決名字的沖突:
class C : public A, public B{
public:
f()
{
//C's own stuff
A::f();
B::f();
}
....
}
除了這樣用來(lái)直接表達(dá)獨(dú)立的多繼承(independent multiple inheritance)的概念之外,看來(lái)還需要更一般的機(jī)制來(lái)表達(dá)在一個(gè)多繼承格(multiple inheritance lattice)中類之間的依賴關(guān)系。在C++中,可以使用虛基類來(lái)表達(dá)一個(gè)類對(duì)象中的一個(gè)子對(duì)象被所有其他的子對(duì)象共享:
class W{ ... }
class Bwindow //window with border
: public virtual W
{ ... }

class Mwindow
: public virtual W
{ ... }

class BMW //window with border and menu
: public Bwindow,public Mwindow
{ ... };

這樣,在一個(gè)BMW對(duì)象中,只有一個(gè)W子對(duì)象被Bwindow和Mwindow子對(duì)象共享。LISP方言提供了方法組合的概念來(lái)減少在編程中使用如此復(fù)雜的類層次,但C++沒(méi)有。

4.5 封裝
考慮類的一些成員(不管是數(shù)據(jù)成員還是函數(shù)成員)需要被保護(hù)起來(lái)以防止未授權(quán)的訪問(wèn)。那么該如何合理地界定哪些函數(shù)可以訪問(wèn)這些成員呢?對(duì)于面向?qū)ο蟮木幊陶Z(yǔ)言來(lái)說(shuō),最明顯的答案是“定義在該對(duì)象上的所有成員函數(shù)”。但由此可以有一個(gè)不太明顯的推論,即實(shí)際上我們不能完整地確定一個(gè)集合,其中包括了所有有權(quán)訪問(wèn)這些受保護(hù)成員的函數(shù);因?yàn)榭偪梢詮倪@些具有保護(hù)成員的類中派生出新的類,并且在派生類上定義新的成員函數(shù)。這樣的方案一方面在很大的程度上防止了意外地訪問(wèn)保護(hù)成員(因?yàn)槲覀儾粫?huì)“意外”地派生出一個(gè)新類),在另一方面,又為使用類層次建立應(yīng)用提供了彈性(因?yàn)槲覀兛梢酝ㄟ^(guò)派生一個(gè)新類來(lái)賦予自己訪問(wèn)保護(hù)成員的能力)。
不幸的是,對(duì)于一個(gè)面向數(shù)據(jù)抽象的語(yǔ)言而言,其答案是不同的:“必須在類的聲明中羅列出所有有權(quán)訪問(wèn)保護(hù)成員的函數(shù)”,但對(duì)于這些函數(shù)本身沒(méi)有特別的要求,特別是,這些函數(shù)不必是類的成員函數(shù)。在C++中,有權(quán)訪問(wèn)私有成員的非成員函數(shù)稱為友元函數(shù)。在上文中定義的類complex具有友元函數(shù)。有時(shí)候,將一個(gè)函數(shù)聲明為多個(gè)類的友元函數(shù)也是很有用的。如果想理解一個(gè)類型的意義,特別是想修改它的時(shí)候,有一個(gè)成員函數(shù)和友元函數(shù)的完整列表是非常有好處的。
以下的例子演示了在C++中封裝的幾個(gè)選擇:
class B{
//class member are default private
int i;
void f1();
protected:
int i2;
void f2();
public:
int i3;
void f3();
friend void g(B*); //any function can be designated as a friend
}
私有和保護(hù)成員不能被外界直接訪問(wèn):
void h(B* p)
{
p->f1(); //error:B::f1 is private
p->f2(); //error:B::f2 is protected
p->f3(); //fine:B::f1 is public
}
保護(hù)成員可以在派生類中訪問(wèn),但私有成員不能:
class D: public B{
public:
void g()
{
f1(); //erro: B:f1 is private
f2(); //fine: B:f2 is protected, but D is derived from B
f3(); //fine: B:f3 is public
}
}
友元函數(shù)可以象成員函數(shù)一樣訪問(wèn)私有和保護(hù)成員:
void g(B* p)
{
p->f1(); //fine: B::f1 is private, but g() is a friend of B
p->f2(); //fine: B::f2 is protected, but g() is a friend of B
p->f3();// fine: B::f1 is public
}
隨著程序規(guī)模,用戶的數(shù)量的增長(zhǎng),同時(shí)如果用戶在地理上分布比較分散,成員保護(hù)機(jī)制的重要性就會(huì)大大增加。文獻(xiàn)Snyder[17]和Stroustrup[18]進(jìn)一步討論了保護(hù)問(wèn)題。
4.6 實(shí)現(xiàn)問(wèn)題
為了支持面向?qū)ο缶幊蹋饕枰倪M(jìn)運(yùn)行時(shí)間系統(tǒng)和編程環(huán)境。在一定程度上,這是因?yàn)槊嫦驅(qū)ο笮枰恼Z(yǔ)言機(jī)制已經(jīng)由數(shù)據(jù)抽象引入了,所以不再需要很多額外的特征。
面向?qū)ο蠹夹g(shù)進(jìn)一步模糊了編程語(yǔ)言及其環(huán)境之間的界限,因?yàn)楦鞣N一般的或是具體的用戶定義類型越來(lái)越多地充斥在程序之中。這需要進(jìn)一步發(fā)展運(yùn)行時(shí)間系統(tǒng),庫(kù)工具,調(diào)試器,性能測(cè)量工具和監(jiān)控工具。理想的情況是這些工具被集成到一個(gè)環(huán)境之中去。Smalltalk是這方面的一個(gè)好例子。

5 限制
為了定義了一個(gè)能夠充分利用數(shù)據(jù)隱藏,數(shù)據(jù)抽象和面向?qū)ο蠹夹g(shù)的語(yǔ)言,我們面對(duì)的主要問(wèn)題是,任何一個(gè)通用的程序語(yǔ)言都必須能夠:
1.在傳統(tǒng)的計(jì)算機(jī)上運(yùn)行
2.和傳統(tǒng)的操作系統(tǒng)并存
3.在時(shí)間效率上堪輿傳統(tǒng)的程序語(yǔ)言媲美
4.用于主要的應(yīng)用領(lǐng)域
這意味著,這個(gè)語(yǔ)言必須能夠高效地執(zhí)行算術(shù)運(yùn)算(在浮點(diǎn)運(yùn)算方面要堪輿媲美Fortran);其訪問(wèn)內(nèi)存的方式必須能夠用于設(shè)備驅(qū)動(dòng)程序;同時(shí)必須能夠遵從傳統(tǒng)操作系統(tǒng)制定的古怪標(biāo)準(zhǔn)來(lái)生成調(diào)用。進(jìn)一步,傳統(tǒng)語(yǔ)言必須能夠調(diào)用使用面向?qū)ο笳Z(yǔ)言書寫的函數(shù),而面向?qū)ο蟮恼Z(yǔ)言也必須能夠調(diào)用使用傳統(tǒng)語(yǔ)言書寫的函數(shù)。
此外,如果一個(gè)面向?qū)ο蟮某绦蛘Z(yǔ)言依賴于不能在傳統(tǒng)系統(tǒng)結(jié)構(gòu)下有效實(shí)現(xiàn)的技術(shù),則它不可能成為一個(gè)通用的語(yǔ)言。除非得到特別的支持,否則方法觸發(fā)機(jī)制的一般性實(shí)現(xiàn)將會(huì)成為負(fù)擔(dān)。
類似的,垃圾收集可能成為性能和移植性的瓶頸。大多數(shù)面向?qū)ο蟮恼Z(yǔ)言都采用垃圾收集機(jī)制來(lái)簡(jiǎn)化程序員的工作,同時(shí)也減少語(yǔ)言本身和編譯器的復(fù)雜性。然而,就算我們可以在一些非關(guān)鍵的領(lǐng)域內(nèi)使用垃圾收集,但一旦需要,我們就應(yīng)該能夠保留對(duì)存儲(chǔ)器的控制權(quán)。另一方面,一個(gè)程序語(yǔ)言選擇放棄垃圾收集,轉(zhuǎn)而為類型管理自身的存儲(chǔ)提供便利的表達(dá)手段也是切實(shí)可行的。C++就是一個(gè)例子。異常處理和并發(fā)特征是另外一個(gè)潛伏著問(wèn)題的地方。任何依賴于連接器的支持才能有效實(shí)現(xiàn)的機(jī)制有可能存在移植問(wèn)題。在一個(gè)語(yǔ)言中擁有“低級(jí)”特征的另一個(gè)方法是可以在主要的應(yīng)用領(lǐng)域中使用一個(gè)獨(dú)立的“低級(jí)”語(yǔ)言。

6. 結(jié)論
基于繼承的編程叫做是面向?qū)ο蟮木幊谭椒,基于用戶自定義類型的編程叫做是基于數(shù)據(jù)抽象的編程方法。除了很少的一些例外情況之外,面向?qū)ο缶幊炭梢砸暈槭菙?shù)據(jù)抽象的一個(gè)超集。只有得到了正確的支持這些技術(shù)才能是有效的;對(duì)數(shù)據(jù)抽象的支持主要來(lái)自語(yǔ)言本身,而面向?qū)ο髣t需要來(lái)自編程環(huán)境的進(jìn)一步支持。為了通用性,支持?jǐn)?shù)據(jù)抽象和面向?qū)ο蟮恼Z(yǔ)言必須能夠高效地利用硬件。

7. 致謝
本文一個(gè)較早的版本在斯德哥爾摩的 Association of Simula Users會(huì)議上面世。在那里的進(jìn)行的討論導(dǎo)致了對(duì)本文的風(fēng)格和內(nèi)容的做了很多改進(jìn)。Brain Kernighan和Ravi Sethi給出了很多建設(shè)性的意見(jiàn)。同時(shí)感謝所有為增強(qiáng)C++作出了貢獻(xiàn)的人。

8.參考文獻(xiàn)
略,請(qǐng)參閱原文