C++之父:C++ 的五個普遍誤解(1)

Sheng Gordon發表於2014-12-22

[編注:為了增加您冬天閱讀的樂趣,我們很榮幸的奉上Bjarne Stroustrup大神的這個包含3個部分的系列文章。這是第一部分;第二第三部分將在接下來的兩個週一釋出,即在聖誕節之前完成這個系列。請欣賞。——Ed]

1. 簡介

本系列包括 3 篇文章,我將向大家展示並澄清關於C++的五個普遍的誤解:

  • 1. “要理解C++,你必須先學習C”
  • 2. “C++是一門物件導向的語言”
  • 3. “為了軟體可靠性,你需要垃圾回收”
  • 4. “為了效率,你必須編寫底層程式碼”
  • 5. “C++只適用於大型、複雜的程式”

如果你深信上述誤解中的任何一個,或者有同事深信不疑,那麼這篇短文正是為你而寫。對某些人,某些任務,在某些時間,其中一些誤解曾經只是正確的。然而,在如今的C++,應用廣泛使用的最先進的ISO C++ 2011編譯器和工具,它們只是誤解。

我認為這些誤解是“普遍的”,是因為我經常聽到。偶爾,它們有原因來支援,但是它們經常地被作為明顯的、不需要理由的支援地表達出來。有時,它們成為某些場景下不考慮使用C++的理由。

每一個誤解,都需要一大篇文章,甚至一本書來澄清,但是這裡我的目標很簡單,就是丟擲問題,並簡明地陳述我的原因。

2. 誤解1:“要理解C++,你必須先學習C”

不。學習C++基礎程式設計比學習C要容易地多。

C幾乎是C++的一個子集,但是它不是最先要學習的最好的子集,因為C缺少計數支援,型別安全,和易用的標準庫,而C++為簡單任務提供了這些。考慮一個拼接email地址的簡單函式:

它可能被這樣使用

而C語言版本需要顯式地字元複製和顯式地記憶體管理:

它可能被這樣使用

哪個版本你更願意教?哪個版本更容易使用呢?剛才我的C版本程式碼寫對了嗎?你確定?為什麼?

最後,哪個版本可能更有效率呢?是的,是C++版本。因為它不需要數引數中字元的個數,也不需要釋放引數中短字串的空間(動態記憶體)。

2.1 學習C++

這不是一個奇怪的孤立例子。我認為它是典型的。那為什麼有那麼多老師堅持“先學習C”的觀點?
• 因為多年來他們一直這麼做。
• 因為這是課程所要求的。
• 因為老師們年輕時就是這麼學習的。
• 因為C比C++小,就認為C比C++簡單。
• 因為學生們遲早要學習C(或者C++的C子集)。

然而,C並不是最先學習C++的最容易或者最常用的子集。更進一步,一旦你知道了C++的合理數量,C子集很容易學習。先學習C,會導致不斷忍受錯誤,以及學習如何避免這些錯誤,而在C++中很容易避免這些錯誤。

對於用現代的方法教學C++,看我的書Programming: Principles and Practice Using C++。它甚至在結尾有一章,專門講如何使用C。它在多個大學中數以萬計的初學者中成功應用。為了靈活學習,這本書的第二版中使用了C++11和C++14工具。

在C++11[11-12]中,C++對初學者更友好。例如,標準庫中使用元素序列初始化一個vector:

在C++98中,我們只能使用列表來初始化陣列。而在C++11中,我們可以定義一個建構函式,接收可以是任意需要型別的{}初始化列表。

我們能夠使用range-for迴圈來遍歷vector:

它將對v中的每一個元素,呼叫一次test()。

range-for迴圈可以遍歷任意序列,因此我們可以直接使用初始化列表來簡化上面的例子:

C++11的一個目標就是,讓簡單的事情簡單化。自然地,它在沒有增加效能負擔的前提下實現了。

3. 誤解2:“C++是一門物件導向的語言”

不。C++支援OOP和其他程式設計風格,但它並不侷限於狹隘的“物件導向”。它綜合地支援了包括物件導向和泛型程式設計技術。通常,一個問題的最優解決方案,包含不止一種風格(範例)。“最優”,我指的是最短、最易於理解、最有效率和最易於維護等。

“C++是一門物件導向的語言”使人們認為C++不是必要的(當與C做比較時),除非你需要一個巨大的類繼承層次以及很多須函式(執行時多型)——對很多人和很多問題,這樣應用並不合適。相信這個誤區導致C++因為不是純物件導向而遭到譴責;畢竟,如果你把“好”和“物件導向”等同起來,那麼C++明顯包含了很多不是物件導向的東西,一定會被認為是“不好”。不管是哪種情形,這個誤解為不學習C++提供了一個很好的介面。

考慮一個例子:

它是物件導向的嗎?當然是;它關鍵依賴類的虛擬函式機制。它是泛型嗎?當然是;它關鍵依賴於一個引數化容器(vector)和泛型函式for_each。它是函式式嗎?某種程度上;它使用了lambda表示式([]構造器)。那麼它到底是什麼?它是現代C++:C++11。

我使用了range-for和標準庫演算法for_each,就是為了炫一下特性。在真實的程式碼中,不管我使用哪種方式,我只會使用一種迴圈。

3.1 泛型程式設計

你想讓這段程式碼更通用嗎?畢竟,它只在指向Shapes的指標向量中使用。如果使用列表和內建陣列會怎麼樣?“智慧指標”(資源管理指標)呢,例如shared_ptr和unique_ptr?對於那些你可以呼叫draw()和rotate()方法,但是類名稱不是Shape的物件呢?考慮:

對於任意你需要從first遍歷到last的序列,它能夠執行。這就是C++標準庫演算法的風格。我使用了auto,來避免不得不對“類似shape的物件”的介面命名。這是一個C++11特性,即“使用表示式的型別初始化”,因此對於for迴圈,p的型別由第一次賦值的型別決定。使用auto來表示lambda引數型別是C++14的一個特性,但是已經在使用了。

考慮:

這裡,我假設Blob是一個包含draw()和rotate()操作的圖形型別,而Container是某種容器型別。標準庫列表(std::list)有成員函式begin()和end(),來幫助使用者遍歷它序列的元素。這是經典的物件導向程式設計。但是如果Container不支援C++標準演算法庫遍歷半開序列[b:e]的約定呢?如果不包含begin()和end()成員呢?好了,我還從來沒有見過哪些容器類是不能被遍歷的,因此我們可以定義適當語義的獨立begin()和end()函式。這個標準庫支援C樣式的陣列,因此如果Container是C樣式的陣列,問題能夠解決——並且C樣式的陣列仍然很常見。

3.2 適應性

考慮一個更難的情況:如果容器包含物件指標,而且有著不同的訪問和遍歷模式呢?例如,假設你需要這樣訪問一個Container:

這種風格並非不常見。我們可以把它轉化成一個[b:e)序列:

注意這種修改方式是非侵入式的:我不需要修改Container或者它的繼承類,來把Container對映到支援C++標準演算法的遍歷模型。這是一種形式的適應,而不是重構。

我選擇這個例子,是為了展示泛型程式設計技術並不侷限於標準演算法庫(標準演算法中它很常見)。同樣,對大多數常見的“物件導向”定義,它們不是物件導向的。

C++程式碼必須是物件導向的想法(即到處使用類繼承和虛方法),可能嚴重損壞效率。如果你需要一組型別的執行時解決方案,物件導向程式設計的觀點是偉大的。我就經常使用它。然而,它是相對嚴格的(並不是每個型別都適應類繼承),並且虛方法呼叫抑制了內鏈函式(它可以在一些簡單而重要的場景中降低你50倍的速度)。

附言

在我的下一部分,我將講解“為了軟體可靠性,你需要垃圾回收”。

相關文章