编程语言导论/参数匹配
几乎所有编程语言都为开发者提供了构建可从程序不同部分激活的小代码片段的能力。这种抽象在不同的语言中有着不同的名称。举几个例子,Haskell 提供函数。C 除了函数之外,还提供过程,过程是不返回值的函数。Java 和 C# 提供方法。Prolog 提供推理规则。为简单起见,本章将所有这些抽象称为函数。在任何情况下,调用代码都必须向被调用代码传递参数。传递这些参数的方式有很多,本章将讨论这些策略。
许多作者将函数声明中使用的参数称为形式参数。例如,下面的 Python 函数 div 有两个形式参数,被除数和除数
def div(dividend, divisor):
r = dividend / divisor
print "Result = ", r
当函数被调用时,我们向它传递所谓的实际参数。例如,在下面的代码片段中,我们向函数 div 传递了两个参数,变量 x 和常数 2.0
>>> x = 3.0
>>> print div(x, 2.0)
Result = 1.5
在这个例子中,存储在变量 x 中的值,也就是实际参数,用于初始化形式参数被除数的值。在这种情况下,形式参数和实际参数之间的匹配是位置匹配。换句话说,实际参数是根据它们在函数调用中出现的顺序与形式参数匹配的。虽然位置方法是匹配形式参数和实际参数的常用方法,但还有一种方法:名义匹配。
>>> x = 3.0
>>> div(divisor=2.0, dividend=x)
Result = 1.5
这次,我们明确地命名了与形式参数匹配的实际参数。名义匹配存在于少数编程语言中,包括 ADA。
一些编程语言允许开发者为参数分配默认值。例如,让我们考虑之前看到的相同 Python 函数 div,但这次实现略有不同
def div(dividend=1.0, divisor=1.0):
r = dividend / divisor
print "Result = ", r
在这个例子中,每个形式参数都被分配了一个默认值。如果与该形式参数对应的实际参数不存在,则将使用默认值。以下所有调用都是可能的
>>> div()
Result = 1.0
>>> div(dividend=3.2, divisor=2.3)
Result = 1.39130434783
>>> div(3.0, 1.5)
Result = 2.0
>>> div(3.0)
Result = 3.0
>>> div(1.5, divisor=3.0)
Result = 0.5
有很多语言使用默认值。C++ 是这个家族中一个著名的例子。例如,以下所有调用都会返回有效答案。如果实际参数的数量少于形式参数的数量,则使用默认值。在这种情况下,匹配是从最左边的声明向最右边的声明进行的。因此,默认值将应用于从最右边的声明到最左边的形式参数。
#include <iostream>
class Mult {
public:
int f(int a = 1, int b = 2, int c = 3) const {
return a * b * c;
}
};
int main(int argc, char** argv) {
Mult m;
std::cout << m.f(4, 5, 6) << std::endl;
std::cout << m.f(4, 5) << std::endl;
std::cout << m.f(5) << std::endl;
std::cout << m.f() << std::endl;
}
有一些编程语言允许开发者创建带可变数量参数的函数。最著名的例子是 C,C 中的典型例子是 printf 函数。该过程接收一个描述将要产生的输出的字符串作为输入。通过分析这个字符串,printf 函数的实现“知道”它可能接下来会接收哪些参数。可能是,因为 C 编译器中没有任何东西强制开发者传递正确数量的参数并使用正确的类型。例如,下面的程序很可能会由于段错误导致运行时错误。
#include "stdio.h"
int main(int argc, char **argv) {
printf("%s\n", argc);
}
C 语言为开发者提供了一种特殊的语法,以及一些库函数,使他们能够创建带可变数量参数的函数。例如,下面的函数可以将四个或更少的整数相加。第一个参数应该是函数期望的参数数量。变量 ap 是一个指向每个参数的数据结构,如传递给函数 foo 的参数。在循环内部,我们使用宏 va_arg 获取指向每个参数的指针。这些指针存储在数组 arr 中。注意,va_arg 是 C 中模拟参数多态性的一个例子。该语言没有类型构造器,也就是说,不能将类型传递给函数。但是,我们可以通过宏来模拟这种能力。
#include <stdio.h>
#include <stdarg.h>
int foo(size_t nargs, ...) {
int a = 0, b = 0, c = 0, d = 0;
int *arr[4];
va_list ap;
size_t i;
arr[0] = &a;
arr[1] = &b;
arr[2] = &c;
arr[3] = &d;
va_start(ap, nargs);
for(i = 0; i < nargs; i++) {
*arr[i] = va_arg(ap, int);
}
va_end(ap);
return a + b + c + d;
}
int main() {
printf("%d\n", foo(0));
printf("%d\n", foo(1, 2));
printf("%d\n", foo(2, 2, 3));
printf("%d\n", foo(3, 2, 3, 5));
}
同样,没有保证会向 foo 传递正确数量的参数。例如,下面的程序将打印值 10。这种行为的答案在于变量 u 的二进制表示。该变量在内存中占用 8 个字节,每半个字节被视为一个参数。
int main() {
// 10,00000000000000000000000000000011
unsigned long long int u = 8589934595;
printf("%d\n", foo(2, 5, u));
}