《一站式C语言》读书笔记(7)

第五章 深入理解函数
5.1 return 语句
在返回值的函数中,return语句的作用是提供整个函数的返回值,并结束当前函数返回到调用它的地方。在没有返回值的函数中也可以使用return语句,例如当检查到一个错误时提前结束当前函数的执行并返回:

#include 
#include 
void print_logarithm(double x)
{
if(x<=0.0)
{
    printf(“Positive number only,please.\n”);
    return;
}
    printf(“The log of x is %f\n”,log(x));
}

这个函数首先检查参数x是否大于0,如果x不大于0就打印“Positive number only,please”。然后提前结束函数的执行并返回到调用者,只有当x大于0时才会计算x的自然对数,在打印了计算结果之后到达函数末尾,结束函数的执行并返回。注意,使用数学函数log需要包含头文件math.h ,由于x是浮点数,应该与同类型的数做比较,所以写成0.0。
接下来我们根据来定义一个函数判断输入数字的奇偶性:

int is_even(int x )
{
  if(x%2==0)
    return 1;
  else 
    return 0;
}

于是我们就可以这样调用函数:

int i=19;
if(is_even(i)){
    /*do something*/
} else {
    /*do something*/
}

函数名通常带有is或if等表示判断的词,这类函数也叫做谓词(Predicate)。然而上面的is_even函数不够简洁,改一下可以得到下面函数

int is_even(int x )
{
    return !(x%2);
}

函数的返回值应该这样理解:函数返回一个值相当于定义一个和返回值类型相同的临时变量并用return后面的表达式来初始化。

习题:
1.编写一个布尔函数int is_leap_year(int year),判断参数year是不是闰年。如果某一年的年份能被4整除,但不能被100整除,那么这一年就是闰年,此外,能被400整除的年份也是闰年。

Int is_leap_year(int year)
{
if((year%4==0&&year%100!=0)||year%400==0)
    return 1;//闰年
else 
    return 0;//不是闰年
}

2. 编写一个函数double myround(double x),输入一个小数,将它四舍五入。 例如myround(-3.51)的值是-4.0,myround(4.49)的值是4.0。可以调用math.h中的库函数ceil和floor实现这个函数,代码要尽可能简洁高效。

#include 
double myround(double x)
{
    if(((x*10)%10-5)>0)
        return ceil(x);
    else 
        return floor(x);
}

5.2 增量式开发
本章节个人觉得是比较重要的,主要是如何开始做自己的C语言项目,因此,我就不精简了,直接将完整的章节写出来。
本节提出的一种增量式(Incremental)开发的思路,很适合初学者。
现在问题来了:我们要编一个程序求圆的面积,圆的半径以两个端点的坐标(x1,y1)和(x2,y2)给出。首先分析和分解问题,把大问题分解成小问题,再对小问题分别求解。这个问题可分为两步:
由两个端点坐标求半径的长度,我们知道平面上两点间距离的公式是:

distance=√(〖(x1-x2)〗^2+〖(y1-y2)〗^2 )

括号里的部分都可以用我们学过的C语言表达式来表示,求平方根可以用match.h中的sqrt函数。因此这个小问题全部都可以用我们学过的只是解决。这个公式可以实现成一个函数,参数是两点的坐标,返回值是distance。
上一步算出的距离是圆的半径,已知圆的半径之后求面积的公式是:

area=π*radius^2

也可以用我们学过的C语言表达式来解决,这个公式也可以实现成一个函数,参数是radius,返回值是area。
首先编写distance这个函数,我们已经明确了它的参数是两点的坐标,返回值是两点间距离,可以先写一个简单的函数定义:

double distance(double x1, double y1, double x1, double y2, ){
    return 0.0;
}

初学者写到这里就已经不太自信了:这个函数定义写的对么?虽然我是按我理解的语法规则写的,但是书上没有和这个一模一样的例子,万一不小心遗漏了什么呢?既然不自信就不要再往下写了,没有一个平稳的心态来写程序很可能会引入bug。所以在函数定义中插入一个return 0.0;立刻结束掉它,然后立刻测试这个函数定义得有没有错:

double distance(double x1, double y1, double x1, double y2, ){
    printf(“distance is %f\n”,distance(1.0,2.0,4.0,6.0));
    return 0.0;
}

编译,运行,一切正常。这时你就会建立起信心:既然没问题,就不用管它了,继续往下写。在测试时给这个函数的参数是(1.0,2.0)和(4.0,6.0),两点的X坐标距离是3.0,Y坐标距离是4.0,因此两点间距离应该是5.0,你必须事先知道正确答案是5.0,这样你才能测试程序计算的结果对不对。当然现在函数还没有实现,计算结果肯定是不对的。现在我们再往函数里添加一点代码。

double distance(double x1, double y1, double x1, double y2, ){
double dx=x2-x1;
double dy=y2-y1;
    printf(“distance is %f\n”,distance(1.0,2.0,4.0,6.0));
    return 0.0;
}

如果你不确定dx和dy这样初始化行不行,那么久次打住,在函数里插一条打印语句把dx和dy的值打出来看看。把它和上面的main函数一起编译运行,由于我们事先知道结果应该是3.0和4.0,因此能够验证程序算的对不对。一旦验证无误,函数里的这句打印就可以撤掉了,像这种打印语句,以及我们用来测试的mai函数,都起到了类似脚手架(scaffold)的作用:在盖房子时很有用,但它不是房子的一部分,房子盖好之后就可以拆掉了。写代码可以有一个更高明的解决办法:把scaffolding的代码注释掉。

double distance(double x1, double y1, double x1, double y2, ){
double dx=x2-x1;
double dy=y2-y1;
    /*printf(“distance is %f\n”,distance(1.0,2.0,4.0,6.0));*/
    return 0.0;
}

这样如果以后出现了新的bug又需要跟踪调试时,还可以把这句重新加进代码中使用。两点的X坐标距离和Y坐标距离都没有问题了,下面求它们的平方和。

double distance(double x1, double y1, double x1, double y2, ){
double dx=x2-x1;
double dy=y2-y1;
double dsquared=dx*dx+dy*dy;
printf(“dsquared is %f\n”,dsquared);
    return 0.0;
}

然后再编译,运行,看看是不是得25.0.这样的增量式开发非常适合初学者,每写下一行代码都编译运行,确保没有问题了再写下一行,一方面在写代码时更有信心,另一方面也方便了调试:总是有一个先前的正确版本做参照,改动之后如果出了问题,几乎可以肯定就是刚才改的那行代码出的问题,这样就避免了必须从很多行代码中查找分析到底是哪一行出的问题。在这个过程中printf功不可没,你怀疑哪一行代码有问题,就插一个printf进去看看中间的计算结果,任何错误都可以通过这个办法找出来。以后我们会介绍程序调试工具gdb,它提供了更强大的调试功能帮你分析更隐蔽的错误。但即使有了gdb,printf这个最原始的办法仍然是最直接,最有效的。最后一步,我们完成这个函数:

#include 
#include 
double distance(double x1, double y1, double x1, double y2, ){
double dx=x2-x1;
double dy=y2-y1;
double dsquared=dx*dx+dy*dy;
double result=sqrt(dsquared);
    return result;
}

int main(void)
{
printf(“distance is %f\n”,distance(1.0,2.0,4.0,6.0));
return 0;
}

然后编译运行,看看是不是得5.0。随着编程经验越来越丰富,你可能每次写若干代码再一起测试,而不是像现在这样每写一行就测试一次,但不管怎么样,增量式开发的思路是很有用的,它可以帮你节省大量的调试时间,不管你有多强,都不应该一口气写完整个程序再编译运行,那几乎是一定会有bug的,到那时候再找bug就难了。
这个程序中引入了很多临时变量:dx,dy,dsquared,result,如果你有信心把整个表达式一次性写好,也可以不用临时变量:

double distance(double x1, double y1, double x1, double y2, ){
    return sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1));
}

这样写简洁得多了。但如果写错了呢?只知道是这一长串表达式有错,根本不知道错在哪,而且整个函数就一个语句,插printf都没地方插。所以用临时变量有它的好处,使程序更清晰,调试更方便,而且有时候可以避免不必要的计算,例如上面这一行表达式要把(x2-x1)计算两边,如果算完了(x2-x1)把结果存在一个临时变量dx里,就不需要再计算第二遍了。
接下来编写area这个函数。

double area(double radius)
{
    return 3.1416*radius*radius;
}

给出两点的坐标求距离,给出半径求圆的面积,这两个问题都给解决了,如何把它们组合起来解决问题呢?给出半径的两端点坐标(1.0,2.0)和(4.0,6.0)求圆的面积,先用distance函数求出半径的长度,再把这个长度传给area函数:

double radius=distance(1.0,2.0,4.0,6.0);
double result=area(radius);

也可以这样:

double result=area(distance(1.0,2.0,4.0,6.0));

我们一直把“给出半径的两端点坐标求圆的面积”这个问题当做整个问题来看,如果它也是一个更大的程序当中的子问题呢?我们可以把先前的两个函数组合起来做成一个新的函数以便日后使用:

double area_point(double x1, double y1, double x1, double y2, ){
    return area(distance(x1,y1,x2,y2));
}

还有另一种组合的思路,不是把distace和area两个函数调用组合起来,而是把那两个函数中的语句组合到一起:

double area_point(double x1, double y1, double x1, double y2, ){
    double dx=x2-x1;
    double dy=y2-y1;
    double dsquared=dx*dx+dy*dy;
    return 3.1416*radius*radius;
}

这样组合是不理想的。这样组合了之后,原来写的distance和area两个函数还要不要了呢?如果不要了删掉,那么如果有些情况只需要求亮点之间的距离,或者只需要给定半径长度求圆的面积呢?area_point把所有语句都写在一起,太不灵活了,满足不了这样的需要。如果保留distance和area同时也保留这个area_point怎么样呢?area_point和distance有相同的代码,一旦在disatance函数中发现了bug,或者要升级distance这个函数采用更高的计算精度,那么不仅要修改distance,还要记着修改area_point,同理,要修改area也要记着修改area_point,维护重复的代码是非常容易出错的,在任何时候都要尽量避免。因此,尽可能复用(reuse)以前写的代码,避免写重复的代码。封装就是为了复用,把解决各种小问题的代码封装成函数,在解决第一个大问题时可以用这些函数,在解决第二个大问题时可以复用这些函数。
解决问题的过程是把大问题分解成小问题,小问题再分解成更小的问题,这个过程在代码中体现为函数的分层设计(Stratify)。distance和area是两个底层函数,解决一些很小的问题,而area_point是一个上层函数,上层函数通过调用底层函数来解决更大的问题,底层和上层函数都可以被更上一层的函数调用,最终所有的函数都直接或间接地被main函数调用,如下图:

屏幕快照 2016-07-26 下午12.57.18

5.3 递归
自己直接或间接调用自己的函数称为递归函数。
例子:求n!的值。
先把Base Case这种最简单的情况写进去:

 int factorial(int n )
{
if(n==0)
    return 1;
}

如果n不是0,那么该return n*factorial(n-1);于是,就有了下面的函数:

int factorial(int n)
{
if(n==0)
    return 1;
else {
    int recurse=factorial(n-1);
    int result=n*recurse;
    return result;
}
}

下图是整个递归函数的调用过程。写递归函数时一定要记得写Base Case。

屏幕快照 2016-07-26 下午1.05.17

习题:
1.编写递归函数求两个正整数a和b的最大公约数(GCD,Greatest Common Divisor),使用Euclid算法:
如果a除以b能整除,则最大公约数是b
否则,最大公约数等于b和a%b的最大公约数
Euclid算法是很容易证明的,请读者自己证明一下为什么这么算出最大公约数,最后,修改你的程序使之适用于所有整数,而不仅仅是正整数。

int Gcd(int a,int b)
{
if (a%b==0)
    return b;
else 
    return (b,a%b);
}

具体的原因可以参考这篇博客如何在C++中实现求两个整数的最大公约数和最小公倍数

适合所有整数的算法:

#include 
int Gcd(int a,int b)
{
if (a%b==0)
    return b;
else 
    return (b,a%b);
}

int main (void)
{
int a =12;
int b=80;
int num;
if(a==||b==0)
{
    printf(“there is no number.\n”);
}
else if(a>b)
{
   num= Gcd(a,b);
}
else 
    num=Gcd(b,a);
printf(“the answer is %d”,num);
}

2编写递归函数求Fibonacci数列的第n项,这个数列是这样定义的:
fib(0)=1
fib(1)=1
fib(n)=fib(n-1)+f(n-2)
上面两个看似毫不相干的问题之间却有一个有意思的联系:Lame定理如果Euclid算法需要k步来计算两个数的GCD,那么这两个数之中较小的一个必然大于等于Fibonacci数列的第K项。

#include 

int fib(int n)
{
    if(n==1||n==2)
        return 1;
    else
        return (fib(n-1)+fib(n-2));
}

int main (void)
{
    int k=15;
    printf ("the 15th of fib is %d",fib(k));
}

0 Comments
Leave a Reply