Learning C++ 之1.10b 設計你的第一個程序

現在你已經學習了編程的一些基本理論,讓我們進一步看一下怎麼設計你的第一個程序。當你坐下來寫一段程序的時候,通常來說你是有一段問題需要解決。現在的程序員往往在將需求轉化爲程序的階段碰上困難。但是這個已經證明你在日常的生活中積累了很多解決問題的技巧。

最重要的事情就是在編程之前先設計你的程序。在很多方面上,編程就像是建築。在你還沒有設計的時候就直接蓋房子會出現什麼後果?結果是除非你非常天才,否則建好的房子會有一堆問題。牆不是直的,低級不牢固,等等。同樣的,沒有設計直接開始寫程序,最終的結果是程序會有一堆問題,你需要花費大量的時間去解決問題,而這些其實在設計階段稍微動一下腦子就可以解決。

提前的一些計劃將會節省你大量維護的時間和減少程序長期運行的挫折。

第一步:定義問題

第一個你需要指出的是你需要解決什麼問題。理想情況下你可以把這些需求寫成一個或者兩個句子。把這些作爲一個輸出表達也很有用,例如:

  • 我想要一個更好的方式去記錄朋友的聯繫方式。
  • 我想生成隨機的地下城,向天然洞穴一樣有趣。
  • 我想要推薦我想買的股票。
  • 我想要模擬一個球從一個塔上墜落的高度。

儘管這些需求非常明顯,但是非常有用。最糟糕的就是你設計的程序和你的需求完全不一致。

第二部:收集需求

當定義了你想要輸出的結果之後,仍然是模糊的,這個時候,你應該手機需求信息。

需求對於你的解決方案所受到的約束(預算,時間,空間,內存),以及程序必須滿足的需求來說是一個非常花哨的詞。同樣的你的需求應該關注what,而不是how。比如:

  • 電話號碼應該被保存,所以之後方便再次使用。
  • 隨機的洞穴必須包含一條進的路和一條出的路。
  • 股票的推薦應該遵循歷史數據。
  • 使用者應該可以登上塔的高度
  • 我們7天內需要一個可測的版本

一個單一的問題可能有很多需求,解決方案並沒有完成,知道滿足了所有的需求之後。

一些列給到工程師的定義好的需求,應該可以讓程序員爲你在需求的基礎上創建程序,但是這個過程是非常枯燥的。

第三:定義你的工具,目標和備選方案

當你是一個有經驗的程序員的時候,這一步還有許多其他的事情要做,比如:

  • 定義你的程序運行的目標和操作系統
  • 定義你所需要的開發工具
  • 決定你是單獨編寫程序還是作爲團隊的一部分
  • 定義你的測試,反饋和發佈策略
  • 定義怎麼備份代碼

然而作爲一個新的程序員,這些問題都比較簡單。你在爲你自己寫程序,操作系統就是你自己的電腦,開發工具就是你手頭有的IDE,這個程序除了你之外,別人應該不會使用,所以這個就變得很簡單了。

也就說如果你正在做的工作不是很複雜,你應該有一個計劃去備份你的代碼。單純的zip或者copy代碼到另一個目錄是不夠的。如果你的電腦崩潰了,你照樣會丟失代碼。比較好的方式是從你的系統中獲取一份副本,有很多簡單的方式來實現,比如email給你自己,或者ftp到另一個服務器,或者放到雲上比如github等。版本控制系統有一個非常大的優勢,不僅可以保存你的代碼,而且還可以輕易地回退到之前地版本。

第四部:將困難的問題分解成爲簡單地問題

在現實生活中,我們經常需要處理非常複雜的問題。盡力指出需要怎麼解決這些問題是非常有挑戰性的。在這種情況下,我們經常會使用自上而下的方式去解決問題。也就是爲了解決這個複雜的問題,我們把它拆解成一些簡單的小問題,各個擊破。如果這些子問題仍然比較難,我們繼續對他進行拆解。通過持續的拆解問題,你會最終可以有效的解決每一個拆解的小問題。

讓我們舉一個例子,比如我們需要報道一篇胡蘿蔔。我們的任務如下:

寫一份關於胡蘿蔔的報道

這個是非常大的一個任務,讓我們做一下分解:

1.寫一份關於胡蘿蔔的報道

  • 對胡蘿蔔做調查
  • 寫一份大綱
  • 對大綱進行細節填充
  • 添加目錄

這樣就非常易於管理了,因爲我們分好了子任務。然而,在這個子任務中對胡蘿蔔做調查仍然比較模糊,所以繼續分解。

1.寫一份關於胡蘿蔔的報道

    對胡蘿蔔做調查

  •         去圖書館看關於胡蘿蔔的書籍
  •         在網上查找胡蘿蔔的資料
  •         從參考資料中記錄相關章節

   寫一份大綱

  •         生長的信息
  •         加工的信息
  •         營養的信息

  對大綱進行填充

  添加目錄

現在我們有了一個層級管理,每一個層級的任務都不是很難。完成每一個相關的子任務,我們就會完成這個複雜的任務:寫一篇胡蘿蔔的報道

另一種創建層級的方式是從底向上的方法,就是我們從一些簡單的細節工作開始,並通過組織他們進行分層。

舉個例子,一些人在工作日會去工作。所以,我們提出一個解決起牀去工作的方案。如果我們問你怎麼起牀,你會給出下面的步驟:

  • 拿出衣服
  • 穿好衣服
  • 吃早飯
  • 開車去公司
  • 刷牙
  • 起牀
  • 準備早餐‘
  • 取車
  • 洗個澡

通過自下而上的方法,我們可以把相似的步驟組織成一個大的組織,最終分成一個層次結構,如下:

起牀去工作

    臥室的事情

        起牀

        拿衣服

        穿衣服

    洗浴室的事情

        洗個澡

        刷個牙

    早餐事情

        準備早餐

        吃早餐

    運輸的事情

        取車

        開車去公司

事實證明,這種層級結構在編程的過程中是非常重要的。一旦你有了一個分層的任務,你將會比較容易定義程序中的結構體。最上層的東西就是main函數,其他層次的就是一般的函數。

一旦函數中有一個非常難實現的點,你可以再詳細拆分。最終你可以簡單的實現每一個你定義的子任務。

第五步,指出事件的序列

現在你的程序已經有一個結構體了,現在需要確認怎麼把這些結構給鏈接起來。第一步是確定要執行的事件序列,例如當你起牀後,你該如果按照步驟來執行每一步呢?

  • 起牀
  • 拿出衣服
  • 洗個澡
  • 穿衣服
  • 準備早餐
  • 吃早餐
  • 刷牙
  • 取車
  • 開車去公司

如果我們寫一個計算器,那麼步驟如下:

  • 從用戶那裏獲取第一個數字
  • 獲取數字上的操作符
  • 從用戶那邊獲取第二個數字
  • 祭祀u按結果
  • 打印輸出

下面的函數定義了你的main()函數裏面的順序:

int main()
{
    getOutOfBed();
    pickOutClothes();
    takeAShower();
    getDressed();
    prepareBreakfast();
    eatBreakfast();
    brushTeeth();
    getInCar();
    driveToWork();
}

計算器:

int main()
{
    // Get first number from user
    getUserInput();
 
    // Get mathematical operation from user
    getMathematicalOperation();
 
    // Get second number from user
    getUserInput();
 
    // Calculate result
    calculateResult();
 
    // Print result
    printResult();
}

如果你用這種方式創建你的程序,在你真正寫之前對每一個函數進行註釋是一個好習慣,然後挨個調試,測試。這樣編譯器就不會報錯了。

第六步:指出你需要輸入和希望輸出的結果

一旦你有了一個層次和事件序列,接下來的事情就是確認每一層次需要的輸入數據和輸出結果。如果你已經有了之前步驟的輸入數據,那麼輸入數據就可以作爲一個參數。如果你正在使用其他函數的輸出作爲參數,那麼一般就把輸出設置成爲一個返回值。

當我們完成時,每個函數都有一個函數原型。如果你忘了,函數原型就是函數名,返回類型,參數,但是不包含具體的函數實現。

讓我們舉幾個例子,getUserInput()是一個非常簡單的函數。我們從用戶那裏得到一個數字並且返回給使用者,因此函數類型如下:

int getUserInput();

在計算器的例子裏,calculateResult()函數需要3個參數,兩個操作數,以及一個操作符。我們需要準備好這三個參數,當我們調用這個函數的時候。所以這三個數值可以作爲函數的參數輸入。這個函數值只是作爲計算結果用,但是並不作爲輸出使用。因此我們需要設置一個返回值,其他的函數可以使用。

所以最終的函數原型如下:

int calculateResult(int input1, int op, int input2);

第七步,寫任務細節

在這一步中,爲了每一個子任務你得寫詳細的任務實現。如果你已經把任務分解成爲了足夠小的部分,每一個部分需要非常簡單直接。如果一個給定的任務過於複雜,或許你該考慮繼續分解。

比如:

int getMathematicalOperation()
{
    std::cout << "Please enter which operator you want (1 = +, 2 = -, 3 = *, 4 = /): ";
 
    int op;
    std::cin >> op;
 
    // What if the user enters an invalid character?
    // We'll ignore this possibility for now
 
    return op;
}

第八步:連接數據的輸入和輸出

最後一步使用合適的方式將輸入和輸出連接起來。比如,你可能把calculateResult()函數的輸出作爲printResult()的輸入,所以最終的結果會打印出來。這個通常需要臨時變量來存儲結果,以便在函數間傳輸。比如:

// result is a temporary value used to transfer the output of calculateResult()
// into an input of printResult()
int result = calculateResult(input1, op, input2); // temporarily store the calculated result in result
printResult(result);

這種方式比起函數直接嵌套調用要清晰的多。

printResult( calculateResult(input1, op, input2) );

這部分對於一個新手程序員來說往往是最難的部分。

下面是一個完整的計算器版本,裏面可能包含了一些我們目前還沒有學習的概念,可以參考一下:

  • if語句是條件語句,當條件爲真時執行
  • ==操作符比較兩邊的數值是否完全相等

現在你沒有必要完全搞懂每一步的意思,只是從整體上看一下整體結構,以及明白數據是怎麼傳遞的。

// #include "stdafx.h" // uncomment if using visual studio
#include <iostream>
 
int getUserInput()
{
    std::cout << "Please enter an integer: ";
    int value;
    std::cin >> value;
    return value;
}
 
int getMathematicalOperation()
{
    std::cout << "Please enter which operator you want (1 = +, 2 = -, 3 = *, 4 = /): ";
 
    int op;
    std::cin >> op;
 
    // What if the user enters an invalid character?
    // We'll ignore this possibility for now
 
    return op;
}
 
int calculateResult(int x, int op, int y)
{
    // note: we use the == operator to compare two values to see if they are equal
    // we need to use if statements here because there's no direct way to convert op into the appropriate operator
 
    if (op == 1) // if user chose addition (#1)
        return x + y; // execute this line
    if (op == 2) // if user chose subtraction (#2)
        return x - y; // execute this line
    if (op == 3) // if user chose multiplication (#3)
        return x * y; // execute this line
    if (op == 4) // if user chose division (#4)
        return x / y; // execute this line
	
    return x + y; // if the user passed in an invalid op, we'll do addition.
 
    // we discuss better error handling in future chapters
}
 
void printResult(int result)
{
    std::cout << "Your result is: " << result << std::endl;
}
 
int main()
{
    // Get first number from user
    int input1 = getUserInput();
 
    // Get mathematical operation from user
    int op = getMathematicalOperation();
 
    // Get second number from user
    int input2 = getUserInput();
 
    // Calculate result and store in temporary variable (for readability/debug-ability)
    int result = calculateResult(input1, op, input2);
 
    // Print result
    printResult(result);
 
    return 0;
}

寫程序的幾點建議:

讓你的程序有個簡單的開始:

通常來說一個程序新手往往對他們的程序有一個比較大的願景。“我想寫一個角色扮演的遊戲,有圖像,聲音,以及商店,地牢,你可以在商城裏買賣熱議你想要的東西”。如果你開始就寫這麼複雜的程序,你非常容易受打擊。相反的,開始的目標確立的簡單一點,在你的能力範圍之內。比如“我想要畫一個2d的圖像在電腦上”。

隨着時間的推移增加功能:

一旦你的簡單的程序運行成功了,你就可以增加功能了。比如,你可以將2d圖像顯示在屏幕上了,那麼增加一個角色可以走來走去。一旦這個角色創建成功了,增加牆來阻擋角色的行動。有牆創建成功了,可以慢慢增加一個城鎮了。有城鎮了之後,再去增加商人。通過這樣一步步地增加功能,最終你的程序會變得非常複雜,來實現你所想地所有功能。

一次集中在一點上:

不需要想着一次同時編寫所有地代碼部分,不要把你的精力分散在多個任務中。一次集中搞定一個任務,儘可能地完成它。一次完成一個任務,比同時做5個沒有完成地任務要好得多。如果你分神了,你很有可能會犯錯誤,並且注意不到細節。

寫程序的過程中,自測你自己地每一部分代碼:

新的程序員經常一次寫一段完整段代碼。然後他們編譯地時候會上報一堆錯誤。這個並不嚇人,如果你的程序不能執行很難去具體定位哪裏出問題了。因此我們建議一次寫一小段代碼,然後立即編譯,檢查有沒有錯誤。如果有問題,你能夠很快的定位問題。一旦確認程序沒有問題,接着寫下一部分,這種不斷重複。雖然這可能花費更長的時間,但是一旦寫完,就可以立即執行,沒有錯誤。否則你將會花費大量的時間在debugging上。

打多數新的程序員都會抄捷徑,不按照上面的建議來。然而長遠來看,按照上面的建議來會節省你大量的時間。少的計劃將會導致大量的後續debug。

好消息是一旦你適應了上面的建議,你就自然而然地按照上面地建議來做了。最後,你就可以直接寫一段完整的代碼,而不用任何的提前計劃了。當然這個需要一個比較長的熟練的過程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章