學習軌道
/
C++
C++
/
練習
/
千層麵大師
千層麵大師

千層麵大師

學習練習

簡介

術語

你已經在幾個概念裡用過也寫過 C++ 函式了,現在該來認真談點技術細節啦。下面的程式碼片段列出了最常見的術語,方便你隨時查閱。由於 C++ 會忽略空白,這裡調整了排版,把每個元素各自放在一行。

// Function declaration:
bool                                              // Return type
admin_detected(string user, string password)      // Type signature
;                                                 // Don't forget the ';' for the declaration

// Function definition:
bool                                              // Return type
admin_detected                                    // Function name
(string user, string password)                    // Parameter list
{ return user == "admin" && password == "1234"; } // Function body
Advanced

宣告就像是寫給編譯器的一張便條,告訴它有個函式叫這個名字,有著這樣的回傳型別和參數列表。 如果少了定義,程式就無法運作。 宣告是選用的,但當你在定義之前就用到這個函式時,就需要它。 宣告可以解決循環參照之類的問題,也能用來把介面和實作分開。

const 限定詞

有時你會想確保某些值在初始化之後就不能再被改變,C++ 使用const關鍵字作為常數的限定詞。

const int number_of_dragon_balls{7};
number_of_dragon_balls--; // compilation error
Note

你經常會看到常數以 UPPER_SNAKE_CASE 來命名。如果沒有其他慣例,建議把這種大小寫形式保留給巨集使用。

如果你在設定之後試著修改常數變數,程式就無法編譯。這有助於避免不小心改動,也為編譯器開啟了最佳化的可能性。對人來說也一樣,如果你知道某些部分不會受到影響,就更容易推敲整段程式碼。

你也可以把const當作函式參數的限定詞。

string guess_number(const int& secret, const int& guess) {
    if (secret < guess) return "lower.";
    if (secret > guess) return "higher.";
    return "exact!";
}

當你傳入const參照給函式時,就能確保它不會被改動。對於複製起來可能很耗資源的物件,例如較長的字串,你經常會看到使用const參照。const限定詞的第三種用途,是不會改變類別執行個體的成員函式。

class Stubborn {
    public:
    Stubborn(string reply) {
        response = reply;
    }
    string answer(const string& question) const {
        if (question.length() == 0) { return ""; }
        return response;
    }
    private:
    string response{};
};

Stubborn的成員函式answer使用const string&參照作為參數,這樣就避免了對傳入函式的原始物件進行複製。

函式多載

只要參數列表不同,多個函式就可以共用同一個名稱。這叫做函式多載,通常用於這些函式要執行非常相似的工作時。

去掉回傳型別的函式標頭,就是這個函式的__型別簽章__。型別簽章只要有變,就是一個新的函式。

play_sound這個例子有六種不同的多載,來應付不同的情境:

// different argument types:
void play_sound(char note);         // C, D, E, ..., B
void play_sound(string solfege);    // do, re, mi, ..., ti
void play_sound(int jianpu);        // 1, 2, 3, ..., 7

// different number of arguments:
void play_sound(string solfege, double duration);

// different qualifiers:
void play_sound(vector<string>& solfege);
void play_sound(const vector<string>& solfege);
Advanced

型別簽章由函式的名稱、參數的數量、參數的型別,以及參數的限定詞所定義(但不包含參數的名稱)。 回傳型別明確地不屬於型別簽章的一部分,如果你有兩個只有回傳型別不同的函式,就會得到編譯錯誤。 編譯器會抱怨,因為它不清楚該用這兩個之中的哪一個。

預設引數

有些函式會變得非常冗長,而且許多呼叫可能對大部分參數都使用相同的值。這些呼叫中的重複,可以用預設引數來避免。

void record_new_horse_birth(string name, int weight, string color="brown-ish", string dam="Alruccaba", string sire="Poseidon");

record_new_horse_birth("Urban Sea", 130); // color will be brown, dam "Alruccabam", sire "Poseidon"
record_new_horse_birth("Highclere", 175, "off-white", "Fall Aspen");   // sire will be "Poseidon"

由於函式宣告通常比定義更早被讀到,所以在宣告裡設定預設引數是比較好的做法。如果某個參數有預設值,它右邊的所有參數也都必須有預設值。有時候可以把複雜的函式多載重構成較少的函式並搭配預設引數,來提升可維護性。

說明

在這個練習中,你會再寫一些程式碼,內容與準備和烹調你最愛食譜中那道絕妙的千層麵有關。

這個練習有五個任務。 第一個任務與烹調本身有關,其他四個則是關於完美的準備工作。

1. 估算準備時間

為了接下來要做的千層麵,你想確保自己預留了足夠的時間,好享受烹調的過程。 你已經規劃好千層麵要有哪些層了。 現在你想根據這些規劃,估算準備工作需要多久。

實作一個函式 preparationTime,它接受一個存放各層的 vector(型別為string),以及每層的平均準備時間(單位為分鐘,型別為int)。 這個函式應根據層數,以int回傳總準備時間的估算值。 如果沒有傳入平均準備時間,就使用預設值2。

std::vector<std::string> layers{"sauce", "noodles", "sauce", "meat", "mozzarella", "noodles"};
preparationTime(layers, 3)
// => 18
preparationTime(layers)
// => 12

2. 計算所需的麵條與醬汁份量

除了預留時間,你也想確保有足夠的醬汁和麵條,能煮出你夢想中的千層麵。 千層麵中每有一層麵條,就需要 50 公克的麵條。 每有一層醬汁,就需要 0.2 公升的醬汁。

定義函式 quantities,它接受一個存放各層的 vector 作為參數,型別為string。 這個函式會算出做這道料理所需的麵條與醬汁份量。 結果應以amount _struct_的形式回傳,這個結構已經替你定義好了。 amount有兩個成員變數,一個是noodles的重量,型別為int;另一個是sauce的體積,型別為double。

struct amount { int noodles; double sauce; };

amount needed{quantities({"sauce", "noodles", "sauce", "meat", "mozzarella", "noodles"})};
// needed.noodles => 100
// needed.sauce => 0.4

3. 加入祕密食材

前陣子你去拜訪一位朋友,在他家吃了千層麵。 那千層麵非常美味,而且有某種特別之處。 朋友把食材清單寄給你,並告訴你清單上的最後一項,就是讓那頓飯如此特別的「祕密食材」。 現在你也想把那個祕密食材加進自己的食譜裡。

寫一個函式 addSecretIngredient,它接受兩個vector of strings作為參數。 第一個參數是你自己食譜裡食材的_參考_,第二個則是朋友寄給你的食材_vector_。 你的食材清單中最後一個元素一律是"?"。 這個函式應把它替換成朋友清單中的最後一項。 注意:addSecretIngredient不會回傳任何東西,你應該直接修改自己的食材清單。 朋友食材的清單也可以使用參考(為了提升效能),但不應該被修改。 請用const參考確保清單不會被修改。

std::vector<std::string> friendsList{"noodles", "sauce", "mozzarella", "kampot pepper"};
std::vector<std::string> myList{"noodles", "meat", "sauce", "mozzarella", "?"};

addSecretIngredient(myList, friendsList);
// myList => {"noodles", "meat", "sauce", "mozzarella", "kampot pepper"}

4. 依比例調整食譜

你的食譜書上列出的份量,只夠做出兩人份的千層麵。 因為下次你想煮給更多人吃,所以想計算不同份數所需的份量。

實作一個函式 scaleRecipe,它接受兩個參數。

  • 一個由double組成的 vector,代表 2 人份所需的份量。
  • 你想烹調的份數。

這個函式應回傳一個新的 vector,內含所需份數對應的份量。 不過,你希望保留原本的食譜。 這代表在這個函式中,quantities引數不應該被修改。

std::vector<double> quantities{1.2, 3.6, 10.5};
scaleRecipe(quantities, 4);
// => { 2.4, 7.2, 21 }

5. 解開家族的祕密

你還記得小時候聽過的故事,那些關於你曾姨婆的傳說,她是家族最深邃烹飪祕密的守護者。 傳說她握有解開超乎想像滋味的鑰匙,但她的方法很不尋常。 她不用清單,也不講份量;她輕聲細語,一次只透露一種食材。

帶著既興奮又忐忑的心情,你決定聯絡曾姨婆。 一如預期,她用神祕難解的方式回應,只給出一種祕密食材,裹著層層謎團。

曾姨婆的貢獻增添了一層複雜度,而你想把它用在食譜裡。 請實作另一個版本的addSecretIngredient函式,它接受兩個引數:一個代表你食材清單的 vector 參考,以及一個代表曾姨婆神祕獻禮的字串。

std::vector<std::string> myList{"noodles", "meat", "sauce", "mozzarella", "?"};
std::string secretIngredient{"spice melange"};
addSecretIngredient(myList, secretIngredient);
// myList => {"noodles", "meat", "sauce", "mozzarella", "spice melange"}

祝你好運,勇敢的廚師!

透過 GitHub 編輯 連結會在新視窗或分頁中開啟
C++ Exercism

準備好開始 千層麵大師 了嗎?

註冊 Exercism,透過 19 個概念100 個練習 和真人引導來學習並精通 C++,全部免費。