C++ Tutorials: C++ Language: Program structure: Functions

C++官网参考链接:Functions - C++ Tutorials (cplusplus.com)

功能
函数允许在代码段中构建程序来执行单独的任务。
在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;
}

输出:

本程序分为addition和main两大函数。记住,无论它们的定义顺序如何,C++程序总是从调用main函数开始。实际上,main是唯一自动调用的函数,而其他函数中的代码只有在其函数从main(直接或间接)调用时才会执行。
在上面的例子中,main首先声明int类型的变量z,然后执行第一个函数调用:它调用addition。函数的调用遵循与函数声明非常相似的结构。在上面的例子中,对addition的调用可以与前面几行它的定义进行比较:

函数声明中的形参与函数调用中传递的实参有明确的对应关系。该调用将两个值5和3传递给函数;这些对应于为函数addition声明的形参a和b。
在main内部调用该函数时,控制将被传递给函数addition:这里,main的执行将停止,只有在addition函数结束时才会恢复。在调用函数时,两个参数(5和3)的值都被复制到函数内的局部变量整数a和整数b中。
然后,在addition中声明另一个局部变量int r;,并通过表达式r=a+b,将a+b的结果赋给r;在这种情况下,a = 5和b = 3,意味着8被分配给r。
函数中的最后一个语句:
return r;
结束函数addition,并将控制返回到调用函数的位置;在这种情况下:返回到函数main。在这个精确的时刻,程序恢复它在main()上的进程,返回到它被addition调用中断的同一点。另外,因为addition有一个返回类型,所以调用被计算为有一个值,这个值是在结束addition的返回语句中指定的值:在这种特殊情况下,局部变量r的值,在返回语句的时刻,它的值是8。

因此,对addition的调用是一个带有函数返回值的表达式,在这种情况下,该值8被赋给z。就好像整个函数调用(addition(5,3))被它返回的值(即8)取代了。
然后main简单地打印该值通过调用:
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';
}

输出:

与上一个示例中的addition函数类似,这个示例定义了一个subtraction函数,它只是返回两个形参之间的差值。这一次,main多次调用这个函数,演示了调用函数的更多可能方式。
让我们检查一下这些调用,记住每个函数调用本身都是一个表达式,它的值是它返回的值。同样,你可以把它想象成函数调用本身被返回值所取代:
z = subtraction(7,2);
cout << "The first result is " << z;
如果我们用函数返回的值替换函数调用(例如,5),我们将得到:
z = 5;
cout << "The first result is " << z;
用同样的程序,我们可以解释为:
cout << "The second result is " << subtraction(7,2);
作为:
cout << "The second result is " << 5;
因为5是通过subtraction(7,2)返回的值。
在以下情况下:
cout << "The third result is " << subtraction (x,y);
传递给subtraction的实参是变量而不是字面量。这也是有效的,并且工作得很好。函数调用时,x和y在调用时的值分别为:5和3,结果返回2。
第四次调用也是类似的: 
z = 4 + subtraction (x,y);
唯一的加法是,现在函数调用也是加法操作的操作数。同样,结果与函数调用被其结果所取代的结果相同:6。注意,由于加法的交换性质,上面也可以写成:
z = subtraction (x,y) + 4;
结果完全一样。还要注意,分号不一定在函数调用之后,而是像往常一样放在整个语句的结束。同样,通过将函数调用替换为其返回值,可以很容易地看到背后的逻辑:
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}
要求声明以type开头。这是函数返回值的类型。但是如果函数不需要返回值呢?在这种情况下,要使用的类型是void,这是一种表示值缺失的特殊类型。例如,一个简单输出消息的函数可能不需要返回任何值:

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

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

int main ()
{
  printmessage ();
}

输出:

还可以在函数的形参列表中使用void来显式指定函数在调用时不接受实际实参。例如,printmessage可以声明为:
void printmessage (void)
{
  cout << "I'm a function!";
}
在C++中,可以使用空形参列表来代替具有相同含义的void,但在形参列表中使用void是由C语言普及的,在C语言中这是一个要求。
在任何情况下,函数名后面的括号都不是可选的,无论是在函数声明中还是在调用函数时。即使函数没有形参,至少也要在函数名后面加一对空的圆括号。看看printmessage是如何在前面的例子中被调用的:
printmessage ();
括号是函数与其他声明类型或语句的区别。下面的代码不会调用该函数:
printmessage;

main函数的返回值
你可能已经注意到main的返回类型是int,但是本章和前面章节中的大多数例子实际上并没有从main返回任何值。
好吧,这里有一个把手:如果main函数的执行正常结束,而没有遇到return语句,编译器就假定函数以隐式return语句结束:
return 0;

注意,由于历史原因,这只适用于main函数。所有其他具有返回类型的函数都应该以包含返回值的正确的返回语句结束,即使这个值从未使用过。
当main返回0(隐式或显式)时,环境将其解释为程序成功结束。其他值可能由main返回,而且有些环境以某种方式将访问该值的权限授予调用者,尽管这种行为不是必需的,也不一定可以在不同平台之间移植。保证在所有平台上以相同的方式解释main的值是:

value(值)description(描述)
0The program was successful(程序执行成功)
EXIT_SUCCESS

The program was successful (same as above).
This value is defined in header <cstdlib>.

(程序执行成功(与上面相同)。

该值在头文件<cstdlib>中定义。)

EXIT_FAILURE

The program failed.
This value is defined in header <cstdlib>.

(程序执行失败。

该值在头文件<cstdlib>中定义。)

因为隐式返回0;对于main中的语句是一个棘手的例外,一些作者认为显式地编写该语句是一个很好的实践。 

实参通过值和引用传递
在前面看到的函数中,实参总是通过值传递的。这意味着,在调用函数时,传递给函数的是这些实参在调用时的值,这些值被复制到函数形参表示的变量中。例如,需要:
int x=5, y=3, z;
z = addition ( x, y );
在本例中,函数addition传递了5和3,它们分别是x和y值的副本。这些值(5和3)用于初始化函数定义中设置为形参的变量,但在函数内部对这些变量的任何修改都不会影响函数外面的变量x和y的值,因为x和y在调用时本身没有传递给函数,而只是它们当时它们值的副本。 

不过,在某些情况下,从函数内部访问外部变量可能会很有用。为此,实参可以通过引用传递,而不是通过值传递。例如,这段代码中的函数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;

输出:

为了获得对实参的访问权,该函数将其形参声明为引用。在C++中,引用在形参类型之后用一个与号(&)表示,如上例中duplicate所取的形参。
当变量通过引用传递时,传递的不再是副本,而是变量本身,函数形参标识的变量,以某种方式与传递给函数的实参相关联,函数中相应局部变量的任何修改都反映在调用中作为实参传递的变量中。

事实上,a、b和c成为了传递给函数调用(x、y和z)的实参的别名,函数内对a的任何更改实际上都是在函数外修改变量x对b的任何更改都会修改y,对c的任何更改都会修改z。这就是为什么在本例中,当函数duplicate修改变量a、b和c的值时,x、y和z的值会受到影响。
如果不定义duplicate为:
void duplicate (int& a, int& b, int& c) 
是否将其定义为没有&号(&):
void duplicate (int a, int b, int c)
变量不是通过引用传递的,而是通过值传递的,而是创建它们值的副本。在这种情况下,程序的输出将是未修改的x、y和z的值(即1、3和7)。

效率考虑和const引用
调用以值传递的形参的函数将导致生成值的副本。对于像int这样的基本类型,这是一个相对便宜的操作,但是如果形参是大型复合类型,它可能会导致一定的开销。例如,考虑以下函数:
string concatenate (string a, string b)
{
  return a+b;

这个函数接受两个string作为形参(按值),并返回连接它们的结果。通过按值传递实参,函数强制a和b成为调用函数时传递给函数的参数的副本。如果这些字符串很长,则可能意味着仅为函数调用复制大量数据。
但如果同时引用两个形参,则可以完全避免这种复制:
string concatenate (string& a, string& b)
{
  return a+b;

通过引用的实参不需要副本。函数直接对作为实参传递的字符串(别名)进行操作,最多它可能意味着将某些指针传递给函数。在这方面,concatenate接受引用的版本比接受值的版本更有效,因为它不需要复制代价昂贵的字符串。
另一方面,带有引用形参的函数通常被认为是修改所传递实参的函数,因为这就是引用形参的实际用途。
解决方案是让函数保证它的引用形参不会被这个函数修改。这可以通过将形参限定为const来实现: 
string concatenate (const string& a, const string& b)
{
  return a+b;
}
通过将它们限定为const,该函数既不能修改a的值,也不能修改b的值,但可以实际访问它们作为引用的值(实参的别名),而不必实际复制字符串。
因此,const引用提供了类似于按值传递实参的功能,但对于大类型的形参具有更高的效率。这就是为什么用于复合类型的实参在C++中非常流行。但是请注意,对于大多数基本类型,在效率上没有明显的区别,在某些情况下,const引用甚至可能更低效! 

inline函数
调用函数通常会造成一定的开销(堆叠实参、跳转等),因此对于非常短的函数,可能更有效的方法是将函数的代码插入调用它的地方,而不是执行正式调用函数的过程。
在函数声明前使用inline说明符通知编译器,对于特定函数,内联展开优于通常的函数调用机制。这完全不会改变函数的行为,而只是用来建议编译器在每次调用函数时插入函数体生成的代码,而不是通过普通的函数调用来调用。
例如,上面的concatate函数可以内联声明为:
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;
}

在本例中,有两个对divide函数的调用。在第一个例子中
divide(12)
调用只向函数传递一个实参,尽管函数有两个形参。在本例中,函数假设第二个形参为2(注意函数定义,它将第二个形参声明为int b=2)。因此,结果是6。
在第二次调用中: 
divide(20,4)
调用将两个实参传递给函数。因此,b的默认值(int b=2)被忽略,b将获取传递作为实参的值,即4,产生5的结果。

声明函数
在C++中,标识符只有在声明之后才能在表达式中使用。例如,某些变量x在用语句声明之前不能使用,例如: 
int x;
这同样适用于函数。函数在声明之前不能被调用。这就是为什么在前面所有的函数示例中,函数总是在main函数之前定义的原因,main函数是调用其他函数的函数。如果在其他函数之前定义main,这将违反函数在使用之前必须声明的规则,因此将无法编译。
可以在不完全定义函数的情况下声明函数的原型,只提供足够的细节,以便知道函数调用中涉及的类型。当然,函数应该在其他地方定义,比如在后面的代码中。但至少,一旦这样声明,它就已经可以被调用了。
声明应该包括所有涉及的类型(返回类型及其实参的类型),使用与函数定义中使用的相同的语法,但用结束的分号替换函数体(语句块)。
形参列表不需要包含形参名称,只需要包含它们的类型。尽管如此,仍然可以指定形参名,但它们是可选的,并不一定需要与函数定义中的形参名匹配。例如,一个带有两个int形参的函数protofunction可以用以下任何一条语句声明:
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);
}

输出:

这个例子确实不是效率的例子。您可以用一半的代码自行编写这个程序的一个版本。不管怎样,这个例子演示了如何在函数定义之前声明函数: 
以下行: 
void odd(int a);
void even(int a);
声明函数的原型。它们已经包含了调用它们所需的所有内容,它们的名称、形参的类型以及它们的返回类型(在本例中为void)。有了这些原型声明,就可以在完全定义它们之前调用它们,例如,允许将调用它们的地方(main)的函数放在这些函数的实际定义之前。
但是在定义函数之前声明函数不仅有助于重新组织代码中函数的顺序。在某些情况下,比如本例,至少需要其中一个声明,因为odd和even是相互调用的;odd中有even,even中有odd。因此,没有办法构造代码,使odd定义在even之前,even定义在odd之前。
递归 
递归是函数必须自我调用的属性。它对某些任务很有用,例如对元素排序或计算数字的阶乘。例如,为了得到一个数字(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;
}

输出:

注意,在函数factorial中,我们包含了对自身的调用,但只有当传递的实参大于1时,因为,否则,函数将执行无限递归循环,在这个循环中,一旦它到达0,它将继续乘以所有的负数(可能会在运行时的某个时刻引发堆栈溢出)。(个人测试所得的结果证明:当实参小于1时,得到的结果为1,不会导致函数执行无限递归循环;当实参大于1时,也会得到正确的结果。)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

weixin_40186813

你的能量无可限量。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值