7.1. 函数的定义
函数由函数名以及一组操作数类型唯一地表示。函数的操作数,也即形参,在一对圆括号中声明,形参与形参之间以逗号分隔。
函数执行的运算在一个称为函数体的块语句中定义。每一个函数都有一个相关联的返回类型。
考虑下面的例子,这个函数用来求出两个 int 型数的最大公约数:
// return the greatest common divisor
int gcd(int v1, int v2)
{
while (v2) {
int temp = v2;
v2 = v1 % v2;
v1 = temp;
}
return v1;
}
这里,定义了一个名为 gcd 的函数,该函数返回一个 int 型值,并带有两个 int 型形参。
调用 gcd 函数时,必须提供两个 int 型值传递给函数,然后将得到一个 int 型的返回值。
函数的调用
C++ 语言使用调用操作符(即一对圆括号)实现函数的调用。正如其他操作符一样,调用操作符需要操作数并产生一个结果。
调用操作符的操作数是函数名和一组(有可能是空的)由逗号分隔的实参。
函数调用的结果类型就是函数返回值的类型,该运算的结果本身就是函数的返回值:
// get values from standard input cout << "Enter two values: \n"; int i, j; cin >> i >> j; // call gcd on arguments i and j // and print their greatest common divisor cout << "gcd: " << gcd(i, j) << endl;
函数调用做了两件事情:用对应的实参初始化函数的形参,并将控制权转移给被调用函数。
主调函数的执行被挂起,被调函数开始执行。函数的运行以形参的(隐式)定义和初始化开始。
也就是说,当我们调用 gcd 时,第一件事就是创建名为 v1 和 v2 的int 型变量,并将这两个变量初始化为调用gcd 时传递的实参值。
在上例中,v1 的初值为 i,而 v2 则初始化为 j 的值。
函数体是一个作用域
函数体是一个语句块,定义了函数的具体操作。通常,这个块语句包含在一对花括号中,形成了一个新的作用域。形参和实参
类似于局部变量,函数的形参为函数提供了已命名的局部存储空间。
它们之间的差别在于形参是在函数的形参表中定义的,并由调用函数时传递函数的实参初始化。
实参则是一个表达式。它可以是变量或字面值常量,甚至是包含一个或几个操作符的表达式。
在调用函数时,所传递的实参个数必须与函数的形参个数完全相同。
与初始化式的类型必须与初始化对象的类型匹配一样,实参的类型也必须与其对应形参的类型完全匹配:实参必须具有与形参类型相同、或者能隐式转换为形参类型的数据类型。
7.1.1. 函数返回类型
函数的返回类型可以是内置类型(如 int 或者 double)、类类型或复合类型(如 int& 或 string*),还可以是 void 类型,表示该函数不返回任何值。
下面的例子列出了一些可能的函数返回类型:
bool is_present(int *, int); // returns bool int count(const string &, char); // returns int Date &calendar(const char*); // returns reference to Date void process(); // process does not return a value
函数不能返回另一个函数或者内置数组类型,但可以返回指向函数的指针,或指向数组元素的指针的指针:
// ok: pointer to first element of the array
int *foo_bar() { /* ... */ }
这个函数返回一个
int 型指针,该指针可以指向数组中的一个元素。
函数必须指定返回类型
在定义或声明函数时,没有显式指定返回类型是不合法的:
// error: missing return type
test(double v1, double v2) { /* ... */ }
早期的 C++ 版本可以接受这样的程序,将 test 函数的返回类型隐式地定义为 int 型。但在标准 C++ 中,上述程序则是错误的。
<Note>:在 C++ 标准化之前,如果缺少显式返回类型,函数的返回值将被假定为 int 型。
早期未标准化的 C++ 编译器所编译的程序可能依然含有隐式返回 int 型的函数。
7.1.2. 函数形参表
函数形参表可以为空,但不能省略。没有任何形参的函数可以用空形参表或含有单个关键字 void 的形参表来表示。例如,下面关于process 的声明是等价的:
void process() { /* ... */ } // implicit void parameter list void process(void){ /* ... */ } // equivalent declaration
形参表由一系列用逗号分隔的参数类型和(可选的)参数名组成。如果两个参数具有相同的类型,则其类型必须重复声明:
int manip(int v1, v2) { /* ... */ } // error int manip(int v1, int v2) { /* ... */ } // ok
参数表中不能出现同名的参数。类似地,局部于函数的变量也不能使用与函数的任意参数相同的名字。
参数名是可选的,但在函数定义中,通常所有参数都要命名。参数必须在命名后才能使用。
参数类型检查
<Note>:C++ 是一种静态强类型语句,对于每一次的函数调用,编译时都会检查其实参。
调用函数时,对于每一个实参,其类型都必须与对应的形参类型相同,或具有可被转换为该形参类型的类型。函数的形参表为编译器提供了检查实参需要的类型信息。例如,第 7.1 节定义的gcd 函数有两个int 型的形参:
gcd("hello", "world"); // error: wrong argument types gcd(24312); // error: too few arguments gcd(42, 10, 0); // error: too many arguments以上所有的调用都会导致编译时的错误。在第一个调用中,实参的类型都是 const char*,这种类型无法转换为 int 型,因此该调用不合法。
而第二和第三个调用传递的实参数量有误。在调用该函数时必须提供两个实参,实参数太多或太少都是不合法的。
如果两个实参都是 double 类型,又会怎样呢?调用是否合法?
gcd(3.14, 6.29); // ok: arguments are converted to int
在 C++中,答案是肯定的:该调用合法!正如第 5.12.1 节所示,double 型的值可以转换为int 型的值。
本例中的函数调用正涉及了这种转换——用 double 型的值来初始化 int 型对象。因此,把该调用标记为不合法未免过于严格。
更确切地说,(通过截断)double 型实参被隐式地转换为 int 型。由于这样的转换可能会导致精度损失,大多数编译器都会给出警告。
对于本例,该调用实际上变为:
gcd(3, 6);返回值是 3。
调用函数时传递过多的实参、忽略某个实参或者传递错误类型的实参,几乎肯定会导致严重的运行时错误!
对于大程序,在编译时检查出这些所谓的接口错误(interface error),将会大大地缩短“编译-调试-测试”的周期。