2-2 函数返回值(返回值函数)

请考虑以下程序:

#include <iostream>

int main()
{
	// get a value from the user
	std::cout << "Enter an integer: ";
	int num{};
	std::cin >> num;

	// print the value doubled
	std::cout << num << " doubled is: " << num * 2 << '\n';

	return 0;
}

image

该程序包含两个概念部分:首先,我们获取用户的输入值;然后告知用户该值对应的双倍数值。

尽管这个程序足够简单,无需拆分为多个函数,但若我们想这样做呢?获取用户输入的整数值是程序需要完成的明确任务,因此非常适合作为函数实现。

现在让我们编写实现此功能的程序:

// This program doesn't work
#include <iostream>

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

int main()
{
	getValueFromUser(); // Ask user for input

	int num{}; // How do we get the value from getValueFromUser() and use it to initialize this variable?

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

	return 0;
}

虽然这个程序是一个不错的解决方案尝试,但它并不完全有效。

当调用 getValueFromUser 函数时,程序会如预期般要求用户输入整数。但当 getValueFromUser 结束并控制权返回主函数时,用户输入的值便会丢失。变量 num 从未被初始化为用户输入的数值,因此程序始终输出答案 0。

我们缺少的关键环节是让 getValueFromUser 函数将用户输入的值传递回主函数,以便主函数能利用该数据。


返回值

编写用户自定义函数时,可自行决定函数是否向调用方返回值。要实现返回值,需满足两个条件:

首先,函数必须声明返回值的类型。这通过设置函数的返回类型return type实现,该类型定义在函数名之前。在上例中,函数 getValueFromUser 的返回类型为 void(表示不向调用方返回值),而 main 函数的返回类型为 int(表示将返回 int 类型的值)。请注意,这仅决定了返回值的类型,并不决定具体返回何种数值。

相关内容
我们将在下一课(2.3 -- 空值函数(不返回值的函数))中进一步探讨返回 void 的函数。

其次,在需要返回值的函数内部,我们使用return语句return statement来指定返回给调用方的具体值。return语句由return关键字开头,后接一个表达式(有时称为返回表达式return expression),末尾以分号semicolon结束。

当return语句被执行时:

  • 返回表达式被求值以产生一个值。
  • 返回表达式产生的值被复制回调用方。该副本称为函数的返回值return value
  • 函数退出,控制权返回给调用方。

将复制值返回给调用方的过程称为按值返回return by value

命名法
返回表达式生成待返回的值。返回值是该值的副本。

返回值函数每次被调用时都会返回一个值。

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

#include <iostream>

// int is the return type
// A return type of int means the function will return some integer value to the caller (the specific value is not specified here)
int returnFive()
{
    // the return statement provides the value that will be returned
    return 5; // return the value 5 back to the caller
}

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

    returnFive(); // okay: the value 5 is returned, but is ignored since main() doesn't do anything with it

    return 0;
}

运行时,该程序输出:

image

执行从 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() // this function now returns an integer value
{
 	std::cout << "Enter an integer: ";
	int input{};
	std::cin >> input;

	return input; // return the value the user entered back to the caller
}

int main()
{
	int num { getValueFromUser() }; // initialize num with the return value of getValueFromUser()

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

	return 0;
}

当该程序执行时,main函数中的第一条语句将创建一个名为num的整型变量。程序在初始化num时,会发现存在对getValueFromUser()函数的调用,因此将转而执行该函数。getValueFromUser函数会提示用户输入数值,随后将该值返回给调用方(main())。该返回值将作为变量num的初始化值。num可在main()内按需多次使用。

提示
若需多次使用函数调用的返回值,请用该返回值初始化变量,随后即可通过该变量进行多次调用。

请自行编译此程序并多次运行,以验证其正确性。


重新审视 main()

现在你已掌握理解 main() 函数实际运作机制的概念工具。程序执行时,操作系统会调用 main() 函数,随后执行流程跳转至 main() 开头。main() 中的语句将按顺序执行,最终 main() 返回一个整数值(通常为 0),程序随之终止。

在C++中,main()函数有两项特殊要求:

  • main()必须返回整型值。
  • 禁止显式调用main()函数。
void foo()
{
    main(); // Compile error: main not allowed to be called explicitly
}

void main() // Compile error: main not allowed to have non-int return type
{
    foo();
}

关键要点:
C语言确实允许显式调用main()函数,因此部分C++编译器出于兼容性考虑会支持此行为。

目前,您仍应将main()函数定义在代码文件底部,置于其他函数之后,并避免显式调用该函数。

对于高级读者

人们常误以为 main 函数总是第一个执行的函数。

全局变量在 main 函数执行之前就会初始化。如果某个全局变量的初始化器调用了函数,那么该函数将在 main 之前执行。我们在第 7.4 课——全局变量简介中讨论全局变量。


状态码

你可能好奇为何main()函数返回0,以及何时会返回其他值。

main()函数的返回值有时被称为状态码status code(较少称为退出码exit code,更罕见地称为返回码return code)。状态码用于指示程序是否执行成功。

按惯例,状态码为0表示程序运行正常(即程序按预期执行并行为)。

最佳实践
当程序正常运行时,main函数应返回值0。

非零状态码通常用于指示某种失败(尽管在多数操作系统上可行,但严格来说无法保证跨平台兼容性)。

对于高级读者

C++标准仅定义了3个状态码的含义:0、EXIT_SUCCESS和EXIT_FAILURE。0与EXIT_SUCCESS均表示程序执行成功,而EXIT_FAILURE表示程序执行失败。

EXIT_SUCCESS和EXIT_FAILURE是定义在头文件中的预处理器宏:

#include <cstdlib> // for EXIT_SUCCESS and EXIT_FAILURE

int main()
{
    return EXIT_SUCCESS;
}

若要最大限度地提高可移植性,应仅使用 0 或 EXIT_SUCCESS 表示成功终止,或使用 EXIT_FAILURE 表示终止失败。

我们在第 2.10 课——预处理器介绍中介绍了预处理器和预处理器宏。

顺带一提……

状态码会被传递回操作系统。操作系统通常会将状态码提供给启动该程序的程序。这为任何启动其他程序的程序提供了一种简易机制,用于判断被启动程序是否运行成功。


不返回值的值返回函数将产生未定义行为

返回值的函数称为值返回函数。若函数的返回类型非 void,则该函数属于值返回函数。

值返回函数必须返回该类型的值(使用 return 语句),否则将导致未定义行为。

相关内容:
我们在第1.6课——未初始化变量与未定义行为中讨论了未定义行为。

以下是一个产生未定义行为的函数示例:

#include <iostream>

int getValueFromUserUB() // this function returns an integer value
{
 	std::cout << "Enter an integer: ";
	int input{};
	std::cin >> input;

	// note: no return statement
}

int main()
{
	int num { getValueFromUserUB() }; // initialize num with the return value of getValueFromUserUB()

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

	return 0;
}

image

现代编译器应对此发出警告,因为 getValueFromUserUB 被定义为返回 int 类型,但未提供 return 语句。运行此类程序将导致未定义行为,因为 getValueFromUserUB() 作为值返回函数却未返回实际值。

大多数情况下,编译器能检测到你是否遗漏了返回值。但在某些复杂场景中,编译器可能无法准确判断函数是否返回值,因此不应依赖此机制。

最佳实践

确保所有返回值非 void 的函数在任何情况下都返回一个值。

值返回函数若未返回值,将导致未定义行为。


若未提供return语句,main函数将隐式返回0

唯一例外是 main() 函数。根据规则,值返回函数必须通过 return 语句返回值,但 main() 函数若未提供 return 语句,将隐式返回值 0。尽管如此,最佳实践仍是从 main() 中显式返回值,这既能体现编程意图,又能与其他函数保持一致(若未指定返回值,其他函数将表现为未定义行为)。


函数只能返回单一值

每次调用时,返回值函数只能向调用方返回单一值。

请注意,return语句中提供的值不必是字面量——它可以是任何有效表达式的结果,包括变量甚至调用其他返回值的函数。在上文getValueFromUser()示例中,我们返回了变量input,该变量存储了用户输入的数字。

针对函数仅能返回单一值的限制,存在多种变通方案,我们将在后续课程中详细探讨。


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

函数返回值的含义由函数作者决定。某些函数将返回值用作状态码,用于指示成功或失败。其他函数则返回计算或选取的值。还有些函数不返回任何值(我们将在下一课中看到这类函数的示例)。

鉴于此处的可能性多种多样,建议通过注释说明函数返回值的含义来完善函数文档。例如:

// Function asks user to enter a value
// Return value is the integer entered by the user from the keyboard
int getValueFromUser()
{
 	std::cout << "Enter an integer: ";
	int input{};
	std::cin >> input;

	return input; // return the value the user entered back to the caller
}

函数复用

现在我们可以说明函数复用的一个良好案例。请看以下程序:

#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;
}

虽然这个程序能运行,但存在冗余问题。事实上,它违反了优秀编程的核心原则之一:不要重复自己Don’t Repeat Yourself(常简称为DRY)。

重复代码为何不可取?若想将文本“请输入整数:”替换为其他内容,我们必须在两个位置进行修改。若需初始化10个变量而非2个呢?这将产生大量冗余代码(使程序冗长且难以理解),并增加大量输入错误的风险。

现在让我们更新程序,改用前面开发的getValueFromUser函数:

#include <iostream>

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

	return input;
}

int main()
{
    int x{ getValueFromUser() }; // first call to getValueFromUser
    int y{ getValueFromUser() }; // second call to getValueFromUser

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

    return 0;
}

该程序产生以下输出:

image

在此程序中,我们两次调用 getValueFromUser 函数:第一次用于初始化变量 x,第二次用于初始化变量 y。这既避免了重复编写获取用户输入的代码,又降低了出错概率。一旦确认 getValueFromUser 函数正常工作,我们便可随心所欲地调用它。

这正是模块化编程的精髓:编写函数、测试验证、确保其正常运行,随后便可放心重复调用——只要不修改函数本身(若修改则需重新测试),它将始终保持有效。

最佳实践

遵循DRY原则:“不要重复自己”。若需执行某操作多次,请考虑如何修改代码以最大限度消除冗余。变量可用于存储需多次使用的计算结果(从而避免重复计算)。函数可用于定义需要多次执行的语句序列。而循环(将在后续章节介绍)则能实现语句的重复执行。

与所有最佳实践相同,DRY原则应视为指导方针而非绝对准则。读者Yariv指出,当代码被拆解为过小单元时,DRY原则反而可能损害整体可读性。
image

顺带一提……
DRY(保持代码简洁)的反义词是WET(“把所有东西都写两遍”),这个说法带着点讽刺意味。


结论

返回值为函数提供了一种将单个值返回给函数调用者的方式。

函数为我们在程序中减少冗余提供了途径。


测验时间

问题 #1

检查(不要编译)以下每个程序。确定程序将输出什么,或者程序是否会引发编译器错误。

假设你已关闭“将警告视为错误”选项。

1a)

#include <iostream>

int return7()
{
    return 7;
}

int return9()
{
    return 9;
}

int main()
{
    std::cout << return7() + return9() << '\n';

    return 0;
}

显示答案

该程序输出数字16。

1b)

#include <iostream>

int return7()
{
    return 7;

    int return9()
    {
        return 9;
    }
}

int main()
{
    std::cout << return7() + return9() << '\n';

    return 0;
}

显示答案

该程序无法编译。不允许使用嵌套函数。

1c)

#include <iostream>

int return7()
{
    return 7;
}

int return9()
{
    return 9;
}

int main()
{
    return7();
    return9();

    return 0;
}

显示答案

该程序能够编译通过,但不会产生任何输出。函数返回的值未被用于任何操作(因此被丢弃)。

1d)

#include <iostream>

int getNumbers()
{
    return 5;
    return 7;
}

int main()
{
    std::cout << getNumbers() << '\n';
    std::cout << getNumbers() << '\n';

    return 0;
}

显示答案

该程序将两次输出数字5(分别位于不同行)。每次调用函数getNumbers()时,返回语句return 5;都会执行,程序将值5返回给调用方。返回语句return 7;从未被执行。

1e)

#include <iostream>

int return 5()
{
    return 5;
}

int main()
{
    std::cout << return 5() << '\n';

    return 0;
}

显示答案:

该程序无法编译,因为函数名称无效。我们在第1.7课——关键词与命名标识符中讨论过命名规则。


问题 #2

“DRY”代表什么,为什么遵循这一原则很有用?

显示答案

DRY代表“不要重复自己”。这是一种编程实践,旨在通过优化代码结构来最大限度地减少冗余。这种方法能使程序更简洁、更少出错且更易于维护。
posted @ 2026-02-09 06:31  游翔  阅读(0)  评论(0)    收藏  举报