【心血来潮】改进一个大学时写的高中时构思的初中时想做的程序

  对于C语言,虽然做开发一直在用,但是却越来越觉得似乎与它并不亲切。可能是源于入门C语言那时用的是谭浩强的那本《C程序设计》吧,每次提到它都不得不吐槽一下:“多少人被他的书启蒙而开启了编程世界的大门……然后关上门头也不回的跑了!”我挺早就开始用这本教材启蒙编程,但此后几乎十年没敢去碰指针,直到后面看了前桥和弥著的《征服C指针》……

  接近年底了,工作上终于搞定之前忙了挺长一段时间的项目,终于有了些许闲暇。借“双十一”又扫回来一些诸如《C语言程序设计现代方法》《C Primer Plus》之类的经典教材,趁着有这么小段闲暇打算好好的放空已有的经验,重新开始从最基础去再触碰它。开始认认真真把每一章的练习题和编程题哪怕是最简单的也逼着自己敲出代码去验证,才发现自己在这方面的水平真的没有想象中那么够用。

  似乎是做着哪道题的灵光一闪,突然想起了以前大一那时写的一个解一元二次方程的程序(在 这里 有当时写的介绍)。简单说来,就是一个敲入一元二次方程之后可以给出一个“可以直接写在作业本上”的答案的程序,比如保留根号、保留分数等等,比如你计算出来是“$1 \pm \sqrt {2}$”,你总不能在作业本上写“X1=2.41421356……”吧;比如计算出来是“2/3”,总不能在作业本上写“X=0.6666666……”吧。虽然现成的解一元二次方程的程序很多,能给个写作业用得上的还真不大好找。

  最早做那个程序是初中那会儿想做的,初中的时候同桌有一台可以编程和绘图的Casio计算器,当时正是学一元二次方程的时候,就用那个计算器自带的编程功能写了一个解这个用的小程序,但是就是得的结果如果是无理数或者小数就直接出一个结果也没法抄上作业本。后来到了高中有了手机,在技术论坛上大家都兴写ELF格式的程序给那台手机用,就想写一个这个程序出来放手机上,但无奈技术水平不足没能实现。再后来上了大学,在机房做完练习闲得蛋疼终于完成了这个程序。

  但是之前写那个程序有一些当时未解决的遗憾是不能化简分数和根号,比如“2/4”不能化简为“1/2”,“$\sqrt {8}$”不能化简为“$2\sqrt {2}$”,而且当时又用上“math.h”,又用上“double”型,又各种“goto”,着实不是一个好程序应有的样子。于是这次突然心血来潮,打算对它进行改进,尽量通过最容易看得懂的方式(未必是最简单、效率最高的)来重新写一遍。

  首先是程序结构上,之前的各种“goto”是个不推荐使用的坏习惯,于是这次老老实实设计了顺序流程,用一个 do{…}while(…); 来构建整个主函数,总算看起来比之前顺眼一些了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* @brief 主函数入口
* @param None
* @retval None
* @note 用于进行基本的输入输出操作,以及初步做判断以调用不同计算函数。
*/
int main(void)
{
int a,b,c,dlt; //方程组化简后的a,b,c,Delta

do{
if(welcome(&a,&b,&c)) //显示屏幕提示并输入数字
{
if (0==a) //一次方程
{
printf("\n此为一次方程,您输入的算式为 %dx+%d=0\n",b,c);
rationalNum((-c),b,0);
}
else
{
printf("\n您输入的算式为 %dx^2+%dx+%d=0\n",a,b,c);
printf("(注:答案中的\"/\"表示根号,其后括号内为根号内数值)\n");
dlt = delta(a,b,c);
if (0==dlt) rationalNum((-b),(2*a),0); //只有一个解
else
{
if (dlt<0) //判断是否为虚数解
{
printf("\n此一元二次方程无实数解,其虚数解为:\n");
printf("(注:答案中的\"i\"表示虚数单位,-1=i*i)\n");
}
ansNum(a,b,c,dlt); //有两个解
}
}
}
}while(quitProg());

return 0;
}

  再然后是解决分数和分式的化简问题,主要构想是通过寻找分式中各项的最大公约数,然后再用各项去除以最大公约数,就可以实现化简了。计算公约数采用的是“辗转相除法”,三个数的最大公约数也差不多是直接用两数的公约数与第三数再做同样运算就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* @brief 计算最大公约数
* @param m 计算最大公约数的第一个数字
* @param n 计算最大公约数的第二个数字
* @retval 返回m和n的最大公约数
*/
int gcd(int m,int n)
{
int gcdTemp;
//两数交换
if (m<n)
{
gcdTemp=n;
n=m;
m=gcdTemp;
}
gcdTemp=m;
//欧几里得算法
while(n)
{
gcdTemp=n;
n=m%n;
m=gcdTemp;
}
if (gcdTemp<0) gcdTemp=-gcdTemp;
return gcdTemp;
}

/**
* @brief 计算三个数的最大公约数
* @param x 计算最大公约数的第一个数字
* @param y 计算最大公约数的第二个数字
* @param z 计算最大公约数的第三个数字
* @retval 返回x,y和z的最大公约数
*/
int gcd3(int x,int y,int z)
{
return gcd(gcd(x,y),z);
}

  其实写完了之后才想起,用嵌套调用的方式可以让代码更简洁的……

  在平方根的化简上面,我的想法是,先把“△”看做是“$n^2 m$”的格式,然后在$n^2 < \Delta$的情况下枚举 1~$\sqrt {n}$ 的平方是否能被 △ 整除并得到相应的m,取 n 的最大满足结果,从而把$\sqrt {\Delta}$化简为$n\sqrt{m}$,从而可以继续通过m的值是否为1来判断是否刚好不需要根号。虽然这种算法并不是最简洁高效的,但是程序的阅读上挺好理解。所以写了下面这个函数直接用指针对外部的 n 和 m 进行操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* @brief 根号化简函数
* @param radNum 根号内的值
* @param *outRad 根号外倍数的地址
* @param *inRad 根号内数字的地址
* @retval 进行了化简返回1,未进行化简返回0
* @note 将“√(n)”视作“a√(b)”格式来进行化简。
*/
int simpRadical(int radNum,int *outRad,int *inRad)
{
int i,out=0,in=0;
for (i=1;(i*i)<=radNum;i++)
{
if (radNum%(i*i)==0)
{
out = i;
in = radNum/(i*i);
}
}
if (1 == out) return 0;
else
{
*outRad=out;
*inRad=in;
return 1;
}
}

  到这里基本上就把之前遗留下来的遗憾全部解决了,其余的就是一些调整格式之类的小问题了。完整代码我已经搬运到我自己的Git托管平台了(点击 这里 查看),在“MinGW Developer Studio”环境下编译通过,可以下这个编译好的来玩玩儿:

  程序运行起来的效果大概如下图:

  整体程序看下来其实没什么技术含量,只是当时没想到或者懒得做而已,这次也借着改进这个程序重新让自己熟悉一下C语言各个方面的基础应用而已。唯有先把基础的东西处理扎实了,上层建筑才能稳固,空中楼阁只有倒塌的下场。

文章目录
|