函数

函数允许将程序组织成代码段,以执行单独的任务。

在C++中,函数是一组被命名并可以从程序某处调用的语句。定义函数的常用语法是:

type name ( parameter1, parameter2, ...) { statements }

其中
- type 是函数返回值的类型。
- name 是函数的可调用标识符。
- parameters(根据需要设置数量):每个参数由一个类型后跟一个标识符组成,参数之间用逗号分隔。每个参数看起来都非常像一个常规变量声明(例如:int x),实际上在函数内充当一个局部于该函数的常规变量。参数的目的是允许从调用它的位置向函数传递实参。
- statements 是函数的体。它是一个用大括号 { } 包围的语句块,指定函数实际执行的操作。

让我们来看一个例子

// function example
#include <iostream>
using namespace std;

int addition (int a, int b)
{
  int r;
  r=a+b;
  return r;
}

int main ()
{
  int z;
  z = addition (5,3);
  cout << "The result is " << z;
}
The result is 8

此程序分为两个函数:additionmain。请记住,无论它们的定义顺序如何,C++程序总是从调用 main 开始。实际上,main 是唯一自动调用的函数,任何其他函数的代码只有在其函数被直接或间接从 main 调用时才会执行。

在上面的例子中,main 以声明类型为 int 的变量 z 开始,紧接着执行第一个函数调用:它调用 addition。函数调用遵循与函数声明非常相似的结构。在上面的例子中,addition 的调用可以与其几行之前的定义进行比较。


函数声明中的参数与函数调用中传递的实参有明确的对应关系。调用将两个值 53 传递给函数;这些对应于为函数 addition 声明的参数 ab

当函数在 main 中被调用时,控制权被传递给函数 addition:这里,main 的执行将暂停,直到 addition 函数结束才会恢复。在函数调用时,两个实参(53)的值被复制到函数内的局部变量 int aint b 中。

然后,在 addition 内部,声明了另一个局部变量(int r),并通过表达式 r=a+b,将 ab 的结果赋给 r;在本例中,当 a 为 5 且 b 为 3 时,将 8 赋给了 r

函数中的最后一条语句

1
return r;

结束函数 addition,并将控制权返回到函数被调用的位置;在本例中是返回到函数 main。在这一精确时刻,程序在 main 中恢复其进程,精确地回到被 addition 调用中断的那个点。但此外,由于 addition 有一个返回类型,因此调用被评估为一个具有值,该值是结束 addition 的 return 语句中指定的值:在这种特定情况下,是局部变量 r 的值,在 return 语句时该值为 8。


因此,对 addition 的调用是一个具有函数返回值值的表达式,在这种情况下,该值 8 被赋给 z。这就像整个函数调用(addition(5,3))被它返回的值(即 8)所替换一样。

然后 main 简单地通过调用打印此值

1
cout << "The result is " << z;

一个函数实际上可以在程序中被多次调用,并且其参数自然不限于字面量。

// function example
#include <iostream>
using namespace std;

int subtraction (int a, int b)
{
  int r;
  r=a-b;
  return r;
}

int main ()
{
  int x=5, y=3, z;
  z = subtraction (7,2);
  cout << "The first result is " << z << '\n';
  cout << "The second result is " << subtraction (7,2) << '\n';
  cout << "The third result is " << subtraction (x,y) << '\n';
  z= 4 + subtraction (x,y);
  cout << "The fourth result is " << z << '\n';
}
The first result is 5
The second result is 5
The third result is 2
The fourth result is 6

与前一个示例中的 addition 函数类似,此示例定义了一个 subtract 函数,该函数仅返回其两个参数的差。这次,main 多次调用此函数,演示了更多可能的函数调用方式。

让我们检查每一次调用,同时记住每次函数调用本身就是一个被评估为其返回值的表达式。同样,您可以将其视为函数调用被返回的值所替换。

1
2
z = subtraction (7,2);
cout << "The first result is " << z;

如果我们用它返回的值(即 5)替换函数调用,我们会得到

1
2
z = 5;
cout << "The first result is " << z;

通过相同的过程,我们可以解释
1
cout << "The second result is " << subtraction (7,2);


1
cout << "The second result is " << 5;

因为 5 是 subtraction (7,2) 返回的值。

在...的情况下

1
cout << "The third result is " << subtraction (x,y);

传递给 subtraction 的实参是变量而不是字面量。这也是有效的,并且工作正常。函数调用时,函数使用 xy 的值,分别作为 5 和 3,返回 2 作为结果。

第四次调用又类似了

1
z = 4 + subtraction (x,y);

唯一增加的是,现在函数调用也是加法运算的操作数。同样,结果与函数调用被其结果替换时相同:6。请注意,由于加法的交换律,上面也可以写成

1
z = subtraction (x,y) + 4;

结果完全相同。还请注意,分号不一定出现在函数调用之后,而是像往常一样,出现在整个语句的末尾。同样,可以通过用其返回的值替换函数调用来轻松看出其背后的逻辑。

1
2
z = 4 + 2;    // same as z = 4 + subtraction (x,y);
z = 2 + 4;    // same as z = subtraction (x,y) + 4; 

没有返回类型的函数。void 的使用

上面展示的函数语法

type name ( argument1, argument2 ...) { statements }

要求声明以类型开头。这是函数返回值的类型。但是如果函数不需要返回值怎么办?在这种情况下,要使用的类型是 void,它是一个特殊的类型,表示没有值。例如,一个仅仅打印消息的函数可能不需要返回值。

// void function example
#include <iostream>
using namespace std;

void printmessage ()
{
  cout << "I'm a function!";
}

int main ()
{
  printmessage ();
}
I'm a function!

void 也可以用于函数参数列表,以明确指定函数在被调用时实际上不接受任何参数。例如,printmessage 可以声明为

1
2
3
4
void printmessage (void)
{
  cout << "I'm a function!";
}

在 C++ 中,可以使用空的参数列表代替 void,其含义相同,但 void 在参数列表中的使用是由 C 语言普及的,在那里这是必需的。

在任何情况下都不可选的是函数名后面的括号,无论是在声明还是在调用它时。即使函数不接受参数,也必须始终在函数名后附加至少一对空括号。看看之前例子中 printmessage 是如何被调用的。

1
printmessage ();

括号是将函数与其他声明或语句区分开来的东西。以下不会调用函数

1
printmessage;

main 的返回值

您可能已经注意到 main 的返回类型是 int,但本章及之前章节中的大多数示例实际上都没有从 main 返回任何值。

好了,这里有一个诀窍:如果 main 的执行正常结束而没有遇到 return 语句,编译器会假定函数以隐式 return 语句结束。

1
return 0;

请注意,出于历史原因,这仅适用于函数 main。所有其他具有返回类型的函数都应以正确的 return 语句结束,该语句包含一个返回值,即使该返回值从未使用过。

main 返回零(隐式或显式)时,环境将其解释为程序成功结束。main 可以返回其他值,并且某些环境以某种方式允许调用者访问该值,尽管这种行为不是必需的,也并非在不同平台之间可移植。main 的值保证在所有平台上以相同方式解释为:

描述
0程序成功执行
EXIT_SUCCESS程序成功执行(与上面相同)。
此值在头文件 <cstdlib> 中定义。
EXIT_FAILURE程序失败。
此值在头文件 <cstdlib> 中定义。

由于 main 的隐式 return 0; 语句是一个棘手的例外,一些作者认为显式编写该语句是一种好习惯。

按值和按引用传递参数

在前面的函数中,参数始终是按值传递的。这意味着,在调用函数时,传递给函数的是调用时这些参数的值,这些值被复制到函数参数所表示的变量中。例如,考虑

1
2
int x=5, y=3, z;
z = addition ( x, y );

在这种情况下,函数 addition 传递了 5 和 3,它们分别是 xy 值的副本。这些值(5 和 3)用于初始化函数定义中设置的参数变量,但这些变量在函数内的任何修改都不会影响函数外部的变量 xy 的值,因为在调用时 xy 本身并未传递给函数,只传递了它们当时值的副本。


然而,在某些情况下,从函数内部访问外部变量可能会很有用。为此,可以按引用而不是按值传递参数。例如,此代码中的 duplicate 函数会复制其三个参数的值,从而导致用作参数的变量实际上被调用修改。

// passing parameters by reference
#include <iostream>
using namespace std;

void duplicate (int& a, int& b, int& c)
{
  a*=2;
  b*=2;
  c*=2;
}

int main ()
{
  int x=1, y=3, z=7;
  duplicate (x, y, z);
  cout << "x=" << x << ", y=" << y << ", z=" << z;
  return 0;
}
x=2, y=6, z=14

为了访问其参数,函数将其参数声明为引用。在 C++ 中,引用通过参数类型后的 ampersand (&) 来指示,就像上面示例中 duplicate 所接受的参数一样。

当一个变量按引用传递时,传递的不再是副本,而是变量本身,函数参数所标识的变量与传递给函数的参数 somehow 关联起来,并且函数内对其相应局部变量的任何修改都会反映在调用中作为参数传递的变量上。



实际上,abc 成为函数调用(xyz)的实参的别名,并且 a 在函数内的任何更改实际上都会修改函数外的变量 x。任何对 b 的更改都会修改 y,对 c 的更改都会修改 z。这就是为什么在示例中,当函数 duplicate 修改变量 abc 的值时,xyz 的值会受到影响。

如果不是将 duplicate 定义为

1
void duplicate (int& a, int& b, int& c)

而是将其定义为没有 ampersand 符号,如

1
void duplicate (int a, int b, int c)

变量将不会按引用传递,而是按值传递,而是创建其值的副本。在这种情况下,程序的输出将是 xyz 的值,但未修改(即 1、3 和 7)。

效率考量和 const 引用

按值传递参数调用函数会复制这些值。对于 int 等基本类型,这是一个相对便宜的操作,但如果参数是大型复合类型,可能会导致一定的开销。例如,考虑以下函数

1
2
3
4
string concatenate (string a, string b)
{
  return a+b;
}

此函数将两个字符串作为参数(按值)传递,并返回连接它们的连接结果。通过按值传递实参,函数强制 ab 成为调用函数时传递给函数的实参的副本。如果这些是长字符串,这可能意味着仅为了函数调用就需要复制大量数据。

但是,如果两个参数都按引用传递,则可以完全避免此复制。

1
2
3
4
string concatenate (string& a, string& b)
{
  return a+b;
}

按引用传递的参数不需要复制。函数直接操作(别名)传递的字符串,最多可能意味着将某些指针传递给函数。在这方面,采用引用的 concatenate 版本比采用值的版本更有效,因为它不需要复制昂贵的字符串。

另一方面,带有引用参数的函数通常被认为是修改传递实参的函数,因为这就是引用参数的实际用途。

解决方案是让函数保证其引用参数不会被该函数修改。这可以通过将参数限定为常量来完成。

1
2
3
4
string concatenate (const string& a, const string& b)
{
  return a+b;
}

通过将它们限定为 const,函数被禁止修改 ab 的值,但可以作为引用(实参的别名)访问它们的值,而无需实际复制字符串。

因此,const 引用提供了与按值传递参数类似的功能,但对于大型类型参数,效率更高。这就是为什么它们在 C++ 中对复合类型的参数非常受欢迎的原因。但请注意,对于大多数基本类型,效率上没有明显的差异,在某些情况下,const 引用可能效率更低!

内联函数

调用函数通常会产生一定的开销(堆叠参数、跳转等),因此对于非常短的函数,将函数的代码插入到调用它的地方可能比执行正式调用函数的过程更有效。

在函数声明前加上 inline 说明符会通知编译器,对于特定函数,首选内联展开而不是常规函数调用机制。这根本不会改变函数行为,只是用于建议编译器将函数体生成的代码插入到函数被调用的每个点,而不是通过常规函数调用来调用。

例如,上面的 concatenate 函数可以声明为内联,如下所示:

1
2
3
4
inline string concatenate (const string& a, const string& b)
{
  return a+b;
}

这会通知编译器,在调用 concatenate 时,程序更倾向于内联展开该函数,而不是执行常规调用。inline 只在函数声明中指定,在调用时不需要。

请注意,大多数编译器即使没有显式标记 inline 说明符,也会优化代码以生成内联函数。因此,此说明符仅表示编译器首选此函数的内联,尽管编译器可以自由地不内联它,并进行其他优化。在 C++ 中,优化是编译器负责的任务,只要生成的结果行为与代码指定的行为一致,编译器就可以自由生成任何代码。

参数的默认值

在 C++ 中,函数也可以有可选参数,这些参数在调用时不需要实参,例如,一个有三个参数的函数可以只用两个参数来调用。为此,函数必须为其最后一个参数包含一个默认值,该默认值在调用参数较少的函数时会被函数使用。例如:

// default values in functions
#include <iostream>
using namespace std;

int divide (int a, int b=2)
{
  int r;
  r=a/b;
  return (r);
}

int main ()
{
  cout << divide (12) << '\n';
  cout << divide (20,4) << '\n';
  return 0;
}
6
5

在此示例中,有两个对函数 divide 的调用。在第一个调用中:

1
divide (12)

调用只向函数传递了一个参数,尽管函数有两个参数。在这种情况下,函数假定第二个参数为 2(注意函数定义,它声明其第二个参数为 int b=2)。因此,结果是 6。

在第二次调用中:

1
divide (20,4)

调用向函数传递了两个参数。因此,将忽略 b 的默认值(int b=2),b 将采用作为实参传递的值,即 4,从而产生结果 5。

声明函数

在 C++ 中,标识符只能在声明后才能在表达式中使用。例如,变量 x 在声明之前不能被使用,例如:

1
int x;

函数也是如此。函数不能在声明之前被调用。这就是为什么在前面所有的函数示例中,函数总是定义在 main 函数之前,而 main 函数是从那里调用其他函数的。如果 main 定义在其他函数之前,这将违反函数必须在使用前声明的规则,因此将无法编译。

函数原型可以在不完全定义函数的情况下声明,只需提供足够的信息即可了解函数调用中涉及的类型。当然,函数必须在其他地方定义,比如代码的后面。但至少,一旦这样声明,就可以调用它了。

声明应包含所有涉及的类型(返回类型及其参数的类型),使用与函数定义中相同的语法,但将函数体(语句块)替换为一个结束分号。

参数列表不需要包含参数名称,只需要它们的类型。尽管如此,仍然可以指定参数名称,但它们是可选的,并且不一定需要与函数定义中的名称匹配。例如,一个名为 protofunction 且有两个 int 参数的函数可以用以下任一语句声明:

1
2
int protofunction (int first, int second);
int protofunction (int, int);

无论如何,为每个参数包含名称总能提高声明的可读性。

// declaring functions prototypes
#include <iostream>
using namespace std;

void odd (int x);
void even (int x);

int main()
{
  int i;
  do {
    cout << "Please, enter number (0 to exit): ";
    cin >> i;
    odd (i);
  } while (i!=0);
  return 0;
}

void odd (int x)
{
  if ((x%2)!=0) cout << "It is odd.\n";
  else even (x);
}

void even (int x)
{
  if ((x%2)==0) cout << "It is even.\n";
  else odd (x);
}
Please, enter number (0 to exit): 9
It is odd.
Please, enter number (0 to exit): 6
It is even.
Please, enter number (0 to exit): 1030
It is even.
Please, enter number (0 to exit): 0
It is even.

这个例子确实不是关于效率的。你可能可以自己写一个代码行数少一半的该程序的版本。无论如何,这个例子说明了函数是如何在定义之前声明的。

以下行

1
2
void odd (int a);
void even (int a);

声明了函数的原型。它们已经包含了调用它们所需的所有信息:它们的名称、参数类型以及返回类型(在本例中为 void)。通过这些原型声明,可以在它们完全定义之前调用它们,例如,允许将调用它们的函数(main)放在这些函数实际定义之前。

但是,在定义之前声明函数不仅对于重新组织代码中的函数顺序很有用。在某些情况下,例如在本例中,至少有一个声明是必需的,因为 oddeven 是相互调用的;在 odd 中有一个对 even 的调用,在 even 中有一个对 odd 的调用。因此,没有办法组织代码使得 oddeven 之前定义,并且 evenodd 之前定义。

递归

递归是函数能够自我调用的属性。它对于某些任务很有用,例如排序元素或计算数字的阶乘。例如,为了获得一个数字的阶乘(n!),数学公式是:

n! = n * (n-1) * (n-2) * (n-3) ... * 1
更具体地说,5!(5 的阶乘)将是:

5! = 5 * 4 * 3 * 2 * 1 = 120
一个递归计算 C++ 中这个值的函数可以是:

// factorial calculator
#include <iostream>
using namespace std;

long factorial (long a)
{
  if (a > 1)
   return (a * factorial (a-1));
  else
   return 1;
}

int main ()
{
  long number = 9;
  cout << number << "! = " << factorial (number);
  return 0;
}
9! = 362880

注意,在 factorial 函数中,我包含了一个对自身的调用,但这仅在传递的参数大于 1 时才发生,否则,函数将执行无限递归循环,一旦到达 0,它将继续乘以所有负数(可能在运行时某个时刻导致堆栈溢出)。
Index
目录