千层面配方进阶

概念学习C++17

简介

术语

前面已经使用并编写了一些 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

进阶:声明与定义 声明相当于通知编译器:存在一个具有指定名称、返回类型和参数列表的函数。只有声明而没有定义,程序仍无法正常链接运行。若函数定义已出现在使用之前,不必再另写声明;若先使用后定义,则需要提前声明。声明也可处理相互调用,并将接口与实现分离。

const 限定符

有时需要保证某些值在初始化后不再改变。C++ 使用 const 关键字限定这样的常量。

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

注意 你可能经常看到使用全大写加下划线命名的常量。若项目没有其他约定,建议将这种命名风格保留给宏。

尝试修改已经初始化的常量会产生编译错误。这既能避免意外修改,也可能为编译器提供优化机会。知道代码中的某些值不会改变,也有助于理解程序。

函数参数也可以使用 const。

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

将对象以 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);

进阶:函数签名 函数签名涉及名称、参数数量、参数类型及相关限定,不包含参数名。返回类型不能单独区分重载;若两个函数仅返回类型不同,会产生编译错误,因为调用时无法据此决定应使用哪一个。

默认参数

有些函数的参数列表很长,而许多调用对大多数参数都使用相同值。默认参数可以避免反复填写这些值。

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 "Alruccaba", sire "Poseidon"
record_new_horse_birth("Highclere", 175, "off-white", "Fall Aspen");   // sire will be "Poseidon"

读者通常先看到声明,因此默认值最好写在声明处。如果某个参数提供默认值,其右侧参数也都必须提供默认值。有时可以用带默认参数的少量函数替代复杂的多组重载,改善可维护性。

任务说明

继续编写与千层面准备和烹饪有关的代码,按照最喜欢的食谱制作美味佳肴。

你需要完成五个任务,涉及制作时间及食材准备。

1. 估算准备时间

下一次制作千层面时,你希望预留足够时间,从容享受烹饪。你已规划好各层食材,现在需要据此估算准备时长。

实现 preparationTime,接收表示各层的字符串动态数组,以及 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,接收表示各层的字符串动态数组,计算所需食材。返回已经为你定义好的 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,接收两个字符串动态数组。第一个参数是自己食材清单的引用,第二个是朋友的清单。自己清单的最后一项始终为 "?",把它替换成朋友清单的最后一项。注意:函数不返回任何值,应直接修改自己的清单。朋友的清单可用引用传递以避免复制,但不能修改,应使用 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 动态数组,表示两份食物所需的各项用量。
  • 希望制作的份数。

返回一个新的动态数组,包含目标份数所需的用量。你仍要保留原食谱,因此函数不能修改传入的 quantities。

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

5. 揭开家族秘方

你想起童年听过的故事:家族中的一位长辈保存着最深的烹饪秘密。据说她掌握非凡风味的秘诀,但方式独特,从不给整张清单或用量,只会一次透露一种食材。

你怀着期待又忐忑的心情联系她。果然,她用神秘的方式给出了一种秘密食材。

你希望把这份特别的馈赠加入食谱。为 addSecretIngredient 实现另一种重载,接收自己的食材动态数组引用,以及一个表示长辈所给食材的字符串。

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

祝你好运,勇敢的厨师!

需要一点帮助?查看分步提示

提示

1. 估算准备时间

  • 使用动态数组的 size() 获取层数。

2. 计算面片与酱汁用量

  • 先创建 amount 对象,记录面片和酱汁用量。
  • 用 for 循环遍历各层。
  • 遇到 "noodles" 或 "sauce" 时,增加对应食材用量。

3. 添加秘密食材

  • 回顾动态数组,了解如何读取与替换元素。
  • 数组 a 的最后一个元素下标是 a.size() - 1,也可以直接使用 a.back()。

4. 按份数调整配方

  • 先创建与输入长度相同的新动态数组。
  • 使用 for 循环遍历输入,生成输出。

5. 揭开家族秘方

  • 可以复用任务 3 的代码。
  • 本任务需要使用函数重载。