复习函数的基本知识
来复习一下介绍过的有关函数的知识。要使用 C++ 函数,必须完成如下工作:
- 提供函数定义;
- 提供函数原型;
- 调用函数。
库函数是已经定义和编译好的函数,同时可以使用标准库头文件提供其原型,因此只需正确地调用这种函数即可。本书前面的示例已经多次这样做了。例如,标准 C 库中有一个 strlen() 函数,可用来确定字符串的长度。相关的标准头文件 cstring 包含了 strlen() 和其他一些与字符串相关的函数的原型。这些预备工作使程序员能够在程序中随意使用 strlen() 函数。
然而,创建自己的函数时,必须自行处理这 3 个方面—定义、提供原型和调用。程序清单 7.1 用一个简短的示例演示了这 3 个步骤。
#include <iostream>
void simple();
int main() {
using namespace std;
cout << "main() will call the simple() function:\n";
simple();
return 0;
}
void simple() {
using namespace std;
cout << "I'm but a simple function.\n";
}
下面是该程序的输出:
main() will call the simple() function:
I'm but a simple function.
执行函数 simple() 时,将暂停执行 main() 中的代码;等函数 simple() 执行完毕后,继续执行 main() 中的代码。在每个函数定义中,都使用了一条 using 编译指令,因为每个函数都使用了 cout。另一种方法是,在函数定义之前放置一条 using 编译指令或在函数中使用 std::cout。
下面详细介绍这 3 个步骤。
定义函数
可以将函数分成两类:没有返回值的函数和有返回值的函数。没有返回值的函数被称为 void 函数,其通用格式如下:
void functionName(parameterList) {
statements(s);
return; //optional
}
其中,parameterList 指定了传递给函数的参数类型和数量,本章后面将更详细地介绍该列表。可选的返回语句标记了函数的结尾;否则,函数将在右花括号处结束。void 函数相当于 Pascal 中的过程、FORTRAN 中的子程序和现代 BASIC 中的子程序过程。通常,可以用 void 函数来执行某种操作。例如,将 Cheers! 打印指定次数(n)的函数如下:
void cheers(int n) //no return value
{
for(int i=0;i<n;i++) std::cout<<"Cheers!";
std::cout<<std::endl;
}
参数列表 int n 意味着调用函数 cheers() 时,应将一个 int 值作为参数传递给它。
有返回值的函数将生成一个值,并将它返回给调用函数。换句话来说,如果函数返回 9.0 的平方根(sqrt(9.0)),则该函数调用的值为 3.0。这种函数的类型被声明为返回值的类型,其通用格式如下:
typeName functionName(parameterList) {
statements;
return value; //value is type cast to type typeName
}
对于有返回值的函数,必须使用返回语句,以便将值返回给调用函数。值本身可以是常量、变量,也可以是表达式,只是其结果的类型必须为 typeName 类型或可以被转换为 typeName(例如,如果声明的返回类型为 double,而函数返回一个 int 表达式,则该 int 值将被强制转换为 double 类型)。然后,函数将最终的值返回给调用函数。C++ 对于返回值的类型有一定的限制:不能是数组,但可以是其他任何类型—整数、浮点数、指针,甚至可以是结构和对象!(有趣的是,虽然 C++ 函数不能直接返回数组,但可以将数组作为结构或对象组成部分来返回。)
作为一名程序员,并不需要知道函数是如何返回值的,但是对这个问题有所了解将有助于澄清概念。(另外,还有助于与朋友和家人交换意见。)通常,函数通过将返回值复制到指定的 CPU 寄存器或内存单元中来将其返回。随后,调用程序将查看该内存单元。返回函数和调用函数必须就该内存单元中存储的数据的类型达成一致。函数原型将返回值类型告知调用程序,而函数定义命令被调用函数应返回什么类型的数据(参见图 7.1)。在原型中提供与定义中相同的信息似乎有些多余,但这样做确实有道理。要让信差从办公室的办公桌上取走一些物品,则向信差和办公室中的同事交代自己的意图,将提高信差顺利完成这项工作的概率。
函数在执行返回语句后结束。如果函数包含多条返回语句(例如,它们位于不同的 if else 选项中),则函数在执行遇到的第一条返回语句后结束。例如,在下面的例子中,else 并不是必需的,但可帮助马虎的读者理解程序员的意图:
int bigger(int a,int b) {
if(a>b) return a; //if a>b ,function terminates here
else return b; //oterwise,function terminates here
}
如果函数包含多条返回语句,通常认为它会令人迷惑,有些编译器将针对这一点发出警告。然而,这里的代码很简单,很容易理解。
有返回值的函数与 Pascal、FORTRAN 和 BASIC 中的函数相似,它们向调用程序返回一个值,然后调用程序可以将其赋给变量、显示或将其用于别的用途。下面是一个简单的例子,函数返回 double 值的立方:
double cube(double x) //x times x times x
{
return x*x*x; //a type double value
}
例如,函数调用 cube(1, 2) 将返回 1.728。请注意,上述返回语句使用了一个表达式,函数将计算该表达式的值(这里为 1.728),并将其返回。
函数原型和函数调用
至此,读者已熟悉了函数调用,但对函数原型可能不太熟悉,因为它经常隐藏在 include 文件中。程序清单 7.2 在一个程序中使用了函数 cheer() 和 cube()。请留意其中的函数原型。
#include <iostream>
void cheers(int); //prototype :no return value
double cube(double x); //prototype:returns a double
int main() {
using namespace std;
cheers(5); //function call
cout << "Give me a number:";
double side;
cin >> side;
double volume = cube(side); //function call
cout<< "A " << side << " -foot cube has a volume of " << volume << " cubic feet.\n";
cheers(cube(2)); //prototype protection at work
return 0;
}
void cheers(int n) {
using namespace std;
for (int i = 0; i < n; i++) cout << "Cheers! ";
cout << endl;
}
double cube(double x) {
return x * x * x;
}
运行结果
Cheers! Cheers! Cheers! Cheers! Cheers!
Give me a number:5
A 5 -foot cube has a volume of 125 cubic feet.
Cheers! Cheers! Cheers! Cheers! Cheers! Cheers! Cheers! Cheers!
main() 使用函数名和参数(后面跟一个分号)来调用 void 类型的函数:cheers(5);,这是一个函数调用语句。但由于 cube() 有返回值,因此 main() 可以将其用在赋值语句中:
double volume=cube(side);
但正如前面指出的,读者应将重点放在原型上。那么,应了解有关原型的哪些内容呢?首先,需要知道 C++ 要求提供原型的原因。其次,由于 C++ 要求提供原型,因此还应知道正确的语法。最后,应当感谢原型所做的一切。下面依次介绍这几点,将程序清单 7.2 作为讨论的基础。
为什么需要原型
原型描述了函数到编译器的接口,也就是说,它将函数返回值的类型(如果有的话)以及参数的类型和数量告诉编译器。例如,请看原型将如何影响程序清单 7.2 中下述函数调用:
double volume=cube(side);
首先,原型告诉编译器,cube() 有一个 double 参数。如果程序没有提供这样的参数,原型将让编译器能够捕获这种错误。其次,cube() 函数完成计算后,将把返回值放置在指定的位置—可能是 CPU 寄存器,也可能是内存中。然后调用函数(这里为 main())将从这个位置取得返回值。由于原型指出了 cube() 的类型为 double,因此编译器知道应检索多少个字节以及如何解释它们。如果没有这些信息,编译器将只能进行猜测,而编译器是不会这样做的。
读者可能还会问,为何编译器需要原型,难道它就不能在文件中进一步查找,以了解函数是如何定义的吗?这种方法的一个问题是效率不高。编译器在搜索文件的剩余部分时将必须停止对 main() 的编译。一个更严重的问题是,函数甚至可能并不在文件中。C++ 允许将一个程序放在多个文件中,单独编译这些文件,然后再将它们组合起来。在这种情况下,编译器在编译 main() 时,可能无权访问函数代码。如果函数位于库中,情况也将如此。避免使用函数原型的唯一方法是,在首次使用函数之前定义它,但这并不总是可行的。另外,C++ 的编程风格是将 main() 放在最前面,因为它通常提供了程序的整体结构。
原型的语法
函数原型是一条语句,因此必须以分号结束。获得原型最简单的方法是,复制函数定义中的函数头,并添加分号。对于 cube(),程序清单 7.2 中的程序正是这样做的:
double cube(double x); //add ;to header to get prototype
然而,函数原型不要求提供变量名,有类型列表就足够了。对于 cheer() 的原型,该程序只提供了参数类型:
void cheers(int); //okay to drop variable names in prototype
通常,在原型的参数列表中,可以包括变量名,也可以不包括。原型中的变量名相当于占位符,因此不必与函数定义中的变量名相同。
原型的功能
正如您看到的,原型可以帮助编译器完成许多工作;但它对程序员有什么帮助呢?它们可以极大地降低程序出错的几率。具体来说,原型确保以下几点:
- 编译器正确处理函数返回值;
- 编译器检查使用的参数数目是否正确;
- 编译器检查使用的参数类型是否正确。如果不正确,则转换为正确的类型(如果可能的话)。
前面已经讨论了如何正确处理返回值。下面来看一看参数数目不对时将发生的情况。例如,假设进行了如下调用:
double z=cube();
如果没有函数原型,编译器将允许它通过。当函数被调用时,它将找到 cube() 调用存放值的位置,并使用这里的值。这正是 ANSI C 从 C++ 借鉴原型之前,C 语言的工作方式。由于对于 ANSI C 来说,原型是可选的,因此有些 C 语言程序正是这样工作的。但在 C++ 中,原型不是可选的,因此可以确保不会发生这类错误。
接下来,假设提供了一个参数,但其类型不正确。在 C 语言中,这将造成奇怪的错误。例如,如果函数需要一个 int 值(假设占 16 位),而程序员传递了一个 double 值(假设占 64 位),则函数将只检查 64 位中的前 16 位,并试图将它们解释为一个 int 值。但 C++ 自动将传递的值转换为原型中指定的类型,条件是两者都是算术类型。例如,程序清单 7.2 将能够应付下述语句中两次出现的类型不匹配的情况:
cheers(cube(2));
首先,程序将 int 的值 2 传递给 cube(),而后者期望的是 double 类型。编译器注意到,cube() 原型指定了一个 double 类型参数,因此将 2 转换为 2.0—一个 double 值。接下来,cube() 返回一个 double 值(8.0),这个值被用作 cheer() 的参数。编译器将再一次检查原型,并发现 cheer() 要求一个 int 参数,因此它将返回值转换为整数 8。通常,原型自动将被传递的参数强制转换为期望的类型。(但第 8 章将介绍的函数重载可能导致二义性,因此不允许某些自动强制类型转换。)
自动类型转换并不能避免所有可能的错误。例如,如果将 8.33E27 传递给期望一个 int 值的函数,则这样大的值将不能被正确转换为 int 值。当较大的类型被自动转换为较小的类型时,有些编译器将发出警告,指出这可能会丢失数据。
仅当有意义时,原型化才会导致类型转换。例如,原型不会将整数转换为结构或指针。
在编译阶段进行的原型化被称为静态类型检查(static type checking)。可以看出,静态类型检查可捕获许多在运行阶段非常难以捕获的错误。

