函数(Function)
函数就是我们编写的代码块,被设计用来执行某个特定的任务,当我们之后说到类(class)的时候,那些代码块被称为“方法(method)”,但在这里,是在明确地说某种不属于某个 class 的东西。对我们而言,分割代码以防止重复代码是很常见的。我们不希望多次写同一代码,因为这样做除了复制和粘贴大量的代码,并最终获得一团糟以外,也意味着,如果我们决定改变一些代码,我们就必须在所有粘贴原始代码的地方改变它,这对维护来说将会是一场灾难。所以我们可以做的就是写一个函数来做我们想做的,然后在我们需要的时候我们可以在代码中多次调用,。可以这么理解函数,有一个输入和一个输出(尽管这并非绝对必要的),我们可以提供特定的参数,然后这个函数可以为我们返回一个值。
假设我们想把两个数相乘,我们想要写出一个函数来实现它,我要做的第一件事就是在这里写一个叫返回值(return value)的东西,这是这个函数会返回的类型。因为我们把两个整数相乘、当然将产生一个整数,所以我们返回值是int,我将给这个函数一个名字,在本例中,叫 Multiply,它会接受两个参数,它们是我们想要相乘的数字,我把它们叫做A和B。然后我将给函数一个函数体(body)它所要做的就是返回 a × b 。
int Multiply(int a, int b)
{
return a * b;
}
所以你可以看到我们这里有一个函数,它接受两个参数,都是整数,然后仅仅返回这两个数字的乘积。
其实我们并不一定要提供参数
例如我可以不提供任何参数,然后返回 5 × 8。
int Multiply()
{
return 5 * 8;
}
这仍然是一个返回一个整数的函数,但它只是不取任何参数。
我们还可以告诉函数我们不想让它返回任何东西。
我们通过写一个void来作为它的返回类型。void当然代表没东西,那么我们可以直接把结果打印到控制台。
int Multiply()
{
std::cout << 5 * 8 << std::endl;
}
让我们回到原来的例子:我们有 int a和 int b,我们返回了这两个整数的乘积。
int Multiply(int a, int b)
{
return a * b;
}
那么我们如何调用这个函数呢?调用一个函数很简单,让我们试着打印乘法的结果。首先,我要声明一个变量来存储这个结果,所以我输入int result = Multiply()。
然后我们就用3和2,这样做的结果就是用这两个参数来调用Multiply函数,然后把返回值,也就是 a × b 的结果,存到这个result整型变量。然后我们可以通过控制台输出这个result
让我们按F5来运行我们的程序,当它build好后,我们将会得到6
int main()
{
int result = Multiply(3, 2);
std::cout << result << std::endl;
std::cin.get();
}
这显然是 3×2 的结果。
说的更详细些,假设我想要做一系列乘法,然后把它们都打印到控制台。如果我在没有函数的情况下做类似的事情那么它看起来会很乱。我需要重复这段代码,去复制粘贴几次,我把它们叫做 result2 和 result3。
int main()
{
int result = Multiply(3, 2);
std::cout << result << std::endl;
int result2 = Multiply(8, 5);
std::cout << result << std::endl;
int result3 = Multiply(90, 45);
std::cout << result << std::endl;
std::cin.get();
}
我们做 85,和 90 × 45 ,如果我运行上面这个程序,将得到一样的值。因为当我复制并粘贴这段代码时,我忘记更改输出语句中的变量了。
这种情况其实经常发生,人们复制和粘贴代码块,然后忘记改变一个小细节,在某些情况下,你可能只是运行你的程序,甚至没有注意到不对,直到在之后某个地方造成错误。然而,如果你为它创建一个函数,这种问题就非常容易解决。让我们通过打印result2和result3来解决它:
如果我运行下面的代码:
int main()
{
int result = Multiply(3, 2);
std::cout << result << std::endl;
int result2 = Multiply(8, 5);
std::cout << result2 << std::endl;
int result3 = Multiply(90, 45);
std::cout << result3 << std::endl;
std::cin.get();
}
我们会得到正确的结果。然而你可以看到,我实际上是多次复制了几乎相同的代码。如果我决定直接用数字相乘的写法来代替这个乘法函数,我必须在每个地方替换它:
int result = 3 * 2;
std::cout << result << std::endl;
int result2 = 8 * 5;
std::cout << result2 << std::endl;
int result3 = 90 * 45;
std::cout << result3 << std::endl;
如果不想这么麻烦,我们就可以为它写一个函数。
返回类型是void,因为它并不会返回什么给我们,它只会执行我们要求它做的事情。我们把它命名为 ”MultiplyAndLog“,然后思考我们想要的参数。所以这三段代码之间到底什么变化了?实际发生变换的只是两个相乘的数字,所以它们成为了我们函数的参数。到底这三段代码什么发生了变化,我们需要指定什么才能让这个函数执行我们想要的操作?让我们写下我们的参数,所以我们会用 int a 和b (你可以管它们叫任何东西)我们将把其中一段复制粘贴到这个函数中:
void MultiplyAndLog(int a, int b)
{
int result = a * b;
std::cout << result << std::endl;
}
当然,我将用我们的参数来替换3和2,这样我们就可以使用我们指定的参数来执行该函数里的乘法运算了,这会导致a*b发生,然后打印结果到控制台。
所以现在,我不需要写很多次这些,我只需要简单地调用MultiplyAndLog,并传入参数
所以比如说3和2,然后是8和5,然后是90和45,仅此而已。
int main()
{
MultiplyAndLog(3,2);
MultiplyAndLog(8,5);
MultiplyAndLog(90,45);
std::cin.get();
}
这就是我们最后得到的样子,干净易读的程序。
这是一个很简单的例子,但它很有效地证明了函数是非常重要的,我们的目标应该是把你的代码拆分成许多许多函数。但不要太过分,你不需要给每一行代码都准备一个函数,那样很难维护、你的代码看起来会很混乱很拥挤。实际上那样会让程序运行更慢。我们每次调用函数时,编译器会生成一个调用指令。这意味着,在一个运行的程序中,为了让我们调用一个函数我们需要为这个函数创建一整个 stack frame(栈框架),也就是说,我们得把参数之类的东西 push(推)到栈上。我们还需要一个叫做返回地址的东西,将其压到栈上,然后我们需要跳到我们程序的某个不同部分,以执行我们的函数里的指令。为了将我们 push进去的结果返回, 我们需要返回到最初调用函数之前的地方。所以这整个过程就是在内存中跳转来跳转去而为了执行函数指令。所有这些都需要时间,所以它减慢了我们的程序。
上面所说的减慢原因的原因是,这都是建立在假设编译器决定保留我们的函数作为一个函数而不是将其内联(inline)。我们将在以后深入讨论 inlining
之所以说这么多,是因为不想让你直接为每一行代码创建一个函数,这需要一点经验来意识到你什么时候需要一个函数。但基本上,如果我们正在多次做一个重复的任务,那么就可以为它创建一个函数。
函数的主要目的是防止代码重复,我们不希望只是到处复制和粘贴代码。
现在我们再回到我们的代码。你可能会注意到这个主函数有点奇怪,它说他的返回值是 int,然而却找不到return这个关键字。很明显我没有返回任何东西。所以如果我指定一个返回类型,我真的需要返回一些东西吗。试试看,在这个乘法函数中什么也返回,再按ctrl F7来编译该文件,我们将会得到一个错误,告诉我Multiply必须返回一个值。因此带有返回类型的函数实际上需要返回值吗?答案是肯定的!他们需要!
主函数实际上是一个特殊的函数,主函数且只有主函数可以不用返回一个值,如果你没有指定返回值,它会自动假设你返回0。这只是现代c、c++版本的特点,为了让你的代码更简洁。
有趣的是,所有这些必须返回一个值的规定实际上只适用于调试模式。如果我们在release模式编译,你会发现我们实际上并没有错误,这并不是说我们在这里做的是正确的,因为如果拿那个返回值做其他事,会得到未定义行为,只是编译器并没有对我们提示而已。
然而,在调试模式下,当特定的调试编译标记(flags)被激活时,我们将会得错误,它将帮助我们调试代码,因为在任何时候你都不应该写一个函数,说了它会返回什么,但是并没有返回。
这就是对函数的基本介绍。函数非常有用,每个程序都是由一系列函数组成的
我们还通常将函数拆分为声明和定义。声明通常会存在头文件,而定义则会写在翻译单元里或者说cpp文件里,下一节会单独来讲头文件中的函数声明。