函数
函数是在代码段中执行单个任务的程序。
在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
|
该程序分为两个函数:addition 和 main。请记住,无论它们的定义顺序如何,C++ 程序始终从main函数开始执行.
事实上,main是唯一自动调用的函数,其他任何函数中只有在main(直接或间接)调用时才会执行。
在上面的例子中,main 首先声明 int 类型的变量 z,紧接着它执行第一个函数调用:
调用 addition. 对函数的调用与函数声明非常相似。
在上面的例子中,可以将 addition 调用与它前面几行的定义进行比较:
函数声明中的参数与函数调用中传递的参数有明确的对应关系。函数调用将两个值5和3传递给函数 addition ;
它们对应于函数 addition 声明的参数 a 和 b。
在从 main 中调用 function 函数的那一刻,控制权被传递给 function :main 函数这里,停止执行,
只有在 function 函数结束后才会恢复。
在函数调用的那一刻,两个参数 (5 和 3)的值都被复制到函数局部变量 a 和 b 内。
然后,在 baddition 内部,声明了另一个局部变量 (int r ),
并通过表达式 r = a + b,把 a + b 的结果赋值给 r;在这种情况下,a = 5 b = 3,意味着 8 被赋值给 r。
函数的最后一条语句:
return r;
结束 addition 函数.程序返回到调用函数的位置,并将控制权交还给 main 函数;
在这种情况下:main 继续执行。
但是,因为 addition 有一个返回值,这个值是在结束的addition函数 return 语句中指定的值:
在这种情况下,return 语句返回局部变量 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';
}
|
The first result is 5
The second result is 5
The third result is 2
The fourth result is 6
|
与上一个示例中的加法函数类似,这个示例定义了一个减法函数,它返回两个参数之间的差。
这一次,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);
|
在只有加法操作的时候,函数调用就是加法操作的一个操作数.
同样,如果函数调用被它的返回值代替结果是一样的.
注意,根据加法交换律,上面例子可以这样写:
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 }
要求声明以类型开头。这是函数返回值的类型。
但是如果函数不需要返回值呢?在这种情况下,要使用的类型是一种特殊类型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 可以被声明为:
void printmessage (void)
{
cout << "I'm a function!";
}
|
在c++中,可以使用空形参列表代替void,
但在参数列表中使用 void 是在 C语言中普及的,这是必需的。
在任何情况下,无论是在函数声明中还是在调用函数时。函数名后面的圆括号都不是可选的,
即使函数没有形参,也至少要在函数名后面加上一对空括号。
看看在前面的例子中 printmessage 是如何被调用的:
圆括号是函数区别于其他类型的声明或语句的地方。下面的函数不会被调用:
printmessage;
☞ main 函数的返回值
你可能已经注意到main的返回类型是int,
但是本章和前几章中的大多数例子实际上并没有从main返回任何值。
这里有个问题:如果 main 函数的执行没有遇到 return语句而正常结束,
编译器就会假定函数以隐式 return语句结束:
return 0;
注意,由于历史原因,这只适用于函数main。
所有其他带有返回类型的函数,都应该以带有返回值的正确的 return语句结束,即使它从未使用过。
当 main 函数(隐式或显式)返回 0 时,运行环境会将其解释为程序成功结束。
main 可能返回其他值,一些运行环境以某种方式向调用者提供对该值的访问,
尽管这种行为不是必需的,也不一定可以在平台之间移植。
保证在所有平台上以相同方式解释的 main 的值是:
value |
说明 |
0 |
程序正常结束 |
EXIT_SUCCESS |
程序正常结束.在<cstdlib>中定义 |
EXIT_FAILURE |
程序运行失败.在<cstdlib>中定义 |
因为main语句隐式return 0; 一些程序员认为显式地编写该语句是很好的习惯。
☞ 参数的值传递和引用传递
在前面的函数中,参数总是通过值传递的。这意味着,在调用函数时,传递给被调用函数的是这些参数的值,
这些值被复制到被调用函数形参的变量中。例如,:
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;
}
|
x=2, y=6, z=14
|
为了获得对实参的访问,函数将其形参声明为引用.
在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)
|
变量不会通过引用传递,而是通过值传递,会创建它们的值的副本。
在这种情况下,程序的输出将是x、y、 和z未经修改的值(即 1、3 和 7)。
☞ 效率分析和常引用
值传递的函数调用,对于int等基本类型来说,这是一种相对简便的操作,但如果形参是大型复合类型,
则可能导致某些额外开销。例如,思考下面函数:
string concatenate (string a, string b)
{
return a+b;
}
|
这个函数接受两个字符串作为参数(按值),并返回连接它们的结果。
由于按值传递参数,a和b成为被调用时传递给函数的参数的副本。
如果这些是长字符串,这可能意味着仅为函数调用就要复制大量数据。
但如果使用引用传递,则可以完全避免这种复制:
string concatenate (string& a, string& b)
{
return a+b;
}
|
引用传参不需要副本。函数直接对传递的字符串(别名)进行操作,可能最多传递一些指针。
在这方面,使用引用的版本比使用值的版本更高效,因为它不需要复制字符串。
另一方面,引用传参的函数通常会修改传递的实参的值,因为引用传参使用的是实际参参。
为了保证实参的值不被修改,可以通过将参数限定为常量来实现:
string concatenate (const string& a, const string& b)
{
return a+b;
}
|
通过将它们限定为const,该函数既不能修改a的值,也不能修改b的值.
但可以作为引用(参数的别名)访问它们的值,而不必实际复制字符串。
因此,const引用提供了类似于按值传参的功能,对于大型类型的形参,它的效率更高。
这就是为什么在c++中非常流行用常引用给复合类型传参。
但是请注意,对于大多数基本类型,在效率上没有明显的区别,在某些情况下,常引用的效率甚至更低!
☞ 内联 (Inline )函数
调用一个函数通常会导致一定的开销(参数入栈,跳转等),因此,对于非常短的函数,
简单地在调用函数的地方插入被调用函数的代码,而不是执行正式调用函数的过程,可能会更有效率。
在函数声明之前带有 inline 标志,编译器会知道,对于特定函数,内联扩展优先于通常的函数调用机制。
这不会改变函数的运行,只是用来建议编译器在调用函数时直接插入被调用函数体生成的代码,
而不是用常规函数调用来调用。
例如,上面的concatenate函数可以声明为inline:
inline string concatenate (const string& a, const string& b)
{
return a+b;
}
|
这将通知编译器,当调用concatenate时,程序希望将函数内联展开,而不是执行常规调用。内联只在函数声明中指定,而不是在调用时指定。
请注意,即使没有显式地使用内联说明符标记,大多数编译器在看到有机会提高效率时,已经对代码进行了优化,以生成内联函数。
因此,这个说明符仅仅指示了这个函数首选内联的编译器,尽管编译器可以自由地不内联它,并进行其他优化。
在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有两个调用。在第一次调用中:
调用函数只向被调用函数传递一个参数,尽管函数有两个参数。
在本例中,函数假定第二个形参为2(注意函数定义,它将第二个形参声明为int b=2)。因此,结果是6。
第二次调用:
调用函数只向被调用函数传递两个参数。因此,b (int b=2)的默认值将被忽略,
b接受作为参数传递的值,即4,结果为5.
☞ 函数声明
在c++中,标识符只有在声明后才能在表达式中使用。例如,某个变量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);
}
|
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.
|
这个例子确实不是高效率的例子。您可以用一半的代码行编写自己的这个程序版本。
不管怎样,这个例子演示了如何在函数定义之前声明函数:
在以下几行:
void odd (int a);
void even (int a);
|
声明了函数的原型。它们已经包含了所有必要的名称、参数的类型和返回类型(在本例中为void)。
有了这些函数原型声明,就可以在完全定义它们之前调用它们,
例如,允许在实际定义这些函数之前,将函数从调用它们的地方(main)引用进来。
在定义函数之前声明函数不仅有助于在代码中重新组织函数的顺序。
在某些情况下,比如在上面例子这个特殊情况下,至少需要一个声明,
因为奇数和偶数是相互调用的;一种调用奇数调用偶数,还有一种调用偶数调用奇数。
因此,没有办法把奇数定义在偶数之前,或者偶数定义在奇数之前。
☞ 递归
递归是函数自己调用自己的一种特性.它对于某些任务很有用,比如排序元素或计算数字的阶乘。
例如,为了得到一个数字(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,
它将继续乘以所有负数(可能在运行时的某个点引发栈溢出)。