当前位置:嗨网首页>书籍在线阅读

04-函数原型和函数调用

  
选择背景色: 黄橙 洋红 淡粉 水蓝 草绿 白色 选择字体: 宋体 黑体 微软雅黑 楷体 选择字体大小: 恢复默认

7.1.2 函数原型和函数调用

至此,读者已熟悉了函数调用,但对函数原型可能不太熟悉,因为它经常隐藏在include文件中。程序清单7.2在一个程序中使用了函数cheer()和cube()。请留意其中的函数原型。

程序清单7.2 protos.cpp

// protos.cpp -- using prototypes and function calls
#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 ";
    cout << 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;
}

在程序清单7.2的程序中,只有在函数使用了名称空间std中的成员时,才在该函数中使用了编译指令using。下面是该程序的运行情况:

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作为讨论的基础。

1.为什么需要原型

原型描述了函数到编译器的接口,也就是说,它将函数返回值的类型(如果有的话)以及参数的类型和数量告诉编译器。例如,请看原型将如何影响程序清单7.2中下述函数调用:

double volume = cube(side);

首先,原型告诉编译器,cube()有一个double参数。如果程序没有提供这样的参数,原型将让编译器能够捕获这种错误。其次,cube()函数完成计算后,将把返回值放置在指定的位置——可能是CPU寄存器,也可能是内存中。然后调用函数(这里为main())将从这个位置取得返回值。由于原型指出了cube()的类型为double,因此编译器知道应检索多少个字节以及如何解释它们。如果没有这些信息,编译器将只能进行猜测,而编译器是不会这样做的。

读者可能还会问,为何编译器需要原型,难道它就不能在文件中进一步查找,以了解函数是如何定义的吗?这种方法的一个问题是效率不高。编译器在搜索文件的剩余部分时将必须停止对main()的编译。一个更严重的问题是,函数甚至可能并不在文件中。C++允许将一个程序放在多个文件中,单独编译这些文件,然后再将它们组合起来。在这种情况下,编译器在编译main()时,可能无权访问函数代码。如果函数位于库中,情况也将如此。避免使用函数原型的唯一方法是,在首次使用函数之前定义它,但这并不总是可行的。另外,C++的编程风格是将main()放在最前面,因为它通常提供了程序的整体结构。

2.原型的语法

函数原型是一条语句,因此必须以分号结束。获得原型最简单的方法是,复制函数定义中的函数头,并添加分号。对于cube(),程序清单7.2中的程序正是这样做的:

double cube(double x); // add ; to header to get prototype

然而,函数原型不要求提供变量名,有类型列表就足够了。对于cheers()的原型,该程序只提供了参数类型:

void cheers(int); // okay to drop variable names in prototype

通常,在原型的参数列表中,可以包括变量名,也可以不包括。原型中的变量名相当于占位符,因此不必与函数定义中的变量名相同。

C++原型与ANSI原型

ANSI C借鉴了C++中的原型,但这两种语言还是有区别的。其中最重要的区别是,为与基本C兼容,ANSI C中的原型是可选的,但在C++中,原型是必不可少的。例如,请看下面的函数声明:

void say_hi();

在C++中,括号为空与在括号中使用关键字void是等效的——意味着函数没有参数。在ANSI C中,括号为空意味着不指出参数——这意味着将在后面定义参数列表。在C++中,不指定参数列表时应使用省略号:

void say_bye(...); // C++ abdication of responsibility

通常,仅当与接受可变参数的C函数(如printf())交互时才需要这样做。

3.原型的功能

正如您看到的,原型可以帮助编译器完成许多工作;但它对程序员有什么帮助呢?它们可以极大地降低程序出错的几率。具体来说,原型确保以下几点:

  • 编译器正确处理函数返回值;
  • 编译器检查使用的参数数目是否正确;
  • 编译器检查使用的参数类型是否正确;如果不正确,则转换为正确的类型(如果可能的话)。

前面已经讨论了如何正确处理返回值。下面来看一看参数数目不对时将发生的情况。例如,假设进行了如下调用:

double z = cube();

如果没有函数原型,编译器将允许它通过。当函数被调用时,它将找到cube()调用存放值的位置,并使用这里的值。这正是ANSIC从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),这个值被用作cheers()的参数。编译器将再一次检查原型,并发现cheers()要求一个int参数,因此它将返回值转换为整数8。通常,原型自动将被传递的参数强制转换为期望的类型。(但第8章将介绍的函数重载可能导致二义性,因此不允许某些自动强制类型转换。)

自动类型转换并不能避免所有可能的错误。例如,如果将8.33E27传递给期望一个int值的函数,则这样大的值将不能被正确转换为int值。当较大的类型被自动转换为较小的类型时,有些编译器将发出警告,指出这可能会丢失数据。

仅当有意义时,原型化才会导致类型转换。例如,原型不会将整数转换为结构或指针。

在编译阶段进行的原型化被称为静态类型检查(static type checking)。可以看出,静态类型检查可捕获许多在运行阶段非常难以捕获的错误。