函数 (function)#

函数是一种抽象工具, 它可以 给一段代码起一个名字, 通过这个名字, 使用者不需要了解代码具体怎么写的, 就能知道代码的功能并使用代码.

1// 打印欢迎提示, 使用者不需要知道具体怎么打印, 函数名字已经说了是要打印欢迎提示.
2void print_welcome_prompt();
3
4int main() {
5  print_welcome_prompt();  // 调用函数
6}

定义函数时需要指明函数需要的参数和预期的返回值.

1int add(int lhs, int rhs);    // 参数为 (int lhs, int rhs), 返回值为 int
2int generate_random_int();    // 没有参数, 返回值为 int
3void print_value(int value);  // 参数为 (int value), 没有返回值

传参和传返回值可以当作声明变量来理解.

声明变量#
1int lhs    = a;
2int rhs    = b;
3int temp   = lhs + rhs;
4int result = temp;
用声明变量进行类比. result 为返回值, lhsrhs 是参数.#
1(int result) add(int lhs, int rhs) {
2  int temp = lhs + rhs;
3  result = temp;
4}
传参和传返回值#
1int add(int lhs, int rhs) {
2  int temp = lhs + rhs;
3  return temp;
4}

例如, 下面这段代码交换了 ab 的值.

1int& lhs = a;
2int& rhs = b;
3
4int temp = lhs;
5lhs      = rhs;
6rhs      = temp;

"这段代码交换 (swap) 了 ab 的值", 这不就是说, 它可以是一个名叫 swap 的函数吗?

1void swap(int& lhs, int& rhs) {
2  int temp = lhs;
3  lhs      = rhs;
4  rhs      = temp;
5}
6swap(a, b);  // 可以交换 a 和 b
7swap(a, c);  // 可以交换 a 和 c
8swap(b, c);  // 可以交换 b 和 c

函数的命名建议#

表示某种操作 (control) 的函数, 通常命名为谓词性短语#

  • void print_welcome_prompt(): 打印欢迎提示.

  • void swap(int& lhs, int& rhs): 交换两个 int 值.

  • bool is_odd(int value): 判断 value 是否是奇数.

  • int generate_random_int(): 生成随机整数.

  • Widget make_widget(): 创建一个 Widget 对象.

获得某种信息 (view) 函数的, 通常命名为偏正短语#

  • int size().

  • int available_tickets().

函数的调用与返回#

定义函数后, 我们可以对函数进行调用. 所谓函数调用是 我们自己 传入实际参数来"初始化"形式参数, 进入函数内部进行运算, 再 (携带返回值) 返回回来. 换句话说, 函数调用没有其他人帮忙, 是 我们自己 进入函数、进行运算、从函数中返回, 然后我们才执行函数调用后的代码:

 1void function() {
 2  int another = 2;
 3  std::cout << "2";
 4}
 5
 6int main() {
 7  int value = 3;
 8  std::cout << "1";
 9  function();
10  std::cout << "3";
11}
12// 输出 123

请在 main() 函数最开始设置断点, 断点调试 以上代码, 观察程序具体如何执行.

当我们断点调试执行到 function() 函数内时, 我们可以明显观察到调用堆栈发生了变化: 原本的 main() 函数上面多了一层 function(). 所以调用堆栈所表示的并不是函数本身, 而是函数调用, 而最上层即我们正在执行的函数调用.

函数调用是针对本次调用传入实际参数并进行运算, 而这些参数和运算中间结果等都需要存储下来, 这样对函数调用的记录称为栈帧. 栈帧存储在调用堆栈上, 于是构成了我们在调用堆栈时看到的 function().

栈帧除了存储有本次函数调用的参数、局部变量等外, 还存储有一个重要的信息——调用结束后返回到什么位置. 即使是 void function() 也需要返回, void 只是说该函数没有返回值, 但既然是 我们自己 进入函数进行运算, 那就需要在函数调用完成后返回回去继续之后的操作, 因此栈帧还需要记录返回到什么位置.

请断点调试进入 function() 函数中, 然后点击调用堆栈中的 main() 函数, 这将切换为显示目前 main() 函数栈帧的状态 (请观察变量窗口等的变化). 可以发现, 除了 function() 函数内标记有一个执行位置外, main() 函数内也标记有一个位置, 这就是本次对 function() 调用完成后, 将会返回到的位置.

不同的函数调用是创建不同的栈帧, 因此函数调用之间是完全独立的. 即使是递归函数, 也只是在 A 函数内再调用 A 函数, 创建一个新的栈帧罢了.

函数的返回值类型#

int *function() 并不是说 function() 被设为了 *, 而是返回值为 int*.

这确实很容易混淆, 尤其是很多教材把 * 放在右边. 所以按我的个人习惯, 我更喜欢使用 C++11 起的函数返回值后置语法:

  • int f() 写为 auto f() -> int.

  • void f() 保持不变.

因为:

  • 从逻辑上, 返回值就该在函数调用 返回.

  • 这样一来一眼就能看到有无返回值和返回值是什么.

  • 不会让新手产生 * 到底作用于什么的疑惑, auto f() -> int*.

  • 能更清晰地发生链式调用:

    1auto multiply_by_2(int value) -> int;
    2auto add_1(int value) -> int;
    3
    4// (int) -> (int) -> int
    5multiply_by_2(add_1(value));
    

交叉内容#

函数与结构体#

一个函数可能有多个返回值, 与其用一些奇怪的方法, 用结构体来解决显得更为直接:

1struct Symmetric_minus_result {
2  int lhs_minus_rhs;
3  int rhs_minus_lhs;
4};
5
6Symmetric_minus_result symmetric_minus(int lhs, int rhs) {
7  return {lhs - rhs, rhs - lhs};
8}

函数指针 (function pointer)#

顾名思义是指向函数的 指针, 由于其声明方式比较复杂, 特此单独列出.

对于形如 int do_something(int, double) 的函数, 其指针为 int (*pointer)(int, double).

它可以同函数一样调用.

1int do_something(int, double);
2
3int (*pointer)(int, double) = &do_something;
4pointer(1, 1.0);     // 函数指针本身就可以被调用
5(*pointer)(1, 1.0);  // 解引用后得到的函数也可以被调用

自然, 如果没被 const 限定, 则它可以切换指向的函数.

相关解答#