新文章

2013年10月29日 星期二

[轉]What are the differences between C and C++?

版上常有人問,C++ / C 有什麼區別? 絕大多數的人都會說沒什麼差別

此言差矣,其實差距是非常大的,相較於 C,C++ 改良與新增了許多功能



Reference : http://www.student.tw/forum23/thread160737.html

我簡單陳述幾點如下:
  • C++ 支援型態安全的 IO 輸出

    相較於 C 使用 printf,C++ 改使用 cout, cin 物件處理輸入輸出

    由於 printf 需要自行指定型態,在輸出的時候很可因為程式設計師的指定錯誤,而產生錯誤的輸出結果

    而 C++ 的 cout 則會自動判斷變數型態,因此較為安全與方便
     
  • C++ 支援較大規模程式開發

    相對於 C,C++ 可適用於更大規模的程式開發

    例如支援 namespace(命名空間),可以讓函數、物件命名上更有彈性
     
  • 左值(Lvalue)與右值(Rvalue)

    在 C 中,大多數的運算結果都是回傳數值(也就是右值)

    而在 C++ 中,則改成回傳記憶體位址(左值)

    因此,++++x 這種運算在 C++ 是合法的而 C 則不被允許
     
  • 引進 bool 型態
     
  • 更安全的變數型態轉換
     
  • 支援函數多載(Function Overloading)

    舉例來說,如果你今天寫了一個處理整數開平方根的函數

    代碼:
    int sqare(int);
    但你又需要一個能處理浮點數的函數,那你應該是要重寫一個呢

    還是將原本的函數改成支援浮點數的版本,無論是哪一個都相當不方便。

    因此 C++ 中允許你做這樣的宣告

    代碼:
    int sqare(int);
    float sqare(float);
    double sqare(double);
    他會視你傳入的參數決定要呼叫哪個函數,此即為函數多載(Function Overloading)
     
  • 支援 inline function

    許多較短的函數,若能合併編譯便能提升許多速度,例如這一個判斷奇數的

    代碼:
    bool even(int n) { return (n&1)==0; }
    這個函數相當短,但程式執行時仍需要花費時間在參數傳遞與結果回傳上,若能改成

    代碼:
    inline bool even(int n) { return (n&1)==0; }
    則編譯器會將此段程式碼直接編譯到呼叫的程式碼中,不需要再進行傳遞
     
  • 函數模板

    在許多時候一個函數可能需要處理很多不同的參數型態

    雖然我們可以用 function overloading 解決此問題,但這樣你就必須撰寫許多不同版本的程式碼

    而函數模板則允許你不將 function 回傳與傳入型態寫死,而是在你實際呼叫的時候才指定

    編譯器會在編譯時幫你自動產生對應的 function 減少開發者的困擾。
     
  • Reference 參數傳遞型態

    參數傳遞進入 function 時通常是在記憶體中複製一份資料到新的空間

    但若參數資料量龐大,複製會花費很多記憶體空間與時間(回傳也是)

    因此, C++ 中如果參數傳遞時加上 & 符號,則他僅會傳入 address,不會重新複製一份
     
  • 支援函數指標 (略)
     
  • 支援 物件導向程式設計 概念

    我想這是 C++ 最困難也最強大的部分,前面所提及的部分頂多是 C++ 改善 C 的基本項目

    而 class 則是 C++ 相對於 C 全新的概念,因此很難三言兩語說完,故暫時不說XD


以我修課的感覺來說,C 僅包含 1/3 C++ 的內容,因此學習 C++ 的難度與時間可想而知

但是否只有 C++ 才能寫出程式呢? 當然不是這樣,C++ 可以做的,C 也完全可以,C++ 改善的是程式開發的效率與難度

事實上在許多記憶體與運算資源有限的環境下,相對於 C++,C 仍是最佳的選擇

所以到底要學哪個好? 其實都好,反正程式設計重視觀念,等學會以後,要再學習其他語言都是很快速的

-----------------------------------------------------------------------------------------------------
一、強化「型別安全」--對型別系統的全面改進

許多涉及語法細節之處就略過了。在此只提出一個較重要的部份,是關於 C++ 與 C 的根本不同之處:

int *v = ...;
void *p = v;
int *p2 = p; // 合法的 C 程式碼,但在 C++ 中不合法

簡單的說,C++ 不允許 void * 隱式轉換為任意型別 T 的指標。但在C 語言中,這是合法的。

C++ 禁止上述操作的理由,是為了強化「型別安全」。程式中一旦使用void *,就等於自動放棄了編譯器對型別的自動檢查與核對動作,也就是放棄了型別安全。而明知不好,C++ 仍然支援 void * 這種用法的原因,主要是為了兼容於 C,但由於 void * 隱式換為任意型別的 T *,這種用法實在太危險,所以在 C++ 中被禁止了。

理想的 C++ 程式,是不應該出現 void * 這種用法的。C++ 之父 B.S.就曾指出,除了低階程式之外,應該儘量避免使用 void *,如果非得用 void * 不可,通常代表你的設計出了某些問題。

仔細觀察,C++ 的每一項基礎設施,都有提升型別安全的意味在其中。
例如:

1引入 bool 型別,避免混淆。(主要問題在函式 overload 時)

2鼓勵以 0 而非自行定義的 NULL 巨集等代表空指標。(B.S.大和另一位 Herb Sutter 大,在 2003 年底提出新增加 nullptr 關鍵字,但不曉得 C++03 是否有通過)。

3引入 const,讓「常數性」成為與型別不可分割的一部份,除了提升安全,讓編譯器承擔檢核的責任之外,也有助於代碼的優化。(因此後來 C 語言也跟進採用。)

4引入 const, inline 等用法,減少非必要巨集的使用。(因為展開巨集是預處理器的動作,沒有通過編譯器,也就沒有型別安全可言)。

5引入 reference 機制,簡化指標的語法,並有效減少指標(尤其是兩層以上的複雜指標)的使用。

6引入 new 和 delete,取代 malloc 和 free,把動態記憶體配置的工作,提升至語言層級,減少強制轉型的使用(另一主要目的是為了配合 operator overloading,提升介面的一致性)。

7引入新的 static_cast, const_cast 等關鍵字,鼓勵儘量減少強制轉型的使用。

8引入 function/operator overloading 機制,讓同名函式及各種運算子,可依據不同的操作型別,實現不同的動作。強調「型別」也是函式具名的一部份,達成介面一致性,並使 UDT 能像內建型別的操作一樣自然。

這些每一個小地方,都可以看出 C++ 為了強化「型別安全」,所付出的用心和努力,雖然除了禁止 void * 的隱式轉型之外,基本上沒有限制C++ 使用者延用舊的 C 語言的舊式習慣寫法,但筆者認為,了解型別系統的特性,並隨時意識著「型別安全」,是掌握良好 C++ 編程風格的最重要觀念。


二、在「思維方法」上的差異

程式語言處理的不外乎資料結構及演算法,STL 的發明人也說過:「程式基於精確的數學。」前面提過,C 語言偉大之處,就是它十分良好地對映到機器模型,免除了直接使用機器語言的晦澀。

也就是說,C 程式人員不必去操心 register 管理、記憶體定址等等極度低階的細節問題。其所思考的,多半像是「我應該用什麼演算法,把某幾段特定記憶體內的資料取出來,經過怎樣的運算後,再存到特定的記憶體區段去……。」這種把運算和存取操作的細部具體動作,轉換為抽象的數學思考的流程,本質上仍然是非常貼近機器模型的。而這樣的風格,不僅反映在 C 程式碼上,更多半根深蒂固地植入 C 程式人員的思維方式內。

隨著資訊科學的發展,愈來愈多的應用問題,需要利用編寫程式來處理;人們發現,大部份應用程式所使用的演算法和資料結構,是極為有限的。另一方面,編寫程式語言的常用技巧,卻已經累積地相當成熟了,程式人員需要付出更多心力的,不再是某個典型的演算法或資料結構,應該如何實現,如何處理;而在於,如何將問題的本身,適當地轉換為程式語言。

因此,一種讓程式語言能夠以「貼近待解決的問題」的方式來思考,而不再只是侷現於「貼近機器模型」的思想,就應運而生。簡單地說,它就是起源於 70 年代(甚至更早),在 80~90 年代開始快速發展,直至今日,雖不再新鮮,卻仍屬方興未艾的「物件導向」的觀念。

由於物件導向(OO: Object-Orientd)的觀念是如此氾濫,甚至已經上升到哲學的層次,幾乎沒有一個比較新的語言(80年代以後),不支援它的特性,所以這裏也就不多介紹了。只是要指出一點, C++ 也好,或其他支援物件導向特性的編程語言也好,它們與 C 語言最大的分別,並不在語法或功能的區別上,而是在於看待問題的基本思考方式,也就是所謂「思維方法」上的差異。


三、multi-paradigm

C++ 和 C 語言,在觀念上最大的不同之處,就是,C++ 是支持 multi-paradigm 的編程語言。如下面所示,C 語言及傳統的 Pascal 語言,是所謂 procedual-based 的編程語言,而 Java, C# 等較新的語言,則是 object-oriented 的編程語言(OOPL)。

至於 C++,它實際上是個支援 multi-paradigm 的編程語言,因為它不僅保留了 C 的程序導向的編程,更重要的是它沒有沒有為了要支援 OO,而破壞基於 C 語言的靜態型別系統,因此它提供的 ADT(abstract datatype)機制,與繼承和執行期繫結等 OO 特性的機制是互相獨立的。這使得 C++ 在 OO 的執行期多型之外,罕有地提供了強大的編譯期多型的機制,也就是一般稱為「泛型編程」的技術。

procedual-based(eg: C, Pascal...)

object-oriented(eg: Objective C, Object Pascal, Java, C#...)

C++: procedual-based  object-based(ADT)
           \          /      \
            \        /        \
             \      /          \
             generic     object-oriented(OO)


由上面的簡單示意圖可看出,泛型(generic)的編譯期多型的特性,不止對應在 ADT 上,也可以直接對應到程序導向的編程,例如 C++ 標準程式庫所提供的泛型演算法,就大部份是以函式而不是 class 來呈現的,實際上,整個 C++ Standard Library,除了 I/O 的部份,幾乎完全沒有用到 OO 的執行期多型的特性(更多的是 ADT 和 template)。

此外,或許有人會提出,其實 Java 或 C# 也是支援 generic 編程的,是沒錯,Java 也有類似 C++ 的樣板容器的功能,但實際上是用「代換法」做的,並沒有真正產生新的型別,因此它無法達到 C++ template 那可以有型別客製化(特殊化: specialization),或與其他抽象化機制合作(例如繼承、甚至遞迴)的多樣化的能力,並不算真正意義上的編譯期多型。實際上,Java 和 C# 語言所採行的單根繼承的泛化型別系統,早就先天限定它們不適合朝編譯期多型的方向發展,它們比較接近純粹的 OOPL。

C 語言的思考方式偏重於資料運算和記憶體存取的動作,物件導向的思考方式,則是將問題分解成不同的抽象概念(class),讓使用者專注在概念與概念間之的關聯,能從一個整體的大的方向,去關注問題,避免過早陷入細節,見樹而不見林。

同時,良好的設計,是當需求有所改變時,只需要修改、調整部份的模組,就可以完成工作,不必整體性的翻修,牽一髮而動全身。這也是物件導向設計的重要精神,有一個專門的領域 DPs(Design Patterns),它與特定程式語言無關,就是在研究面對各種問題需求的典型解決方式,現在學物件導向設計一定會接觸到它。

至於,C++「多思維面向」(multi-paradigm)的特性,又是如何影響編程的思考方式呢?

這裏舉個《Modern C++ Design》第七章的例子。Smart Pointer 的發展動機,是為了防止直接操作指標所帶來的危險性,但隨著各種不同的需求,它的實作細節也就有所不同。例如:它能不能與其他容器類(例如標準
程式庫中的 vector, list 等)共用,以及使用的細節如何?是否允許取得原始指標?是否對各種操作動作進行檢查,如何檢查?甚至,是否支援多緒程式安全地操作……等等。

如果將各種需求組合都列出清單,再一個一個實作,勢必沒完沒了。最理想的方式,是讓程式員自由選擇各種「需求策略」,讓編譯器自動產生相應的程式碼。這種設計乍看來是遙不可及的理想,但實際上已經做到了。
這就是 Loki 函式庫所提供的實作品 class template SmartPtr:

template
<
  typename T,
  template <class> class OwnershipPolicy = RefCounted,
  class ConversionPolicy = DisallowConversion,
  template <class> class CheckingPolicy = AssertCheck,
  template <class> class StoragePolicy = DefaultSPStorage
>
class SmartPtr;

由於牽涉的選擇項目過多,這裏只解釋 OwnershipPolicy,也就是實際物件擁有權的策略,它預設是 RefCounted,也就是參用計數的規則。但也可以依據需求的不同,選擇其他的擁有權策略,例如:RefCountedMT、DestructiveCopy、DeepCopy、……等等。使用方式如下:

class User {...};

typedef SmartPtr<User, RefCounted> UserPtr;

如此,UserPtr 就變成類似 boost::shared_ptr<User> 的作用,可以和標準容器合作,而實現 Java、C# 語言常見的功能。又假如:

class Manager {...};
typedef SmartPtr<Manager, DestructiveCopy> ManagerPtr;

現在,MangerPtr 則和 std::auto_ptr<Manager> 一樣,採取所謂「摧毀式複製」的語義,也就是同時只有一個 ManagerPtr 可以真正操縱同一份Manager 類型的實體物件。
實際上,SmartPtr 的實現牽涉到 ADT、多重繼承、編譯期多型等等的特性,它應用了一種叫 policy-based 的設計觀念。這與其他程式語言或是DPs 所標榜的 OO 的特性,或所謂「良好設計」的最終目的,並沒有不同,同樣是將不同的概念獨立分解,再巧妙組合起來。只不過,在 C++ 中,除了傳統 OO 執行期多型的技術之外,還多了強大的編譯期多型的支援,使得不僅「物件」(資料結構和演算法),可以在執行期被彈性處理,就
連「型別」(概念)的本身,在編譯期,也可以自由的選取整合。這對程式碼編寫的簡潔、靈活性和執行效率,都能帶來很大的提升。


沒有留言:

張貼留言