這幾年在公司一直帶徒弟,每次必教的內容就是C++。在我看來,C++已經有非常好的教材了(注1),實在沒有必要從頭教起。自學就可以了,可是結果總是不盡人意。
不想再重復一次“把C++當成一門新語言來學習”,自己直接教吧。
總論
C++是一門實踐的編程語言,它由數十位工業界的大佬們共同設計出來,它是一種至力於解決問題的語言。我們在學習的過程中,同樣也不應糾結於細節,而是專注於如何優雅的解決問題。
C++要解決的基本問題有兩個,一是如何處理好int類型;二是如何處理好vector類型。在解決這兩個問題的時候,C++的設計者們遇到了相當多的細節問題,不過它們都已經被記錄在了C++ programming language一書中。幸好我們不用設計語言,這些問題我們不用再去思考,只需要知道如何做最好(best practice).
在一切開始之前,我們先明確什么是類型(type)?
類型可以粗略的理解成一個概念,比如自然數,動物或宇宙。概念可以由一系列實際的物品舉例,但很完全例舉。我們說1,2,3,4等都是自然數,但不能把所有自然數都例舉出來。這些實例我們稱為類型的值。我們可以說1,2,3等大於-32768,小於32767的整數都是int類型,也可以說所有一維有序的數列都是vector類型。同樣,可以說int類型的值是所有大於-32768,小於32767的整數。
類型說明這些值之中包括一些關系,計算機科學上也稱為操作,比如1+1=2。int類型可以包括算術操作,位操作等等。
計算機中,值都是以內存中的bit來表示,一片保存了值的內存,我們可稱為一個對象,為了方便使用,我們為對象取名,以說明它保存了什么用途的值,這個有名字的對象,我們叫變量。
對於有經驗的程序員可以關注一下以下名詞,來自於A tour of C++。
- 類型(為對象)定義了一組可能的值以及一組可能的操作。
- 值是一組bit的組合,其含義由類型來解釋。
- 對象是一片內存,保存了某個類型的一個值。
- 變量是一個有名字的對象。
其次我們需要了解什么是編程?
解決問題的一種方式是依次執行一系列步驟。計算機解決問題正是這種方式,不過它有一些限制。首先,它的每個步驟必須已經定義,我們稱為這些已經定義的步驟為基本操作。其次,它的步驟必須有限,不能無窮多。
編程的核心內容就是把問題用有限次的基本操作解決。這是一個很難的工作,同時具有工程特點和藝術性。
在一切編程之前,我們先要定義一個基本操作集合,它可能是一個CPU的指令集,也可以是C++的運行時環境。
如果有這樣一個計算機,它除了可以進行整數運算外,還可以接收一個數值n,輸出一定數量的1分、2分、5分的硬幣,並且數量足夠多,以足成n分錢。我們可以這樣解決這個問題(注2):
int five-cent-count = n / 5; output-5-cents(five-cent-count); int two-cent-count = (n - 5*five-cent-count) / 2; output-2-cents(two-cent-count); int one-cent-count = n - 5*five-cent-count - 2 * two-cent-count; output-1-cents(one-cent-count);
上面是一個有效C或者C++程序的主體部分,中間部分步驟以int開頭,它說明了隨后是一個變量,其類型為int。
我們還可以定義這樣的一個計算機,它的類型Node的值,包括兩個部分:data和 ptr。data為int類型,ptr為另一個Node類型的對象位置(指針)。除了int類型的操作外,我們可能的操作還包括:
- cons(data, ptr) : 由data和ptr來構成一個Node類型的值,並返回對象的位置。
- first(node): 取node的data部分的值。
- rest(node): 取node的ptr部分的值,即指向另一個Node對象的位置。
這個神奇的計算機與我們常見的有像大海一樣連續內存的計算機不一樣,它的數據像是保存在海島上,你需要一張機票才能到達。但是,使用這樣的計算機編程並沒有什么不同,只不過基本的操作不同。
假設有一天,計算機已經相當發達,說明定我們會遇到,能走路,說話,會推理的計算機,但編程不會發生變化,同樣只是基本操作不同。
我們了解一下什么是計算機程序?
通常說計算機程序是一個可執行的文檔。可執行有兩方面的意思,一是計算機認識,二是有效的基本操作序列。C/C++程序一般是原生程序,原生是指它由CPU直接認識,它的有效基本操作序列是CPU的指令集。CPU執行程序的過程是:
讀入指令-->執行操作-->輸出-->再讀入...
我們編程完成的源代碼,還不是一個可執行的文檔,它要通過編譯,把文本變成CPU指令,再連接,連接C++的運行時環境對應的CPU的指令集。
源代碼---(編譯)-->目標文件---(連接C++運行時環境對應的CPU指集)-->可執行文檔。
程序的執行過程一般是:
讀入一些數據-->執行操作-->輸出--回到開始的狀態。
注意到這個過程和一個原生程序的執行過程非常類似,一個程序可以視為一個虛擬機,只是它的基本操作非常有限。圖形化程序的輸入可以認為是鼠標和鍵盤的操作。雖然,圖形化操作已經非常類似編程。由於這些操作系列不滿足有限,因此還不能說圖形化操作也是編程。
我們如何設計一個計算機程序?
這個問題等同於“如何用計算機解決一個具體問題?”。簡化的步驟如下,
-
首先,把問題已知和未知用計算機形式設計出來,也就是明確它的類型。
-
其次,分析最簡單的情況來,找到可能的解法。
-
然后,試圖在一般的情況解決它,這里可能需要補充一些中間類型的值。
-
接着,證明解法正確,且在有限的步驟內可以完成。
-
最后,用計算機編程語言來描述它。
例如,輸出以下由空格和星號組成的圖形,圖形的高度小於1000行。
*
***
*****
........
計算機的表示
這個問題的已知是一個高度n,小於1000,所以類型可以是int.
輸出是一個基本操作,打印一行文字,共有a個空格和b個星號,我們記作put-stars(a, b)
未知是n行輸出。
從簡單的情況來看:
n=1時,有1行輸出a=0, b=1;
n=2時,有2行輸出,第1行a=1,b=1, 第2行a=0,b=3
n=3時,有3行輸出,第1行a=2,b=1, 第2行a=1,b=3,第3行a=0,b=5
結合圖形的觀察,我們可以猜測,n行中每行的a和b都是一個固定序列的第i項。b比較容易看出來,它的通項是2i-1;a可能略難,不過從圖形上可以看出,它是n-i。
從一般情況下來看:
當我們知道第n-1行的a=1,b=2(n-1)-1時,不難知道下一行是a=0,b=2n-1。
正確性證明
從圖形上來看,每行空格比上一行少一個,星號多兩個,可知a序列滿足要求,b序列也滿足要求。同時,最多需要n次輸出操作就可以完成。
最后用計算機語言來實現它:
void draw(int n) { for (int i = 1; i <= n; ++i) { put-stars(n-i, 2*i-1); } }
以后,我們不討論具體如何分析和設計,只考慮在已知算法的情況下,如何來實現它。比如:C++的習慣上,計數項從0開始到n-1結束,我們可以重新寫通項為n - i - 1和2 * i + 1。代碼實現為:
1 void draw(int n) 2 { 3 for (int i = 0; i < n; ++i) { 4 put-stars(n-i-1, 2*i+1); 5 6 } 7 }
注1:我認為好的教材有幾種:有些經驗編程的同學可以用簡單明了的Essential C++;經驗較少的同學可以用事無巨細的C++ Primer;對於零基礎的初學者,按部就班用Programming:principles and practice design using C++更好。
注2:整數除法運算時,小數部分會直接丟棄,而不是通常的四舍五入。