策略、次序和测试—《狂人C》习题解答16(第三章习题6)
题目:
6.某人有12品脱的啤酒一瓶,另有一个8品脱和5品脱的容器。按照如下步骤操作
(0)把12品脱酒瓶内的酒倒入8品脱的容器,倒满为止;
(1)把8品脱的容器内的倒入5品脱的容器,倒满为止;
(2)把5品脱容器内酒的倒入12品脱酒瓶,倒完为止;
(3)把8品脱酒瓶内的酒倒入5品脱的容器,倒完为止;
(4)把12品脱的容器内的倒入8品脱的容器,倒满为止;
(5)把8品脱容器内酒的倒入5品脱酒瓶,倒满为止;
(6)把5品脱容器内酒的倒入12品脱酒瓶,倒完为止;
编程模拟这个过程,输出最后各容器内剩多少酒。
这个问题不难,但对初学者来说能做对并不很容易。如何能正确、高效、有条理地完成这个题目的代码,是本文要讨论的主题。
1.解题策略
做任何事都存在策略,写代码也是如此。
好的策略能使代码写得优雅从容事半功倍,差的策略让人手忙脚乱、事倍功半甚至无功而返。
经验表明,拿到题目不假思索就猝然开写,最后代码的质量通常都不高明,是为无谋。相反,在写代码之前深思熟虑,谋后而动是优质代码的必要前提。
本题目步骤虽多,但无外乎“倒满”与“倒完”,正确地模拟出这两个步骤,是保证程序正确性的前提。
把大的问题拆解为若干小的问题,再逐个对小问题各个击破,此为代码编写之道。
所以优秀代码的功夫恰恰是在代码之外。在编写之前对问题的思考与分解,貌似无功,其实比代码本身更重要。
2.善积小胜
把问题分解为小问题后,面临的首要问题就是把解决小问题的代码写好。
很多人轻视这一点,最后的代码即使能勉强使用,也不过是在千疮百孔上面打了无数补丁,这样的代码远远算不上优秀的代码。这种短视的工作方法忽视了一个最基本的常识,你不可能用一堆质量低劣的零件组装成一辆质量超群的汽车。(据业内人士讲,国产汽车就是这样生产的)
所以,要想写出优秀的代码,就必须懂得“勿以善小而不为,勿以恶小而为之”这个道理,并努力加以践行。
首先写从A容器倒入B容器并倒满的代码。
3.学会抽象
程序员必须学会抽象:抽象地提出问题,并解决抽象地解决问题。缺乏这种能力只会对着12、8、5这样的具体数值写代码则永远写不出优秀的代码。
与解决问题相比,正确地提出问题更为重要。在实际编写程序的过程中,程序员需要不断地自己向自己提出问题,然后加以解决。如果不能正确地提出问题,也就不可能正确地解决问题。
从A容器倒入B容器并倒满问题:
若,A容器的容积为V_A升,最初有啤酒C_A升;B容器的容积为V_B升,最初有啤酒C_B升。且V_A、C_A、V_B、C_B皆为整数,并满足C_A+C_B ≥ V_B 。把A容器内的啤酒倒入B容器直至倒满为止,问之后两容器内的啤酒各有多少。
4.次序安排
解决抽象的问题必然需要抽象地解决问题,然而计算机只能针对具体的数值进行计算。所以下面的代码假设了一些具体的数值并通过符号常量把这些数据引入程序使问题具体化。这些符号常量带来的好处之一就是更方便组织测试。我们很难对代码的正确性心中有底,然而没有经过测试的代码让人心中更没底。
下面代码解决的具体问题是:
若,A容器的容积为12升,最初有啤酒12升;B容器的容积为8升,最初有啤酒0升。把A容器内的啤酒倒入B容器直至倒满为止,问之后两容器内的啤酒各有多少。
/*
若,A容器的容积为V_A升,最初有啤酒C_A升;
B容器的容积为V_B升,最初有啤酒C_B升。
且V_A、C_A、V_B、C_B皆为整数,并满足C_A+C_B≥V_B。
把A容器内的啤酒倒入B容器直至倒满为止,问之后两容器内的啤酒各有多少。
*/
#include <stdio.h>
#include <stdlib.h>
#define V_A 12U
#define V_B 8U
//初始值
#define C_A 12U
#define C_B 0U
int main( void )
{
unsigned beer_A = C_A ,
beer_B = C_B ;
//把A容器内的酒倒入B容器,倒满为止
beer_B = V_B ; //B容器被倒满 //注:这段代码是错误的
beer_A -= ( V_B - beer_B ) ; //A容器减去被倒出的
printf("%u,%u\n", beer_A , beer_B );
system("PAUSE");
return 0;
}
测试结果并不理想,这段程序的运行后的输出为:
12,8
这是初学者常见的一个错误:忽视次序。实际上代码所表达对计算机要求的不仅仅是运算,还有运算的次序,如果运算没有遵循正确的次序,程序是不可能正确的。
上述代码的错误在于先进行了“beer_B = V_B”这个赋值运算,这样beer_B中原来存储的值就由于存储了新的值而“消失”了,此后再计算“( V_B - beer_B )”就不再是B容器中倒入了或A容器倒出多少啤酒。如果要正确地模拟这个过程应该先进行第二个赋值运算。在编程时应该注意,变量在不同的时刻存储的值的意义不同,初学者往往在变量的值改变之后还把变量的值误以为是先前的值的意义。
改正之后的代码为
/* 若,A容器的容积为V_A升,最初有啤酒C_A升; B容器的容积为V_B升,最初有啤酒C_B升。 且V_A、C_A、V_B、C_B皆为整数,并满足C_A+C_B≥V_B。 把A容器内的啤酒倒入B容器直至倒满为止,问之后两容器内的啤酒各有多少。 */ #include <stdio.h> #include <stdlib.h> #define V_A 12U #define V_B 8U //初始值 #define C_A 12U #define C_B 0U int main( void ) { unsigned beer_A = C_A , beer_B = C_B ; //把A容器内的酒倒入B容器,倒满为止 beer_A -= ( V_B - beer_B ) ; //A容器减去被倒出的 beer_B = V_B ; //B容器被倒满 printf("%u,%u\n", beer_A , beer_B ); system("PAUSE"); return 0; }
改正后程序的运行结果为:
4,8
这个结果与预期相符。再以另外几组数据测试,测试的一般原则应为两个容器中啤酒的各种边界值和中间值(12,11和8,2,0)的各种合理组合:
#define C_A 12U
#define C_B 2U
#define C_A 12U
#define C_B 8U
#define C_A 11U
#define C_B 0U
#define C_A 11U
#define C_B 2U
#define C_A 11U
#define C_B 8U
测试结果都正确,这些测试可以增加编程者对代码的自信。
从这里可以看到,符号常量的一个巨大的好处是在测试时不用修改代码部分,而对代码的任何修改都可能引入新的错误。
5.依样画瓢
很容易在前面“倒满”代码的基础上给出“倒完”的代码并组织测试。
/* 若,A容器的容积为V_A升,最初有啤酒C_A升; B容器的容积为V_B升,最初有啤酒C_B升。 且V_A、C_A、V_B、C_B皆为整数,并满足C_A+C_B≤V_B。 把A容器内的啤酒倒入B容器直至倒 完 为止,问之后两容器内的啤酒各有多少。 */ #include <stdio.h> #include <stdlib.h> #define V_A 8U #define V_B 12U //初始值 #define C_A 8U #define C_B 0U //#define C_A 8U //用于测试 //#define C_B 2U //#define C_A 6U //#define C_B 0U //#define C_A 6U //#define C_B 2U //#define C_A 0U //#define C_B 12U //#define C_A 0U //#define C_B 8U //#define C_A 0U //#define C_B 0U int main( void ) { unsigned beer_A = C_A , beer_B = C_B ; //把A容器内的酒倒入B容器,倒完为止 beer_B += beer_A ; //B容器加上A容器的 beer_A = 0 ; //A容器被倒空 printf("%u,%u\n", beer_A , beer_B ); system("PAUSE"); return 0; }
6.完成程序
完成了前面的准备,就可以简单轻松地解决题目提出的问题了。
首先给出代码的框架:
#include <stdio.h> #include <stdlib.h> //容积 #define V_12 12U #define V_8 8U #define V_5 5U //初始值 #define C_12 12U #define C_8 0U #define C_5 0U int main( void ) { unsigned beer_12 = C_12 , beer_8 = C_8 , beer_5 = C_5 ; //三个容器最初所盛啤酒 //把12品脱酒瓶内的酒倒入8品脱的容器,倒满为止; //把8品脱的容器内的倒入5品脱的容器,倒满为止; //把5品脱容器内酒的倒入12品脱酒瓶,倒完为止; //把8品脱酒瓶内的酒倒入5品脱的容器,倒完为止; //把12品脱的容器内的倒入8品脱的容器,倒满为止; //把8品脱容器内酒的倒入5品脱酒瓶,倒满为止; //把5品脱容器内酒的倒入12品脱酒瓶,倒完为止; printf("%u,%u,%u\n", beer_12 , beer_8 , beer_5 ); system("PAUSE"); return 0; }
由于“倒满”与“倒完”的代码已经完成:
beer_A -= ( V_B - beer_B ) ; //A容器减去被倒出的
beer_B = V_B ; //B容器被倒满
和
beer_B += beer_A ; //B容器加上A容器的
beer_A = 0 ; //A容器被倒空
所以只要将这两段代码分别复制粘贴到代码中需要的地方再稍加编辑即可。最后的代码为:
#include <stdio.h> #include <stdlib.h> //容积 #define V_12 12U #define V_8 8U #define V_5 5U //初始值 #define C_12 12U #define C_8 0U #define C_5 0U int main( void ) { unsigned beer_12 = C_12 , beer_8 = C_8 , beer_5 = C_5 ; //三个容器最初所盛啤酒 //把12品脱酒瓶内的酒倒入8品脱的容器,倒满为止; beer_12 -= ( V_8 - beer_8 ) ; beer_8 = V_8 ; //把8品脱的容器内的倒入5品脱的容器,倒满为止; beer_8 -= ( V_5 - beer_5 ) ; beer_5 = V_5 ; //把5品脱容器内酒的倒入12品脱酒瓶,倒完为止; beer_12 += beer_5 ; beer_5 = 0 ; //把8品脱酒瓶内的酒倒入5品脱的容器,倒完为止; beer_5 += beer_8 ; beer_8 = 0 ; //把12品脱的容器内的倒入8品脱的容器,倒满为止; beer_12 -= ( V_8 - beer_8 ) ; beer_8 = V_8 ; //把8品脱容器内酒的倒入5品脱酒瓶,倒满为止; beer_8 -= ( V_5 - beer_5 ) ; beer_5 = V_5 ; //把5品脱容器内酒的倒入12品脱酒瓶,倒完为止; beer_12 += beer_5 ; beer_5 = 0 ; printf("%u,%u,%u\n", beer_12 , beer_8 , beer_5 ); system("PAUSE"); return 0; }
运行结果为:
6,6,0