Skip to content

Latest commit

 

History

History
431 lines (289 loc) · 16.8 KB

Think-C_05.md

File metadata and controls

431 lines (289 loc) · 16.8 KB

#第五章 有返回值的函数

##5.1 返回值

我们用过的一些内置函数,例如数学函数,会产生结果。那么,调用这样的函数就会生成一个新的值,通常要把它赋给一个变量,或作为一个表达式的一部分。例如:

double e = exp(1.0);
double height = radius * sin(argle);

但是,目前为止我们写过的都是 void 函数;也就是没有返回值的函数。调用 void 函数时,通常是独占一行,没有赋值语句:

PrintLines(3);
Countdown(n-1);

在这一节,我们将写的带有返回值的函数,我把它叫做有返回值的函数。第一个例子是 area,带一个 double 型参数,并返回给定半径的圆的面积:

double Area(double radius)
{
	double pi = acos(-1.0);
	double area = pi * redius * redius;
	return area;
}

首先要注意的是,这个函数定义的开头不一样了。这里用 double 代替了原先的 void ,它指出这个函数的返回值的类型是 double

其次,函数最后一行的 return 语句包含了一个值。这条语句的意思是,“立即从这个函数返回,并且用后面的表达式作为返回值”。这个表达式可以写的非常复杂,所以,这个函数可以写的更简洁:

double Area(double radius)
{
	return acos(-1.0)*radius*radius;
}

但是,像 area 这样的临时变量通常更容易调试。任何情况下,return 语句中的表达式的类型必须和该函数的返回值类型一致。换一句话说,当你声明的返回值类型是 double ,就意味着你承诺这个函数最终会产生一个 double 。如果你尝试在 return 中不带表达式,或者带一个错误类型的表达式,编译器会提醒你。

有时候,需要有多个 return 语句,根据情况返回不同的值:

double AbsoluteValue(double x)
{
	if(x<0)
	{
		return -x;
	}
	else
	{
		return x;
	}
}

因为这些 return 语句在一组 if 语句中,所以只有一条会被执行。虽然在一个函数中写多条 return 语句是合法的,但是你应该记住,只要执行了一条,该函数后面的语句就都不会执行了。

位于 return 语句之后的代码,或者其他永远不会被执行的代码,叫做 dead code 。有些编译器会对此给予警告。

如果把 return 语句放在了条件语句中,一定为每一种可能出现的情况写一条 return 语句。例如:

double AbsoluteValue(double x)
{
	if(x<0)
	{
		return -x;
	}
	else if(x>o)
	{
		return x;
	}					/* WRONG!! */
}

这个程序是错误的,因为如果 x 为 0,两个条件都不符合,函数最终不会执行 return 语句。不幸的是,很多 C 编译器不会注意这个错误。结果,这个程序可以编译并运行,但是当 x==0 的时候,返回值就是不确定的,在不同的环境下可能是不同的值。

现在,你可能讨厌看到编译器错误,但是这样可以获得更多的经验,你会明白,比编译器报错更坏的事情是编译器无法发现程序的错误。

会发生这样的事情:你用很多不同的 x 值来测试 AbsoluteValue 都工作正常。然后你把程序交给了其他人,他们在另一个环境中运行它。结果很奇怪的运行失败了,并且花了好几天来调试,最终发现问题在 AbsoluteValue 。如果编译器警告过你就不会这样了!

今后,如果编译器指出了一个程序中的错误,不要责怪编译器。相反,你应该感谢编译器发现了错误,并且要不遗余力的去调试。有些编译器提供一个选项,可以让编译器严格的检查并报告所有能够找到的错误。你应该一直开启这个选项

随便说一下,math 库中有一个函数叫做 fabs ,用于计算一个 double 数的绝对值。

##5.2 程序开发

本节你可以看到完整的 C 函数和它们的作用。但是不会像前面那么详细地介绍编写步骤。我会推荐一种技巧,我把它称作 逐步开发

举个例子,假设你想要通过两个点的坐标 (x1,y1) 和 (x2,y2) 计算它们之间的距离,通常这样计算:

5-1.jpg

第一步要考虑的是 C 语言的 Distance 函数应该是什么样的。换句话说,输入(参数)是什么,输出(返回值)是什么。

本例中,两个点是参数,那就需要四个 double 数来表示。返回值是距离,应该是 double 型。

现在可以写出这个函数的大致轮廓:

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

return 语句是一个浮点数,所以函数可以通过编译并返回,虽然答案并不正确。现在这个函数还没有任何用处,但是可以试着编译它,检验有没有什么语法错误。

为了测试这个新函数,我们要用简单的值来调用它。可以在 main 函数中添加:

double dist = Distance(1.0,2.0,4.0,6.0);
printf("%f\n",dist);

选这几个值的话,水平距离是 3 ,垂直距离是 4 ,结果就应该是 5 。当你测试一个函数的时候,你应该事先知道正确的结果是什么。

一旦我们检查完函数的语法,就可以开始逐行添加代码。每次修改后,就编译运行一次。这样的话,当出现错误时,我们就知道问题肯定在最后一次添加的代码中。

第二步是计算 x2-x1 和 y2-y1 。我将这两个计算的结果储存在变量 dx 和 dy 中。

double Distance(double x1,double y1,double x2,double y2)
{
	double dx = x2 - x1;
	double dy = y2 - y1;
	printf("dx is %f\n", dx);
	printf("dy is %f\n", dy);
	return 0.0;
}

我添加了输出语句,方便检查计算结果。前面已经提到,它们的结果应该是 3.0 和 4.0。

写完函数后,我会删除输出语句。像这样的代码叫做 脚手架 ,因为它有助于编写程序,但又不是最终程序的一部分。如果稍后还需要 脚手架 代码,可以只将其注释,而不删除。

下一步就是计算 dx 和 dy 的平方。我们可以使用 pow 函数,但是直接自身相乘会更快更简单。

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

同样的,我们可以在这里编译运行程序,检查结果(应该是 25.0)。

最后,我们可以用 sqrt 函数计算并返回结果。

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

然后在 main 函数中输出并检查结果。

当你获得更多的编程经验时,就可以一次编写并调试多行代码。这样逐步开发的过程可以节省大量的调式时间。

这个过程的关键是:

  • 由一个可工作的程序开始,逐步做小的修改。任何时候,如果出现了错误,你都知道错误的位置。
  • 使用临时变量保存每一步的值,这样就可以输出并检查它们。
  • 一旦程序可以正常工作了,你就可以删除一些 脚手架 ,或合并多个语句,但是不要影响程序的可读性。

##5.3 组合

让你所愿,一旦定义了一个新函数,就可以在表达式中使用它,并且可以用已经存在的函数来构建新的函数。例如,已知两个点:圆心和圆周上的一点,求圆的面积。

我们将圆心用变量 xc 和 yc 表示,圆周上的点用 xp 和 yp 表示。第一步就是得到圆的直径,也就是这两点的距离。幸运的是,我们已经有了一个函数,Distance,所以直径就是:

double radius = Distance(xc,yc,xp,yp);

第二步是计算通过直径计算圆的面积,并返回:

double result = Area(radius);
return result;

把上面的结合为一个函数就是:

double AreaFromPoints(double xc,double yc,double xp,double yp)
{
	double radius = Distance(xc,yc,xp,yp);
	double result = Area(radius);
	return result;
}

临时变量 radiusarea 有助于开发和调试,但是确定程序可以工作后,就可以让函数更简洁:

double AreaFromPoints(double xc,double yc,double xp,double yp)
{
	return Area(Distance(xc,yc,xp,yp));
}

##5.4 布尔值

到目前为止,我们看到的数据类型都可以保存很大的值。包括很多整数,更多的是浮点数。相对而言,字符就小了很多。很多计算机语言使用的基本类型甚至更小。它叫做 __Bool(布尔),只有两个值:true(真) 和 false(假) 。

不幸的是,早期的 C 语言标准没有将布尔当做一种专门的类型,而是用整数值 0 和 1 来代替。0 表示 false ,1 表示 true 。特别要指出,C 语言把所有非 0 的整数都当做 true 。如果你通过与 1 比较来判断一个数是否为 true,就会出错。

暂且不讲这个问题,我们会在下一章使用布尔值。if 语句中的条件就是一个布尔表达式。比较运算符的结果就是布尔值。例如:

if(x == 5)
{
	/* do something */
}

运算符 == 比较两个整数,然后产生一个布尔值。

C99 标准没有为 truefalse 提供关键字。很多程序都用 C 语言的宏定义来代替。例如:

#define FALSE 0
#define TRUE 1
...
if(TRUE)
{
	/* will be always executed */
}

如果用在循环语句,程序会一直执行(除非遇到 returnbreak 语句)。

##5.5 布尔变量

C 语言没有直接支持布尔值,所以我们不能声明布尔类型的变量。程序员通常用 short 型变量结合宏定义来存放真值。

#define FALSE 0
#define TRUE 1
...
short fred;
fred = TRUE;
short testResult = FALSE

第一行是一个简单的变量声明;第二行是一个赋值,第三行是一个声明和赋值的组合,称作初始化。

前面已经提过,比较运算符的结果就是布尔型,所以可以存放到一个变量

short evenFlag = (n%2 == 0);    /* true if n is even */
short positiveFlag = (x > 0);   /* true if x is positive */

然后把它作为条件语句的一部分来使用:

if(evenFlag)
{
	printf("n was even when I checked it");
}

这样使用的变量叫做 flag ,因为它标示了条件的真假。

##5.6 逻辑运算符

C 语言有三种 逻辑运算符:AND,OR 和 NOT,非别用 && ,|| 和 ! 表示。这些运算符的含义和它们的英文意思类似。例如 x>0 && x<10 ,如果 x 大于 0 并且(AND)小于 10 ,这个表达式就为真。

如果 evenFlag || n%3 == 0 中有一个条件为真——也就是说,如果 evenFlag 为真或者(OR)n 可以被 3 整除——则表达式为真。

最后,NOT 运算符表示否定,它可以翻转一个布尔表达式,所以,如果 evenFlag 为假,!evenFlag 就为真,即 n 为奇数。

逻辑运算符通常提供了一种简单的嵌套条件语句的方法。例如,怎样只用一个条件语句就完成下面的代码:

if(x>0)
{
	if(x<10)
	{
		printf("x is a positive single digit.\n");
	}
}

##5.7 布尔函数

布尔函数就是返回布尔型数据的函数。它对于函数内部进行测试非常方便。例如:

int IsSingleDigit(int x)
{
	if(x>=0 && x<10)
	{
		return TRUE;
	}
	else
	{
		return FALSE;
	}
}

这个函数叫做 IsSingleDigit,听起来就像是一个 yes/no 的问题。返回值的类型是 int ,依然遵循 0 表示 false 和 1 表示 true 的规则。每一条 return 语句都要遵循这个惯例,我们用的是宏定义。

代码本身是很简单的,虽然它有一点多。别忘了表达式 x>=0 && x<10 的结果是布尔值,所以可以直接将其返回,无需 if 语句:

int IsSingleDigit(int x)
{
	return (x>=0 && x<10);
}

在 main 函数里直接调用:

printf("%i\n", IsSingleDigit(2));
short bigFlag = !IsSingleDigit(17);

第一行会输出 true 的值,因为 2 是一个单数。不幸的是,C 语言输出的布尔值不是单词 TRUEFALSE ,而是整数 1 和 0 。

第二行会将 true 的值赋给 bigFlag ,因为 17 不是一个正的单数。

布尔函数常用在条件语句里

if(IsSingleDigit(x))
{
	printf("x is little\n");
}
else
{
	printf("x is big\n");
}

##5.8 main 的返回值

我们已经学习了函数的返回值,现在可以详细的讲一下 main 函数的返回值。它支持返回一个整数:

int main(void)

通常从 main 返回的是 0 ,表示程序运行成功。如果出了什么差错,通常返回 -1 ,或者用其他整数代表一种错误现象。C 语言为此提供了两个常量,EXIT_SUCCESSEXIT_FAILURE

int main()
{
	return EXIT_SUCCESS;    /*program terminated successfully*/
}

当然,你可能想要知道谁会获得这个返回值,因为我们从来没有调用过 main 。事实是,当操作系统执行一个程序的时候,是从调用 main 开始的,调用的方法和其他函数是一样的。

系统可以向 main 传递一些参数,稍后我们会详细介绍。

##5.9 词汇

return type(返回类型): 函数返回值的类型。

return value(返回值): 调用函数后产生的值。

dead code(死代码): 程序里的一部分,永远都不会被执行,通常位于一个 return 语句之后。

scaffolding(脚手架): 程序开发过程中用到的代码,但是不会出现在最终版本中。

void(空): 一种特殊的返回类型,表示函数没有返回值。

boolean(布尔): 一种值或变量,只有两个状态,通常叫做 truefalse 。C 语言中,布尔值可以存放在 bool 型变量中。

flag(标识): 一种变量(通常是 bool 型),表示一种条件或状态。

comparison operator(比较运算符): 一种运算符,用于比较两个值,并产生一个表示二者关系的布尔值。

logical operator(逻辑运算符): 一种将布尔值组合到一起的运算符,用于测试复合条件。

##5.10 练习

Exercise 5.1

又是德文!!

Exercise 5.2

如果给你三根木棒,你能否将它们摆成一个三角形。例如,如果其中一根长 12 英寸,另外两根长 1 英寸,很明显,那两根短的无法连接到一起。对于任意三个长度,有一种简单的方法可以判断它们能否组成一个三角形:

“如果三个长度中,有一个比另外两个的和还要长,那就无法组成一个三角形。其他的情况都可以”

写一个名为 IsTriangle 的函数,参数为三个整数,返回值为 truefalse ,判断给定长度能否组成一个三角形。

这个练习的目的是用条件语句写一个带返回值的函数。

Exercise 5.3

德文。。。

Exercise 5.4

德文。。。

Exercise 5.5

德文。。。

Exercise 5.6

a. 新建一个名叫 Sum.c 的程序,输入如下两个函数。

int FunctionOne(int m,int n)
{
	if(m ==n)
	{
		return n;
	}
	else
	{
		return m + FunctionOne(m+1,n);
	}
}

int FunctionTwo(int m,int n)
{
	if(m == n)
	{
		return n;
	}
	else
	{
		return n * FunctionTwo(m, n-1);
	}
}

b. 在 main 函数中写几行代码测试这两个函数。多调用几次,使用一些不同的值,看看得到怎样的结果。通过测试和研究,指出这些函数是做什么的,并为它们起一个更有意义的名字。为函数添加注释,简单地描述函数的作用。

c. 在函数的开始处添加 printf 语句,打印参数的值。这是一个有用的调试技巧,这样可以展示程序的执行过程。

Exercise 5.7

(这个练习基于 Abelson 和 Sussman 的 Structure and Interpretation of Computer Pragrams 的 44 页)

下面的算法出现在 Euclid 的 Elements (Book 7,ca. 300 B.C.)。它可能是最古老的不平凡算法。

这个算法基于这样的情况,如果 r 是 a 除以 b 的余数,那么 a 和 b 的公因子等于 b 和 r 的公因子。因此可以用等式

gcd(a,b) = gcd(b,r)

将计算一对整数的 GCD 的问题简化为计算越来越小的整数的 GCD 。例如,

gcd(36,20) = gcd(20,16) = gcd(16,4) = gcd(4,0) = 4

所以 36 和 20 的 GCD 是 4 。它可以从任意一对数字开始,逐步减小,直到第二个数为零,这时第一个数就是这对数的 GCD 。

写一个名为 gcd 函数,带两个整型参数,使用 Euclid 的算法,计算并返回两个数的最大公因子(GCD)。

##笔记

  • so far :目前为止
  • in either case :在任一情况下
  • make a promise :做一个承诺
  • sick of :腻烦
  • from now on :今后
  • if only :但愿,只要
  • agreement :协议,约定
  • convention :惯例
  • positive :正
  • negative :负
  • remainder :余数
  • greatest common divisor :最大公因子