1. return语句

之前我们一直在main函数中使用return语句,现在是时候全面深入地学习一下了。在有返回值的函数中,return语句的作用是提供整个函数的返回值,并结束当前函数返回到调用它的地方。在没有返回值的函数中也可以使用return语句,例如当检查到一个错误时提前结束当前函数的执行并返回:

#include <math.h>

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

这个函数首先检查参数x是否大于0,如果x不大于0就打印错误提示,然后提前结束函数的执行返回到调用者,只有当x大于0时才能求对数,在打印了对数结果之后到达函数体的末尾,自然地结束执行并返回。注意,使用数学函数log需要包含头文件math.h,由于x是浮点数,应该与同类型的数做比较,所以写成0.0。

第 2 节 “if/else语句”中我们定义了一个检查奇偶性的函数,如果是奇数就打印x is odd.,如果是偶数就打印x is even.。事实上这个函数并不十分好用,我们定义一个检查奇偶性的函数往往不是为了打印两个字符串就完了,而是为了根据奇偶性的不同分别执行不同的后续动作。我们可以把它改成一个返回布尔值的函数:

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

有些人喜欢写成return(1);这种形式也可以,表达式外面套括号表示改变运算符优先级,在这里不起任何作用。我们可以这样调用这个函数:

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

返回布尔值的函数是一类非常有用的函数,在程序中通常充当控制表达式,函数名通常带有isif等表示判断的词,这类函数也叫做谓词(Predicate)is_even这个函数写得有点啰嗦,x % 2这个表达式本来就有0值或非0值,直接把这个值当作布尔值返回就可以了:

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

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

int 临时变量 = !(x % 2);
函数退出,局部变量x的存储空间释放;
if (临时变量) { /* 临时变量用完就释放 */
	/* do something */
} else {
	/* do some other thing */
}

if语句对函数的返回值做判断时,函数已经退出,局部变量x已经释放,所以不可能在这时候才计算表达式!(x % 2)的值,表达式的值必然是事先计算好了存在一个临时变量里的,然后函数退出,局部变量释放,if语句对这个临时变量的值做判断。注意,虽然函数的返回值可以看作是一个临时变量,但我们只是读一下它的值,读完值就释放它,而不能往它里面存新的值,换句话说,函数的返回值不是左值,或者说函数调用表达式不能做左值,因此下面的赋值语句是非法的:

is_even(20) = 1;

第 3 节 “形参和实参”中讲过,C语言的传参规则是Call by Value,按值传递,现在我们知道返回值也是按值传递的,即便返回语句写成return x;,返回的也是变量x的值,而非变量x本身,因为变量x马上就要被释放了。

在写带有return语句的函数时要小心检查所有的代码路径(Code Path)。有些代码路径在任何条件下都执行不到,这称为Dead Code,例如把&&和||运算符记混了(据我了解初学者犯这个低级错误的不在少数),写出如下代码:

void foo(int x, int y)
{
	if (x >= 0 || y >= 0) {
		printf("both x and y are positive.\n");
		return;
	} else if (x < 0 || y < 0) {
		printf("both x and y are negetive.\n");
		return;
	}
	printf("x has a different sign from y.\n");
}

最后一行printf永远都没机会被执行到,是一行Dead Code。有Dead Code就一定有Bug,你写的每一行代码都是想让程序在某种情况下去执行的,你不可能故意写出一行永远不会被执行的代码,如果程序在任何情况下都不会去执行它,说明跟你预想的不一样,要么是你对所有可能的情况分析得不正确,也就是逻辑错误,要么就是像上例这样的笔误,语义错误。还有一些时候,对程序中所有可能的情况分析得不够全面将导致漏掉一些代码路径,例如:

int absolute_value(int x)
{
	if (x < 0) {
		return -x;
	} else if (x > 0) {
		return x;
	}
}

这个函数被定义为返回int,就应该在任何情况下都返回int,但是上面这个程序在x==0时安静地退出函数,什么也不返回,C语言对于这种情况会返回什么结果是未定义的,通常返回不确定的值,等学到第 1 节 “函数调用”你就知道为什么了。另外注意这个例子中把-号当负号用而不是当减号用,事实上+号也可以这么用。正负号是单目运算符,而加减号是双目运算符,正负号的优先级和逻辑非运算符相同,比加减的优先级要高。

以上两段代码都不会产生编译错误,编译器只做语法检查和最简单的语义检查,而不检查程序的逻辑[7]。虽然到现在为止你见到了各种各样的编译器错误提示,也许你已经十分讨厌编译器报错了,但很快你就会认识到,如果程序中有错误编译器还不报错,那一定比报错更糟糕。比如上面的绝对值函数,在你测试的时候运行得很好,也许是你没有测到x==0的情况,也许刚好在你的环境中x==0时返回的不确定值就是0,然后你放心地把它集成到一个数万行的程序之中。然后你把这个程序交给用户,起初的几天里相安无事,之后每过几个星期就有用户报告说程序出错,但每次出错的现象都不一样,而且这个错误很难复现,你想让它出现时它就不出现,在你毫无防备时它又突然冒出来了。然后你花了大量的时间在数万行的程序中排查哪里错了,几天之后终于幸运地找到了这个函数的Bug,这时候你就会想,如果当初编译器能报个错多好啊!所以,如果编译器报错了,不要责怪编译器太过于挑剔,它帮你节省了大量的调试时间。另外,在math.h中有一个fabs函数就是求绝对值的,我们通常不必自己写绝对值函数。

习题

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

2、编写一个函数double myround(double x),输入一个小数,将它四舍五入。例如myround(-3.51)的值是-4.0,myround(4.49)的值是4.0。可以调用math.h中的库函数ceilfloor实现这个函数。



[7] 有的代码路径没有返回值的问题编译器是可以检查出来的,如果编译时加-Wall选项会报警告。