面向對象語言還需要指針么?


     大三那會還在搞單片機和MFC,玩的純C系的語言,每天和指針打交道,一切皆指針。有一天,聽說JAVA里沒有了指針,我大驚失色,指針都沒了,這語言還能搞啥?

    后來,類似C#,JAVA的高級面向對象語言用得多了。反過來思考,高級面向對象語言沒有了指針,到底是好事還是壞事?這種區別體現在哪里?本文以C#和C++為例做個對比,JAVA機制和C#類似。與各位共同探討。

     為了簡單,我們先定義一個Point類, 只有X,Y 兩個變量。看看C++和C#之間的使用區別

1. 指針和引用

      C++中,指針和引用的有一定的區別,指針是一個地址,而引用只是別名,引用使用起來要方便得多。因為指針本身是地址,地址當然可以指向任何地方,所以便有了指針的指針,如果再和數組,函數,結構和類聯系上,那簡直就是考驗人的大腦。 C++的引用,定義了就必須在聲明時就初始化,而且不能更改。

      C#中,只有引用,一切“引用型變量”都是引用。但這個引用和C++中的引用不同,它更像一個“地址”。如果你聲明了 Point p, p就是引用。但是這個p可能沒有初始化,你也可以在任何時候改變它。

     點評: 指針本來是好東西,但它太靈活,搞得太復雜了。反正我現在不大喜歡看*和&這類符號。

2.類的構造

     C++中,可以使用兩種方式新建一個變量:

       (1) Point p,  你就構造了一個Point變量。 它處在棧區。

       (2) Point* p=new Point;    它處在堆區:

       最牛的在於,Point points[10]; 這樣的聲明,會直接產生十個Point對象,處於棧區。

      也可以這么定義,Point* ps=new Point[10]; 處於堆區。

    C#中,只有一種方式創建:Point p=new Point();  處於堆區。

3. 交換兩個對象

     大家一定都記得,初學C++時經典的數字交換問題。

     C++中,  兩種做法: void Swap(int* a,int *b)  或者是 void Swap(int& a,int& b) ,這個沒什么好說的

     有意思的是,如果交換兩個對象呢? 如果是void Swap(Point a, Point b),那你就錯了,在執行這個函數時,拷貝構造函數會將你的實參分別拷貝到a,b兩個對象中,處在堆區。當函數返回時,你除了浪費了一堆時間做無謂的拷貝工作外,對象還是原來的對象。  因此還得是 void Swap(Point* a, Point *b)或者 void Swap(Point& a, Point& b)。

     C#中,交換值類型時,可以使用void Swap(ref int a, ref int b), 如果是引用類型,直接使用void Swap(Point a, Point b)就行了。   這句話是錯的,交換對象,也必須使用ref關鍵字,因為a,b這兩個引用,依舊處在棧上,它不能影響原有狀態

     點評:你會發現,C++中根本就沒有值類型和引用類型這回事,加上*,&才是“引用類型”,否則就是值類型,值類型傳遞,就會調用拷貝構造函數,造成一定的性能損失。而C#不存在這個問題。

4.函數返回值與工廠模式

      設計模式的構造模式中,工廠是最常見的,你可以非常方便的寫一個C#版本的工廠,但在C++中,怎么實現工廠?

Point GetProduct()
      {
          Point p; //設置為默認值就可以了。
          return p;
      }

   調用時 Point *p= &GetProduct();

   你覺得這樣對么?留給讀者討論。

5.類的組合和對象拷貝

     類可以組合,也可以繼承。但在C++中,類組合會帶來額外的問題,考慮如下的結構:

class PointEx
 {
     public:
       Point A;
       Point B;
  //其他成員和函數
 }

       那么你聲明一個PointEx pex, 那么A,B兩個對象就會被自動創建。如果希望能在構造函數中傳遞A,B兩個類的參數,那必須使用“內部對象構造函數”,具體細節參考普通C++教材。而對象拷貝時,也帶來了額外的隱患,默認的拷貝構造函數只能拷貝普通類型,但這些組合的成員,如果直接=的話,那么兩個PointEx可能就指向同一個Point A。

      在C#中,不存在這樣的問題,首先A,B在PointEx的構造中,根本就不會被構造。除非你在PointEx中顯式的new出A和B。

      除此之外,C++中,子類和父類的構造函數與析構函數的執行順序相當重要。C#中,析構函數被取消了(因為GC),代之以Dispose模式。

6. 函數指針和委托

     首先,函數指針是無敵強大的,因為存儲區分為堆,棧,代碼區和全局數據區。所以代碼區也是能被指針訪問的,因此函數指針指向代碼區,就能把函數流程指向那個區域。這是函數指針的實質。 

     C++中: 你可以這樣定義函數指針 int (*sqrt) (int x);   (看着真惡心,你還沒見函數指針數組,返回函數指針的函數等等喪心病狂的類型) 。可以這樣賦值:

      先隨便定義一個函數sqrt2, 然后func= sqrt2;就可以了,

    C#中:可以這樣定義委托 delegate int sqrt(int x); 賦值類似。 當然還有更牛的“事件”,實現了多播委托(我理解就是委托數組)

    點評:  委托的最大好處是強類型的,而且看着比函數指針優雅的多,功能也更強。

       最近,我認識到了函數式編程的強大。如果能認識到“函數也是變量”,那么就是一個很大的進步。

7. 指針類型轉換

    先說普通值類型的指針類型轉換。

    C++中: 定義兩個指針, int*a 和float*b . 兩個是不能直接賦值的。要想賦值,只能采用空指針作為中介:

void *pv= a;  
   b= static_cast<float>(pv);

    如果是對象,而且是有繼承關系的,例如Point3D繼承於Point,那么從Point*到Point3D*如何轉換呢?

   C#中,可以采用強制類型轉換或as關鍵字:

    Point3D p3d= (Point3D)p2d;  或者是 Point3D p3d= p2d as Point3D;

    點評: 類型是任何一門語言中都非常復雜的一部分。 C#有強大得多的類型系統和元數據模型,處理起來要更高級一些。 不過,我有個疑問,C++中,什么區域存儲一個變量的類型?機器怎么知道某一塊區域內的內存是int而不是double? 難道存儲在代碼區?

  8. 類和結構體的區別

     C++中,類和結構體本質幾乎沒有區別,結構體也能定義函數,定義不同的變量成員,只是所有成員都是公開的;而類有訪問控制,可以實現繼承和派生。

    C#中:類是引用類型,結構體是值類型。結構體作為參數或返回值,都需要做拷貝。結構還可以包含構造函數常量字段方法屬性索引器運算符事件嵌套類型,但如果同時需要上述幾種成員,則應當考慮改為使用類作為類型。結構可以實現接口,但它們無法繼承另一個結構。 因此,結構成員無法聲明為 protected。(來自MSDN, 感謝沖殺同學的糾錯)

  導致這些區別的原因:內存分配

   其實,說這么多,導致C#和C++的 這些區別的本質原因,在於它們內存分配機制的不同

  C++中,不論是對象還是普通的值,如果是通過 Point p這樣的語法生成的話,那么就在棧上。一旦函數結束,棧就被回收。只有new關鍵字生成的對象,才放在堆上。

   放在棧上的數據因為隨時可能被回收,才需要這么復雜的指針機制。指針的地址問題,這就像裝盒子一樣,一個盒子不安全,就多套幾層盒子,盒子越套越多,搞得越來越復雜。

   C#中,有值類型和引用類型的區別。所有的引用類型,都在堆上,回收靠GC處理。普通函數中定義的值類型都在棧上。如果是對象當中的“值”,那當然也定義在堆上。因為堆比棧方便多了,一個地址就可以了。所以使用起來要比在棧上方便的多。

   要說性能,當然是棧比堆快,和內存訪問的集中性有關,棧的數據基本都在高速緩存當中,命中率極高。但堆卻不一定。 正是因為GC的作用,允許引用類型就定義在堆上。當然,我相信對於C#這樣的語言,在編譯或運行時應該會計算哪些是數據訪問熱點,從而優化命中率。  C#這類高級語言,是靠一個強大的“運行時”(runtime)和虛擬機來幫助它實現了這類區別。

   我現在還不是很清楚,堆和棧的比例是怎么分配的。以前搞單片機的時候,會有一個編譯選項選擇它們的比例,我一般會把棧內存(heap)拉到90%,我不願意用堆,因為覺得麻煩。

那么,高級面向對象語言需要指針么?

    有了上面的那一段,估計大家都有答案了。因為有了強大的運行時支持,有了垃圾回收器,使得堆的使用率比棧大的多。雖然性能上會有那么一點點損失吧,但帶來的確實是代碼的簡潔,高效,程序員再也不需要玩指針的游戲了。它帶來了以下好處:

       1. 省略了拷貝構造函數和析構函數

       2. 簡化了函數的參數和返回值,不需要再去調用拷貝函數了,性能有所提升。

       3. 簡化了對象組合的復雜性,C++內存管理本來就夠復雜了,面向對象會變得更加復雜,稍微不注意就會造成內存泄露和指針懸掛等。而在C#中,你可以放心大膽的使用組合和繼承。思路會清晰的多。

       4.C#,JAVA代碼好看得多,起碼沒有那些奇怪的符號。

       5. 其他我還沒有想到的好處。

     從這個角度來說,指針的作用已經被“引用”類型代替了

     但是,C++不需要運行時和虛擬機,直接編譯為原生代碼,性能肯定更好,但確實C++難學。C++大牛肯定能列出一堆C++內存機制的好處,這個見仁見智了。

     還有些遺留的問題,當開啟C#的unsafe選項后,可以在C#里直接寫C++,那么這種內存管理是如何完成的?

     歡迎大家討論!

 

     夜深了, 博主有點暈,給大家講個笑話:燙燙燙燙燙燙,屯屯屯屯屯屯屯,笑話講完了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM