这一章的标题是《计算》,想法是:计算是一个过程,是处理输入得到输出的过程。也有B站网友称之为 IPO 编程:Input, Process, Output. 其中 Process 相当于是广义上的「计算」。
计算过程的输入
如果认为程序是以计算为目的,那么就需要考虑:「计算」这一过程的输入和输出分别是什么?输入的来源有很多:键盘、鼠标、触屏、传感器、摄像头,etc
避免孤立的从输入得到输出
输出的途径有很多;但是最有意思的I/O是这两个:
- 从其他程序输入或输出
- 从同一个程序的其他部分输入或输出
在2000年前后,中小学课本用的是「信息技术」这个关键字来表示电脑课/微机课,所谓信息其实就是输入、输出的统称,甚至说更主要指代输入。
而高中、大学课本里,更多的使用「算法」这个关键字,它其实是一类程序的统称,把关注点从无限的数据,转移到了能用规则描述清楚的、有限文字或公式表述的过程上。
实现计算过程时的注意事项
注意优先级,先正确,再保持简单,最后是保持高效:
实现保持简单,目的是控制程序的复杂度; 单个函数超过1000行,和分解为10个100行的函数,显然后者更容易维护:
善于使用高质量的库,而不是完全从头写,既能保证质量,也能减少工作量:
良好的代码组织结构,是代码能够长期健康存活的关键;没有结构的代码犹如用泥砖盖房子,虽然能用但很凑合,无法盖起5层高楼:
编程时对编码结构和质量所付出的努力,可以大大简化最令人沮丧的编程工程:调试。原因:好的程序结构,一方面可以减少错误的发生,另一方面能缩短发现和改正错误的时间。
表达式:变量在等号左边表示对象,在等号右边表示变量的值:
使用常量,替代硬编码的 magic number,一方面增加了代码的可读性,另一方面增加了代码的可维护性
不要觉得「硬编码的魔数,含义很直白」,因为读代码的人可能从未接触过你的领域知识细节,比如 299792458,它缺乏语义信息,是「面向信息编程」,是混乱的;要改为 const int light_speed = 299792458
才有意义,才是「面向概念的编程」:
程序的正确性:需要检查输入的合法性,避免非法输入数据执行计算、得到错误结果,例如如下的 if/else,并没有判断 unit 不为 i 时,可能是c(合法),也可能是c和i之外的字母(非法):
改进方式是对输入做检查,只支持承诺的输入数据,对于不支持的则明确告知:
练习 - 货币转美元
v1
对于这个任务,我最初的代码是:
void convert_money_to_us_dollars()
{
float money;
std::string type;
// en: japan
// eu: euro
// uk: uk pound
std::cin >> money >> type;
float us_dollars = 0.f;
if (type == "en")
{
us_dollars = money * 0.0068f;
}
else if (type == "eu")
{
us_dollars = money * 1.1027f;
}
else if (type == "uk")
{
us_dollars = money * 1.3126f;
}
std::cout << money << " " << type << " == " << us_dollars << "\n";
}
存在的问题:
- 命名:money 需要改为 currency
- 命名:en 需要改为 jpy(Japan Yen),eu 需要改为 Eur, uk 需要改为 gbp(Great British Pound)
- 没处理非法输入的类型
- 硬编码了汇率转换数据
- 缺少输入提示
- 缺少单元测试代码
v2
void convert_currency_to_us_dollars()
{
float money;
std::string type;
std::cout << "Please input currency value and type(one of: jpy, eur, gbp)\n";
constexpr float usd_per_jpy = 0.0068f; // Japan Yen
constexpr float usd_per_eur = 1.1027f; // Europe
constexpr float usd_per_gbp = 1.3126f; // Great British Pound
std::cin >> money >> type;
float us_dollars = 0.f;
if (type == "jpy")
{
us_dollars = money * usd_per_jpy;
}
else if (type == "eu")
{
us_dollars = money * usd_per_eur;
}
else if (type == "uk")
{
us_dollars = money * usd_per_gbp;
}
else
{
std::cout << "error: invalid currency type. valid types: jpy, eur, gbp\n";
return;
}
std::cout << money << " " << type << " == " << us_dollars << " usd\n";
}
// Test function
void test_convert_currency_to_us_dollars()
{
std::string test_input = "100 jpy\n"; // Change this to test other inputs
std::istringstream iss(test_input);
// Redirect cin to use the stringstream input
auto cin_buff = std::cin.rdbuf(); // Save original buffer
std::cin.rdbuf(iss.rdbuf());
// Run the function to test
convert_currency_to_us_dollars();
// Restore original cin
std::cin.rdbuf(cin_buff);
}
这一版代码,整体有改进,但是仍存在问题:
- 输入:是通过 cin 给出的,需要改为函数参数; 提示信息应当作为应用程序(测试函数)的输入,而不是核心转换函数的输入
- 输出:是通过 cout 给出的, 需要改为返回值; 错误输入时的报错信息,可以让应用程序给出
v3
float convert_currency_to_us_dollars(float currency, const std::string& type)
{
constexpr float usd_per_jpy = 0.0068f; // Japan Yen
constexpr float usd_per_eur = 1.1027f; // Europe
constexpr float usd_per_gbp = 1.3126f; // Great British Pound
if (type == "jpy")
{
return currency * usd_per_jpy;
}
else if (type == "eu")
{
return currency * usd_per_eur;
}
else if (type == "uk")
{
return currency * usd_per_gbp;
}
else
{
return -1.0;
}
}
void convert_currency_to_us_dollars_app()
{
std::cout << "Please input currency value and type (one of: jpy, eur, gbp)\n";
float currency;
std::string type;
std::cin >> currency >> type;
float usd = convert_currency_to_us_dollars(currency, type);
if (usd == -1.0f)
{
std::cout << "error: invalid currency type: " << type << ". valid types: jpy, eur, gbp\n";
return;
}
std::cout << currency << " " << type << " == " << usd << " usd\n";
}
// Test function
void test_convert_currency_to_us_dollars()
{
std::string test_input = "100 jpy\n"; // Change this to test other inputs
std::istringstream iss(test_input);
// Redirect cin to use the stringstream input
auto cin_buff = std::cin.rdbuf(); // Save original buffer
std::cin.rdbuf(iss.rdbuf());
// Run the function to test
convert_currency_to_us_dollars_app();
// Restore original cin
std::cin.rdbuf(cin_buff);
}
这一版的代码,从原来的2级调用,改为3级。最内层的函数 convert_currency_to_us_dollars()
只负责转换,中间一层的函数 convert_currency_to_us_dollars_app()
则负责给出输入、输出的打印信息, 最外层的 test_convert_currency_to_us_dollars()
则负责测试。
对于C++11,基本上就这样了;如果是C++17,可以用 std::optional 作为返回值类型。。。
switch-case 语句, 可以一定程度上改进 if/else 语句,但是如果 case 中忘记写 break, 编译器没有报错,也没有警告:
不过, clang-tidy 可以会给出提示
4.5.1 使用函数
初学者,大概会写出如下代码:
void print_squares_v1(){
for(int i=0;i<100;i++)cout<<i<<'\t'<<i*i<<'\n';
}
大概写了一年,注意到空格缩紧格式,会改进为:
void print_squares_v2()
{
for (int i = 0; i < 100; i++)
cout << i << '\t' << i * i << '\n';
}
但是,把什么内容都放在单个函数里,就缺乏了抽象层级;大概从业3年~10年,会意识到,要使用函数则可以把独立的任务加以封装,调用这个函数的地方,整体代码的逻辑更加清晰:
void print_square(int v)
{
cout << v << '\t' << v * v << '\n';
}
void print_squares_v3()
{
for (int i = 0; i < 100; i++)
print_square(i);
}
4.6 vector
定义变量: vector<T>(n) v
指定了长度为n,类型为T。
.push_back()
增加元素.
.size()
获取 vector 的长度。
sort(v.begin(), v.end()
展示了 begin() 和 end() 成员函数用法。
总结
4.2 这一章节「目标和工具」,着重强调了代码结构和质量的重要性,值得仔细品味。
4.3.1 常量表达式举出了 pi 和 299792458 这两个例子,对代码可读性和可维护性的重要性做了展示。「面向概念编程」是我自创的词语,是和「面向信息编程」这种低效率、混乱代码相对立的。它也类似于「数学分析课本中注重概念,从而注重证明」,和「高等数学课本中注重计算」相对立;它也类似于人们说的「先理解再记忆」。
有了结构的代码,是有「二次生命」的代码;没有结构的代码,是「一次性的代码」,无法被拿去复用。对于具备规模的软件,它的代码应当是具有「二次生命」的代码。
4.4.1 通过展示 if/else 语句,表达了程序要检查输入合法性的思想,非法的输入会得到非法输出(GIGO, Garbage In, Garbage Out); 以及,程序只应当提供它承诺的功能,对于没承诺的功能,需要明确告知使用者「不支持」、「输入非法」。
通过编写 convert_currency_to_us_dollars()
函数,并多次重构,得到了不那么糟糕的实现。
4.4.1.3 给出了 switch/case 语句,强调了 case 缺少 break 时编译器没有错误和警告的问题,写的时候要注意。
4.5.1 通过 squares 的例子,给出了糟糕代码到好代码的改进。
4.6 是 vector 的一些操作。