函数返回值

考虑以下程序:

#include <iostream>

int main()
{
    // 从用户那里获取一个值
    std::cout << "Enter an integer: ";
    int num{};
    std::cin >> num;

    // 打印该值的两倍
    std::cout << num << " doubled is: " << num * 2 << '\n';

    return 0;
}

这个程序由两个概念部分组成:首先,我们从用户那里获取一个值。然后我们告诉用户该值的两倍是多少。 尽管这个程序足够简单,我们不需要将其分解为多个函数,但如果我们想要这样做呢?从用户那里获取一个整数值是一个我们希望程序执行的明确任务,因此它将是一个适合作为函数的好候选。

函数返回值的必要性

所以让我们编写一个程序来实现这一点:

// 此程序无法工作
#include <iostream>

void getValueFromUser()
{
    std::cout << "Enter an integer: ";
    int input{};
    std::cin >> input;  
}

int main()
{
    getValueFromUser(); // 向用户请求输入

    int num{}; // 我们如何从getValueFromUser()获取值并用它来初始化这个变量?

    std::cout << num << " doubled is: " << num * 2 << '\n';

    return 0;
}

虽然这个程序是一个很好的尝试,但它并不完全有效。 当调用函数getValueFromUser时,用户被要求输入一个整数,正如预期的那样。但是当getValueFromUser终止并且控制返回到main时,他们输入的值就丢失了。变量num从未用用户输入的值进行初始化,因此程序总是打印答案0。

我们缺少的是一种方法,让getValueFromUser将用户输入的值返回给main,以便main可以利用这些数据。

返回值的基本概念

当您编写一个用户定义的函数时,您可以决定您的函数是否将返回值返回给调用者。要返回值给调用者,需要两件事。 首先,您的函数必须指明将返回给调用者的值的类型。这是通过设置函数的返回类型来完成的,即在函数名称之前定义的类型。在上面的例子中,函数getValueFromUser的返回类型是void(意味着不会返回任何值给调用者),函数main的返回类型是int(意味着将返回一个int类型的值给调用者)。请注意,这并不决定返回什么具体值——它只决定返回什么类型的值。

其次,在将返回值的函数内部,我们使用return语句来指明被返回给调用者的具体值。return语句由return关键字组成,后跟一个表达式(有时称为返回表达式),以分号结束。

return语句的执行过程

当执行return语句时:

  1. 返回表达式被求值以产生一个值。

  2. 由返回表达式产生的值被复制回调用者。这个副本被称为函数的返回值。

  3. 函数退出,控制返回到调用者。

返回复制值回调用者的过程被称为按值返回。

术语解释

返回表达式产生要返回的值。返回值是该值的一个副本。

每次调用返回值的函数时,它都会返回一个值。

返回值函数示例

让我们来看一个简单的返回整数值的函数,以及一个调用它的示例程序:

#include <iostream>

// int是返回类型
// int类型的返回值意味着函数将返回一些整数值给调用者(这里没有指定具体值)
int returnFive()
{
    // return语句提供了将被返回的值
    return 5; // 将值5返回给调用者
}

int main()
{
    std::cout << returnFive() << '\n'; // 打印5
    std::cout << returnFive() + 2 << '\n'; // 打印7

    returnFive(); // 可以:值5被返回,但被main()忽略,因为它没有对它做任何事情

    return 0;
}

当运行这个程序时,它打印:

5
7

函数返回值的使用

执行从main的顶部开始。在第一条语句中,对returnFive()的函数调用被求值,这导致调用函数returnFive()。返回表达式5被求值以产生值5,该值被返回给调用者并通过std::cout打印到控制台。

在第二次函数调用中,对returnFive的函数调用被求值,这导致再次调用函数returnFive。函数returnFive将值5返回给调用者。表达式5 + 2被求值以产生结果7,然后通过std::cout打印到控制台。

在第三条语句中,函数returnFive再次被调用,导致值5被返回给调用者。然而,函数main没有对返回值做任何事情,所以没有进一步的事情发生(返回值被忽略)。

注意:除非调用者通过std::cout将返回值发送到控制台,否则返回值不会被打印。在上面的最后一个案例中,返回值没有发送到std::cout,所以什么也没有打印。

返回值的使用建议

提示 当被调用的函数返回一个值时,调用者可以决定在表达式或语句中使用该值(例如,用它来初始化一个变量,或将其发送到std::cout)或忽略它(什么也不做)。如果调用者忽略了返回值,它将被丢弃(对它什么也不做)。

修复示例程序

考虑到这一点,我们可以修复我们在课程顶部提出的程序:

#include <iostream>

int getValueFromUser() // 此函数现在返回一个整数值
{
    std::cout << "Enter an integer: ";
    int input{};
    std::cin >> input;  

    return input; // 将用户输入的值返回给调用者
}

int main()
{
    int num { getValueFromUser() }; // 用getValueFromUser()的返回值初始化num

    std::cout << num << " doubled is: " << num * 2 << '\n';

    return 0;
}

当这个程序执行时,main中的第一个语句将创建一个名为num的int变量。当程序去初始化num时,它将看到有一个函数调用getValueFromUser(),所以它将去执行那个函数。函数getValueFromUser要求用户输入一个值,然后它将该值返回给调用者(main())。这个返回值被用作变量num的初始化值。然后在main()中,num可以根据需要使用任意次数。

提示 如果您需要多次使用函数调用的返回值,请用返回值初始化一个变量,然后根据需要使用该变量任意次数。

main()函数的工作原理

您现在有了概念工具来理解main()函数实际上是如何工作的。当程序执行时,操作系统对main()进行函数调用。然后执行跳转到main()的顶部。main()中的语句依次执行。最后,main()返回一个整数值(通常是0),您的程序终止。

在C++中,main()有两个特殊要求:

  1. main()必须返回int。

  2. 显式调用main()是不允许的。

void foo()
{
    main(); // 编译错误:不允许显式调用main
}

void main() // 编译错误:main不允许有非int返回类型
{
    foo();
}

关于main()函数的补充说明

关键洞察 C确实允许显式调用main(),所以一些C++编译器出于兼容性原因会允许这样做。

目前,您还应该在代码文件的底部定义您的main()函数,放在其他函数下面,并避免显式调用它。

对于高级读者 一个常见的误解是main总是第一个执行的函数。 全局变量在main执行之前被初始化。如果这样的变量的初始化器调用了一个函数,那么那个函数将在main之前执行。

状态码的使用

您可能想知道为什么我们从main()返回0,以及我们何时可能返回其他东西。 从main()返回的值有时被称为状态码(或不太常见的退出码,或很少的返回码)。状态码用于指示您的程序是否成功。

按照惯例,状态码0意味着程序正常运行(意味着程序执行并按预期行为)。

最佳实践 如果程序正常运行,您的main函数应该返回值0。

非零状态码通常用于表示某种失败(虽然这在大多数操作系统上可以很好地工作,严格来说,它并不保证是可移植的)。

标准状态码定义

对于高级读者 C++标准只定义了3个状态码的含义:0、EXIT_SUCCESS和EXIT_FAILURE。0和EXIT_SUCCESS都意味着程序成功执行。EXIT_FAILURE意味着程序没有成功执行。 EXIT_SUCCESS和EXIT_FAILURE是在头文件中定义的预处理器宏:

#include <cstdlib> // 用于EXIT_SUCCESS和EXIT_FAILURE

int main()
{
    return EXIT_SUCCESS;
}

如果您想最大化可移植性,您应该只使用0或EXIT_SUCCESS来表示成功终止,或使用EXIT_FAILURE来表示不成功的终止。

顺便说一句… 状态码被传回操作系统。操作系统通常会将状态码提供给启动返回状态码的程序的任何程序。这为任何启动另一个程序的程序提供了一个粗略的机制,以确定启动的程序是否成功运行。

返回值函数的错误处理

不返回值的返回值函数将产生未定义行为 返回值的函数被称为返回值函数。如果返回类型不是void,则函数是返回值的。 返回值函数必须返回该类型的值(使用return语句),否则将产生未定义行为。

这里有一个产生未定义行为的函数的例子:

#include <iostream>

int getValueFromUserUB() // 此函数返回一个整数值
{
    std::cout << "Enter an integer: ";
    int input{};
    std::cin >> input;

    // 注意:没有return语句
}

int main()
{
    int num { getValueFromUserUB() }; // 用getValueFromUserUB()的返回值初始化num

    std::cout << num << " doubled is: " << num * 2 << '\n';

    return 0;
}

现代编译器应该生成一个警告,因为getValueFromUserUB被定义为返回int但没有提供return语句。运行这样的程序会产生未定义行为,因为getValueFromUserUB()是一个返回值函数,但没有返回值。

在大多数情况下,编译器会检测到您是否忘记了返回值。然而,在一些复杂的情况下,编译器可能无法正确确定您的函数是否在所有情况下都返回值,所以您不应该依赖于此。

函数返回值的最佳实践

最佳实践 确保您的非void返回类型的函数在所有情况下都返回值。 从返回值函数中不返回值将导致未定义行为。

如果未提供return语句,函数main将隐式返回0 返回值函数必须通过return语句返回值的规则的唯一例外是函数main()。如果未提供return语句,函数main()将隐式返回值0。也就是说,最好的做法是从main显式返回值,既是为了显示您的意图,也是为了与其他函数保持一致(如果不指定返回值,其他函数将表现出未定义行为)。

函数返回值的限制

函数只能返回单个值

返回值函数每次被调用时只能返回单个值给调用者。

请注意,return语句中提供的值不需要是字面量——它可以是任何有效表达式的结果,包括变量甚至调用另一个返回值的函数。在上面的getValueFromUser()示例中,我们返回了一个变量input,它保存了用户输入的数字。 我们将在以后的课程中介绍各种方法来解决函数只能返回单个值的限制。

函数作者可以决定返回值的含义

函数返回的值的含义由函数的作者决定。一些函数使用返回值作为状态码,以指示它们是否成功或失败。其他函数返回计算或选择的值。其他函数不返回任何东西(我们将在下一课中看到这些示例)。

由于这里的可能性多种多样,最好用注释记录您的函数,以指示返回值的含义。例如:

// 函数要求用户输入值
// 返回值是用户从键盘输入的整数
int getValueFromUser()
{
    std::cout << "Enter an integer: ";
    int input{};
    std::cin >> input;  

    return input; // 将用户输入的值返回给调用者
}

重用函数 现在我们可以说明函数重用的一个很好的案例。考虑以下程序:

#include <iostream>

int main()
{
    int x{};
    std::cout << "Enter an integer: ";
    std::cin >> x; 

    int y{};
    std::cout << "Enter an integer: ";
    std::cin >> y; 

    std::cout << x << " + " << y << " = " << x + y << '\n';

    return 0;
}

虽然这个程序有效,但它有点冗余。实际上,这个程序违反了良好编程的一个核心原则:不要重复自己(通常缩写为DRY)。 为什么重复代码是不好的?如果我们想将文本"Enter an integer:“更改为其他内容,我们将不得不在两个位置更新它。如果我们想初始化10个变量而不是2个呢?那将是很多冗余代码(使我们的程序更长,更难理解),并且有很多机会出现打字错误。 让我们更新这个程序,使用我们上面开发的getValueFromUser函数:

#include <iostream>

int getValueFromUser()
{
    std::cout << "Enter an integer: ";
    int input{};
    std::cin >> input;  

    return input;
}

int main()
{
    int x{ getValueFromUser() }; // 第一次调用getValueFromUser
    int y{ getValueFromUser() }; // 第二次调用getValueFromUser

    std::cout << x << " + " << y << " = " << x + y << '\n';

    return 0;
}

这个程序产生以下输出: Enter an integer: 5 Enter an integer: 7 5 + 7 = 12

在这个程序中,我们两次调用getValueFromUser,一次用于初始化变量x,一次用于初始化变量y。这使我们免于复制获取用户输入的代码,并减少了犯错的可能性。一旦我们知道getValueFromUser有效,我们可以随心所欲地多次调用它。 这是模块化编程的本质:能够编写一个函数,测试它,确保它有效,然后知道我们可以随心所欲地重用它,并且它将继续有效(只要我们不修改函数——在这种情况下,我们将不得不重新测试它)。

最佳实践 遵循DRY:“不要重复自己”。如果您需要多次做某事,请考虑如何修改您的代码以尽可能减少冗余。变量可以用来存储需要多次使用的计算结果(这样我们就不必重复计算)。函数可以用来定义我们想要多次执行的一系列语句。循环(我们将在后续章节中介绍)可以用来多次执行一个语句。 像所有最佳实践一样,DRY旨在作为指导方针,而不是绝对规则。读者Yariv指出,当代码被分解成太小的部分时,DRY可能会损害整体理解。

顺便说一句… DRY的(讽刺)对立面是WET(“两次写一切”)。

结论 返回值提供了一种方式,让函数返回单个值给函数的调用者。 函数提供了一种方式,使我们的程序中的冗余最小化。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"