Skip to content

2.4 初识函数

This content is not available in your language yet.

这一节中,我们将开始讲述 C++ 中的一种重要的程序结构:函数。函数涉及的内容非常多,我们将分多个部分来讲解。

考虑这样一个问题:

  1. 已知一个 int 类型的数 x
  2. 将这个数 x 赋值给一个数 a
  3. 判断 a * a 是否大于 x,如果是,则进行第4步,否则进行第5步
  4. 求 (a + x / a) / 2 的值,并赋值给 a,然后回到第3步
  5. 如果不是,将 a 的值赋给 sqrt_x

写成 C++ 代码,这个问题的解决方案可能是这样的:

int x = 42; // 已知 x 的值
int a = x; // 将 x 赋值给 a
while (a * a > x) {
a = (a + x / a) / 2;
}
int sqrt_x = a; // 将 a 的值赋给 sqrt_x

现在,我们需要计算十个这样的 x 对应的 sqrt_x。我们当然可以这样实现:

int x1 = 42;
int a1 = x1;
while (a1 * a1 > x1) {
a1 = (a1 + x1 / a1) / 2;
}
int sqrt_x1 = a1;
int x2 = 43;
int a2 = x2;
//...
//... 依此类推

这样直接复制粘贴这段代码十次,这会导致代码会非常冗长,难以阅读。此外,如果我们要更改计算方法,那么我们需要修改十处代码,这样的代码是非常难以维护的。

所以,我们需要一种方法来避免这种情况。这就是函数最初的作用。

为了解决上面的问题,我们可以定义一个函数来计算 sqrt_x。函数定义的形式如下:

return_type function_name ( parameter_list ) statement

其中,return_type 表示函数的返回类型,是一个表示类型的描述符;function_name 表示函数的名字,是一个描述符;parameter_list 是函数的参数列表;statement 表示函数的函数体,是一个复合语句。

返回值是函数计算的结果,返回类型表示返回值的类型(返回值在后面返回语句中介绍)。

函数名是一个用来代指函数的标识符,函数名可以用来调用函数。注意,函数并不是一个对象,不能使用赋值表达式来给函数赋值。

参数列表可以为空,或者是一个用逗号分隔的参数声明列表,形式是:

  • 参数声明
  • 参数声明列表 , 参数声明

这里面,每个参数声明的形式是 类型 标识符 或者 类型(后者省略标识符)。

函数体用一个复合语句来表示,当函数被调用时,复合语句中的语句会被依次执行。

这样,我们可以定义一个函数来计算 sqrt_x:

int sqrt(int x) {
int a = x;
while (a * a > x) {
a = (a + x / a) / 2;
}
return a;
}

上面的代码里,int 是返回类型,sqrt 是函数名,int x 是参数列表。

在上面这段函数的最后,出现了一个 return a; ,这是一种跳转语句,称为返回语句。

返回语句的形式是 return 表达式;,在执行 return 语句时,会计算 表达式 的值,并将这个值作为函数的返回值,然后结束函数的执行。

这里,return a; 的作用是将 a 的值作为函数的返回值,然后结束函数的执行。

函数的调用是一种后缀表达式,其形式是:

function_name ( argument_list )

这里,function_name 是函数的名字,argument_list 是参数列表。

其中,参数列表是用逗号分隔的表达式序列,形式是:

  • assign_expression
  • argument_list , assign_expression

这里,assign_expression 是赋值表达式。

例如,我们可以这样调用 sqrt 函数:

int x = 42;
int sqrt_x = sqrt(x);

在这个例子中,sqrt(x) 是一个函数调用表达式,sqrt 是函数名,x 是参数列表。

当然,我们也可以用更复杂的表达式作为参数:

int x = 42;
int sqrt_x = sqrt(x + 1);

甚至我们可以用函数调用表达式作为参数:

int x = 42;
int sqrt_x = sqrt(sqrt(x + 1));

前面提到,参数可以是赋值表达式,因此这样也可以:

int x = 42;
int sqrt_x = sqrt(x += 43);

函数调用表达式的计算过程是:

  1. 计算函数调用表达式中的参数列表中,每个参数的值,初始化函数的参数
  2. 依次执行函数体中的语句,直到函数执行结束
  3. 将结束函数调用所使用的 return 语句种表达式的值作为函数调用表达式的值

考虑这一段代码,仍然使用这一段 sqrt 函数的代码:

int sqrt(int x) {
int a = x;
while (a * a > x) {
a = (a + x / a) / 2;
}
return a;
}
int xn = 42;
int sqrt_x = sqrt(xn + 1);

整个代码的执行过程是:

  1. 初始化 xn 的值为 42
  2. 计算 xn + 1 的值,得到 43
  3. 用 43 初始化 sqrt 函数的参数 x
  4. 执行 sqrt 函数体中的语句,直到函数执行结束,此时返回语句 return a; 的表达式是 a,即 a 的值,得到 6
  5. 将 6 作为 sqrt(x + 1) 的值,初始化 sqrt_x

初学者常常会弄混 x 和 xn 的区别。需要记住,进入函数体前,会使用参数列表中的值(这里是 xn + 1)初始化函数的参数(x),函数体中这个 x 的作用域只在函数体内部,不会影响函数调用表达式中的参数。

这样,我们只需要调用 sqrt 函数十次,就可以得到十个 sqrt_x 的值,而不需要把这个计算过程抄十次。如下:

int sqrt(int x) {
int a = x;
while (a * a > x) {
a = (a + x / a) / 2;
}
return a;
}
int x1 = 42;
int sqrt_x1 = sqrt(x1);
int x2 = 43;
int sqrt_x2 = sqrt(x2);
//...
//... 依此类推

有的函数不需要返回值,这样的函数称为无返回值函数。无返回值函数的返回类型是 void。例如,我们可以定义一个无返回值函数 add_a:

int a = 1;
void add_a(int x) {
a += x;
}

这里,a += x 是一个赋值表达式,其值是 a + x 的值,副作用是将 a + x 的值赋值给 a。

函数内部语句的副作用称为函数的副作用,这里,add_a 函数的副作用是是将 a 对象增加参数 x 的值。

无返回值函数的返回类型是 void,这是一个特殊的类型,表示没有返回值。因此,无返回值函数的 return 语句是没有表达式的,例如:

int a = 1;
void add_a(int x) {
if(x > 0) return; // 无返回值
a += x;
}

此外,调用无返回值函数时,这个函数调用表达式也没有值。例如:

int a = 1;
void add_a(int x) {
a += x;
}
add_a(2); // 正确:add_a 是无返回值函数
int b = add_a(2); // 错误:add_a 是无返回值函数,没有返回值,这个初始化语句是错误的

在函数调用时,参数的初始化是通过参数列表中的表达式来完成的。

参数和返回值的初始化如同普通变量的初始化一般,遵循相同的转换规则。例如:

int foo(bool a) {
return a;
}
int r = foo(42); // r 的值是 1

这里,int r = foo(42); 进行了这样的过程:

  1. 使用 42 初始化 foo 函数的参数 a, 这会将 a 初始化为 true
  2. 执行 foo 函数
    1. 执行 return a; 语句,返回 a 的值,即 true
    2. 使用 true 初始化返回值,这会将 true 转换为 1
  3. 将 1 赋值给 r

最终,r 的值是 1。

类似于一个对象的名字必须要在声明之后才能使用,函数的名字也必须在定义之后才能调用。例如:

int sqrt(int x) {
add_sqrt_counter(); // 错误:add_sqrt_counter 函数还没有定义
int a = x;
while (a * a > x) {
a = (a + x / a) / 2;
}
return a;
}
int sqrt_counter = 0;
void add_sqrt_counter() {
if (sqrt_counter < 10)
sqrt_counter += 1;
else
sqrt_counter = sqrt(sqrt_counter + 1);
}

在上面的程序里,add_sqrt_counter 函数在 sqrt 函数中被调用,但是 add_sqrt_counter 函数的定义在 sqrt 函数的定义之后。这样的程序是错误的。然而,我们又不能把 add_sqrt_counter 函数的定义放在 sqrt 函数的定义之前,因为 add_sqrt_counter 函数中又调用了 sqrt 函数。

这时,我们可以使用前向声明。函数的前向声明的形式是:

return_type function_name ( expression_list ) ;

其中,return_type 是函数的返回类型,function_name 是函数的名字,expression_list 是参数列表。

读者可以将函数的前向声明的形式理解为把函数体改成一个分号 ;。

使用前向声明,我们可以这样写:

void add_sqrt_counter(); // 前向声明
int sqrt(int x) {
add_sqrt_counter(); // 正确:add_sqrt_counter 函数已经声明
int a = x;
while (a * a > x) {
a = (a + x / a) / 2;
}
return a;
}
int sqrt_counter = sqrt(42);
void add_sqrt_counter() { // add_sqrt_counter 函数的定义
sqrt_counter += 1;
}

这样,我们就解决了函数之间的循环依赖无法表达的问题。

前向声明可以声明多次,但是只能定义一次。前向声明的返回类型和参数列表必须保持一致,但是参数名没有限制。例如:

void foo(int x, bool y); // 符合语法的前向声明
void foo(int x, bool); // 符合语法得前向声明
void foo(int, int); // 错误:参数列表类型不一致
void foo(bool, int); // 错误:参数列表类型不一致(顺序也必须一致)
int foo(int x, bool y); // 错误:返回值类型不一致
void foo(int, bool y) { // 函数定义
// ...
}