轨道
/
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"

由于函数声明往往在定义之前就被读到,在那里设置默认实参是更好的做法。 如果一个形参有默认声明,那么它右侧的所有形参也都需要有默认声明。 有时,复杂的函数重载可以重构为数量更少、带默认实参的函数,以提高可维护性。

说明

在这个练习中,你将编写更多代码,用来准备和烹饪你最爱菜谱里那道绝妙千层面。

一共有 5 个任务。 第一个与烹饪本身有关,另外 4 个则围绕完美的准备工作展开。

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. 计算所需的 noodles 和 sauce 用量

除了预留时间,你还要确保 sauce 和 noodles 足够,好做出梦想中的千层面。 千层面中每有一层 noodles,就需要 50 克 noodles。 每有一层 sauce,就需要 0.2 升 sauce。

定义quantities函数,它的形参是一个层 vector,元素为string。 函数会算出做这顿饭所需的 noodles 和 sauce 用量。 结果应以amount struct 的形式返回,它已经为你定义好了。 amount有两个成员变量:一个是用int表示的noodles重量,另一个是用double表示的sauce体积。

struct amount { int noodles; double sauce; };

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

3. 加入秘密配料

不久前你去朋友家做客,在那里吃了千层面。 那味道太棒了,还带着某种特别的东西。 朋友把配料清单发给了你,并告诉你清单上的最后一项正是让这顿饭如此特别的“秘密配料”。 现在你也想把这味秘密配料加进自己的菜谱。

编写一个addSecretIngredient函数,它接受 2 个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. 换算菜谱用量

你菜谱上列出的用量只够做 2 份千层面。 下次你想给更多人做,所以需要算出不同份数对应的用量。

实现一个scaleRecipe函数,它接受 2 个形参。

  • 一个由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函数,它接受 2 个实参:一个代表你配料列表的 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++,全部免费。