眾所周知,Java語言最明顯的優勢在於用它設計的程序可以廣泛地運行於互聯網上所有安裝了VM解釋器的計算機上。然而,如今JAVA之所以在市場上如此流行,還得益於它的另一賣點:它提供了安全可靠和使用方便的存儲治理機制。這是部分編程人員將它與其前身C++語言對比後所得出的結論。本文將針對兩種語言的內存(以對象為單位)使用機制,通過從靈活性、易用性和效率三個方面的比較,來證實這樣一個事實:在C++中可以實現與JAVA一樣的存儲治理機制。
一、JAVA對象是C++對象和指針二者的繼續
JAVA作為C++的後繼,在內存分配和對象使用上與之有很大的相似之處。請看下面的比較:
表1
操作
JAVA
C++
指針使用
非指針使用
聲明
ObjectClass Instance
ObjectClass* Instance
ObjectClass Instance
創建
Instance=new ObjectClass()
Instance=new ObjectClass()
聲明時自動創建
數據訪問
Instance.Data
Instance->Data
Instance.Data
方法調用
Instance.Method()
Instance->Method()
Instance.Method()
復制
指針復制
Instance1=Instance2
Instance1=Instance2
不提供
內容復制
由類自身定義
不提供
缺省,或由類自身定義
比較
指針比較
Instance1==Instance2
Instance1==Instance2
不提供
內容比較
由類自身定義
不提供
缺省,或由類自身定義
銷毀
不再引用時由垃圾收集器自動銷毀
delete Instance
超出作用域時自動銷毀
注:
① C++的"指針使用"一列中並未列出形如*Instance的使用,因為這樣做的實質不是指針使用;
②"指針復制"是指使得兩個對象今後使用相同的一塊內存區域,任何對此區域的修改同時會反映到這兩個對象上;
③"內容復制"則指拷貝兩個對象各自的存儲區域,拷貝後內容相同,但各自保留自己的存儲區,以後對任一者的修改不會影響另一者。
從上表可以看出,除了對象銷毀機制以外,JAVA的對象其實是從C++中的對象和指針共同繼續而來的。
但是,很多極力提倡JAVA語言的人似乎沒有意識到這種關系。他們批評C++指針的概念太難被初學者接受。的確,對初學者來說,接受計算機存儲器和指針的概念並不是輕而易舉的事。事實上,很多程序員都經歷過這樣一個迷惘的階段。但這並不意味著存在一種對存儲器的解釋可以完全避免"指針"這一概念--在JAVA語言中也是如此。現在有很多講解JAVA語言的教材,但真正能夠從頭到尾不出現"指針"或者類似概念(不包括抨擊C++語言時的使用)的,又有幾本呢?
非凡地,JAVA初學者由於理解的障礙,經常提出像這樣的問題:"為什麼像int、float這樣的變量使用前不需要先用new命令來創建而對象卻要呢?為什麼兩個對象互相復制以後,修改其中一個會影響另一個,而像int、float這樣的基本數據類型卻不會呢?兩個值相等的對象,用==比較的結果為什麼是false,它們有什麼是不等呢……"面對這樣的問題,即使許多對JAVA比較熟悉的人有時也說不出個所以然來。究其原因,就是JAVA中的對象從來就沒有離開C++指針的影子,非凡是在創建、復制(事實上,JAVA默認時只提供指針復制)和比較等最常用的操作上。因而使用它們就必須遵循指針的規則,否則將無法為計算機或編程者所理解。在C++中,指針和對象其實是與int、float共通的數據類型,但又各有其特性;繼續到JAVA中以後,二者的特性互相糅合而融為一體,因此對其含義的問題就產生模稜兩可的解釋:JAVA對象有時是對象,有時是指針,但大多數時候是指針。
對C++指針的另一種批評指出,C++答應指針指向任意內存區域,因此輕易引起系統的干擾,即使很有經驗的程序員在使用時也難免產生疏忽。這種批評不無道理,因為大部分C++程序出錯的原因都與指針有關。但由此而批評指針存在的價值是不對的。沒有C++程序員願意從不使用指針。指針是程序設計的一樣利器,凡涉及內存的操作,沒有指針不能做到的,並且它的效率比其他任何替代方法都高。這就是眾多C++程序員寧願冒著高度的出錯風險也堅持使用指針的最大原因,而並不是他們無法避免使用指針。假如真正要像JAVA語言那樣刻意避免指針的話,筆者在後面可以證實,只要他們願意,在C++程序員同樣可以做到,而且性能比JAVA更好。他們可以設計一類徹頭徹尾的C++對象,而他們的使用方法卻與JAVA對象一摸一樣!這恐怕是許多JAVA崇拜者所始料不及的。
本文後面所附的程序,為用戶營造了這樣一個編程環境:只涉及對象使用;避免指針禍害,但卻保持像指針一樣快速高效地訪問內存的優點;像JAVA所倡導的那樣,不須操心對象釋放問題,在不再引用時由系統自動清理。必須強調的是,盡管該程序段理想地模擬出了JAVA的存儲使用環境,編程者卻確確實實在使用C++語言,並不會因此失去C++語言所具備的其他一切高效特性,甚至可以繼續使用其他的指針。
更多內容請看網絡治理實用手冊專題,或
二、Agent類:用C++指針模擬JAVA對象
為了更好的說明C++與JAVA的相似之處,筆者建立了Agent類。它通過把一個特定對象類型的指針作為自己的保護成員,來實現對C++指針的包裝。
任何使用Agent類模擬JAVA的程序必須通過如下表所示的方法來使用對象:
表2
操作
JAVA
使用Agent後的C++
聲明
ObjectClass Instance
Agent<ObjectClass> Instance
創建
Instance=new ObjectClass()
Instance=new Agent<ObjectClass>
數據訪問
Instance.Data
Instance().Data
方法調用
Instance.Method()
Instance().Method()
復制
指針復制
Instance1=Instance2
內容復制
由類自身定義
比較
指針比較
Instance1==Instance2
內容比較
由類自身定義
銷毀
不需要,由程序內部自動治理
上表顯示了兩種對象在使用上的驚人的相似性。本質上,兩種對象是一樣的,因為JAVA解釋器本身就是使用與Agent相似的實現方法。但有些形式上的相似性事實上是無法做到的,因為它們究竟屬於兩種不同的語言,必須依照各自的規定。以下是兩種需要注重的形式上的差別:在C++中,
1、 象所屬類的名稱(如ObjectClass)必須放在Agent後,用〈 〉包括起來(否則,該類將與本文所討論的Agent類毫無關系);
2、 對象本身數據的訪問和方法的調用必須在對象表識符後加一對括號,如Instance().Method(),因為Agent重定義了操作符operator (),以幫助編譯器將一個Agent的實例(如Instance)解釋成用戶所使用的具體某一個類(如ObjectClass)的實例,而Instance().Method()這一調用本身也等價於((ObjectClass&)Instance).Method()。
另外,任一使用了Agent的程序必須在首部加入#include "Agent.h"才能實現對它的訪問。
以下為包含類Agent全部定義的C++頭文件。由於該文件篇幅較小,所有Agent的方法均采用內聯函數的形式定義。
#ifndef OBJECT_AGENT_CLASS
#define OBJECT_AGENT_CLASS
#define null 0
template<class ObjectType>
class Agent
{
int *Reference;
static bool bNewOperation;
protected:
ObjectType *Marrow;
void Finalize()
{
if (Reference)
{
(*Reference)--;
if (Marrow)
{
if (*Reference<=0 && Marrow)
{
delete Marrow;
delete Reference;
}
Marrow=null;
}
Reference=null;
}
}
public:
// constrUCtors
Agent()
{
if (bNewOperation)
{
Marrow=new ObjectType;
Reference=new int;
*Reference=1;
bNewOperation=false;
}
else
{
Marrow=null;
Reference=null;
}
}
Agent(ObjectType obj)
{
Marrow=new ObjectType;
Reference=new int;
*Reference=1;
*Marrow=obj;
}
// destructor
~Agent() { Finalize(); }
// convertions
operator ObjectType&() { return *Marrow; }
// operators
Agent<ObjectType>& operator=(Agent<ObjectType> obj)
{
Finalize();
Marrow=obj.Marrow;
Reference=obj.Reference;
(*Reference)++;
return *this;
}
Agent<ObjectType>& operator=(Agent<ObjectType>* obj)
{
Finalize();
if (obj)
{
Marrow=obj->Marrow;
Reference=obj->Reference;
}
return *this;
}
bool operator ==(Agent<ObjectType> obj) const
{
return Marrow==obj.Marrow;
}
bool operator !=(Agent<ObjectType> obj) const
{
return Marrow!=obj.Marrow;
}
ObjectType& operator ()() { return *Marrow; }
void *operator new(size_t size)
{
bNewOperation=true;
return new char[size];
}
};
template<class ObjectType> bool Agent<ObjectType>::bNewOperation=false;
#endif
從源程序中可以看出,Agent類實際上是一個模版(template)。這樣做的好處是,用戶不必為包容自己不同類型的對象而定義之相對應的Agent類。
Agent類的工作原理是這樣的:
當用戶使用Agent<ObjectClass>來定義對象的類型時,他事實上定義的是一個Agent類型的對象。編譯器自動為該對象產生一個類型為ObjectClass*,名為Marrow的成員,而ObjectClass* Agent::Marrow才真正代表用戶將要建立的對象。整型成員Reference記錄當前對此對象的Marrow的引用個數,當降為0時自動消除Marrow,即銷毀用戶定義的ObjectClass實例。當用戶調用Instance=new Agent<ObjectClass>來創建對象時,分配Marrow,Reference置1,對象處於可使用狀態。兩個Agent實例互相賦值時,重定義的操作符operator =協助復制Marrow指針,更改公共的Reference成員,使之指示正確的引用計數。還有一個必須注重的靜態成員:bNewOperation。它是一個布爾(真假值)類型的變量。當程序員用Agent<ObjectClass> Instance來定義對象時,其值為false,指出不需要真正建立Agent::Marrow的實例,因而Marrow將被賦值為null;當用Instance=new Agent<ObjectClass>來創建對象實例時,其值為true,提示構造函數建立相應Marrow的實例。其控制通過操作符operator new實現。而操作符operator ()則使得,當調用Instance()時,編譯器自動返回ObjectClass類型的*Marrow,所以調用用戶所需的對象成員可以使用Instance().Data和Instance().Method()。
更多內容請看網絡治理實用手冊專題,或
三、應用例子
下面的JAVA與C++例子程序執行同樣的操作:建立100000個xy類型的對象,保存到一個對象數組中,釋放內存廢區,如此重復10次,在結束時顯示各自運行的時間。這個例子可以幫助讀者了解兩種語言的差異。
1、JAVA程序如下:
public class xy //一個簡單的數據類
{
int x, y;
}
public class TestTime {
static int OBJECTS=100000;
static int CHECKTIMES=10;
public static void main(String[] args) {
xy[] obj=new xy[OBJECTS];
long start, end;
long total=0, max=0, min=OBJECTS*CHECKTIMES, time;
System.out.print("PROGRESS: ");
for (int j=0;j<CHECKTIMES;j++) {
System.out.print(".");
start=System.currentTimeMillis();
for (int i=0;i<OBJECTS;i++) obj[i]=new xy();
if (j>0) System.gc(); //從第二次循環開始強制回收內存廢區
end=System.currentTimeMillis();
time=end-start;
total+=time;
if (time<min) min=time;
if (time>max) max=time;
}
System.out.print("FINISHED!
Minimum time in 1 check: "+min+" Milliseconds");
System.out.print("
Maximum time in 1 check: "+max+" Milliseconds");
System.out.print("
Average time in 1 check: "+total/CHECKTIMES+" Milliseconds");
System.out.print("
Total time in "+CHECKTIMES+" checks: "+total+" Milliseconds");
}
}
2、使用Agent類後的C++程序:
#include "stdio.h"
#include "time.h"
#include "Agent.h"
#define OBJECTS 100000
#define CHECKTIMES 10
class xy //一個簡單的數據類
{
int x,y;
};
void main() {
Agent<xy> obj[OBJECTS]; //數組自動創建,不須使用new
clock_t start, end;
unsigned long total=0, max=0, min=-1, time;
printf("PROGRESS: ");
for (int j=0;j<CHECKTIMES;j++) {
printf(".");
start=clock();
for (int i=0;i<OBJECTS;i++)
obj[i]=new Agent<xy>; //operator new和構造函數被調用,創建Marrow
//對象被重新賦值時自動釋放,不須像System.gc()這樣的語句強制實施
end=clock();
time=(end-start)*1000/CLOCKS_PER_SEC;
total+=time;
if (time<min) min=time;
if (time>max) max=time;
}
printf("FINISHED!
Minimum time in 1 check: %d Milliseconds", min);
printf("
Maximum time in 1 check: %d Milliseconds", max);
printf("
Average time in 1 check: %d Milliseconds", total/CHECKTIMES);
printf("
Total time in %d checks: %d Milliseconds", CHECKTIMES, total);
}
程序運行結果:
更多內容請看網絡治理實用手冊專題,或 四、程序結果的分析與比較
以下從靈活性、易用性和效率三方面對JAVA中的對象和使用Agent以後的C++對象的使用情況進行分析和比較。
1、靈活性
<!-- frame contents -->
<!-- /frame contents -->
由於Agent類直接包含了C++的對象指針Marrow,所以任何C++的指針操作均可應用於Agent類上。從它派生出來的任何子類都可以對這一受保護的對象指針實施所需要的操作,使得用戶獲得了最大限度的靈活性。但就Agent本身而言,並不答應外部程序訪問Marrow,故不存在很多人擔心的指針誤用的問題,因而是安全的。此外,這樣做把對象的治理和使用分開來。就是說,譬如有對象Agent<ObjectClass> Instance,則Instance.Method ()指調用對象中屬於Agent類的對所有對象類型均適用的方法(如存儲治理、對象數據維護等功能,用戶可以在Agent的派生類中自行添加),而Instance().Method()則調用屬於ObjectClass本身用於實施具體操作的成員(對於繪圖類的繪圖操作、打印類的打印操作等等)。通過這種方法,有需要的程序員可以參與系統內部的對象治理機制,而一般的編程者則可以避免誤用指針的煩惱。這種C++特有的靈活性和適應性是JAVA所不能具備的。
2、易用性
使用Agent類與JAVA的主要區別僅在於上文列舉的兩處形式上的不同點。對Agent不作任何繼續和更改的情況下,程序員可以像正在使用JAVA那樣,不須關心內存分配和清理,不須深入了解指針的概念,依照JAVA的步驟來使用對象。所以二者在此方面相當。
3、運行效率
這是大多數程序員最關心的。下面有兩組測試數據,分別是上述例子C++程序(使用Microsoft Visual C++ 6.0編譯運行)與JAVA程序(分別運行於IBM VisualAge for JAVA 3.0和Borland J Builder 3.0下)在兩台不同型號的計算機上的運行結果,使用的操作系統為 Microsoft Windows 98,且保證測試過程中沒有出現影響結果准確性的明顯讀磁盤現象。從表中的數據可以很清楚地看到,使用Agent類之後,C++的程序仍然比JAVA程序快很多。這是由於編譯型語言對於解釋型語言在速度上具有一貫的優勢;另外, C++程序並不需要創建一個獨立的線程來治理資源,因此其運行開銷比JAVA更小。
表3
計算機配置
VisualAge for JAVA 3.0
JBuilder 3.0
使用Agent後Visual C++ 6.0
最小運行時間(ms)
最大運行時間(ms)
平均運行時間(ms)
總共運行時間(ms)
最小運行時間(ms)
最大運行時間(ms)
平均運行時間(ms)
總共運行時間(ms)
最小運行時間(ms)
最大運行時間(ms)
平均運行時間(ms)
總共運行時間(ms)
Duron 700
128M RAM
1335
2008
1512
15122
220
270
225
2250
110
170
143
1430
Celeron 333
64M RAM
2504
4988
3778
37784
440
550
466
4660
160
280
258
2580
從這三方面比較可以看出,在對象使用上,JAVA的表現並不如預期的理想,而C++亦並不如JAVA廣告中所指出的那麼差強人意。JAVA摒棄指針使用的根本原因就在於JAVA必須實現其跨平台使用的優點。雖然,JAVA的任何缺點都不能掩蓋這個優點,然而,為了實現這個其他眾多語言都不可能達到的優點,它作出了巨大的犧牲:假如網絡計算機上任一塊內存都可以被遠程程序訪問的話,網絡的安全性和穩定性就無法保障。放棄指針和代理內存的分配和回收等等措施的產生,很大程度上是由於把這些操作留給用戶實在不適應網絡編程的要求。出於類似的考慮,除了內存資源以外,JAVA解釋器也為用戶代管了大多數的計算機資源。因此,與其說這些是設計者們精心改良的成果,不如說是設計者們為了適應網絡特性而采取的折中。而這種折中正是JAVA作為一門新興網絡語言得以生存的要害因素。
五、結論
JAVA雖然流行,但並不代表它任何創新的方面都是值得吹噓的;C++成形雖然已經十年,但是它的優越性並未被軟件業發展的潮流所沖垮,C++程序員不必對之失去信心:任何問題都應該全面地、辯證地看待。
更多內容請看網絡治理實用手冊專題,或