第七章 函数(part3) return 语句 & 函数声明

7.3. return 语句

return 语句用于结束当前正在执行的函数,并将控制权返回给调用此函数的函数。return 语句有两种形式:

     return;
     return expression;
7.3.1. 没有返回值的函数

不带返回值的 return 语句只能用于返回类型为 void 的函数。

在返回类型为 void 的函数中,return 返回语句不是必需的,隐式的 return 发生在函数的最后一个语句完成时。

一般情况下,返回类型是 void 的函数使用 return 语句是为了引起函数的强制结束,这种return 的用法类似于循环结构中的break 语句的作用。

例如,可如下重写 swap 程序,使之在输入的两个数值相同时不执行任何工作:

     // ok: swap acts on references to its arguments
     void swap(int &v1, int &v2)
     {
          // if values already the same, no need to swap, just return
          if (v1 == v2)
              return;
          // ok, have work to do
          int tmp = v2;
          v2 = v1;
          v1 = tmp;
          // no explicit return necessary
     }

这个函数首先检查两个值是否相等,如果相等则退出函数;

如果不相等,则交换这两个值,隐式的 return 发生在最后一个赋值语句后。

返回类型为 void 的函数通常不能使用第二种形式的 return 语句,但是,它可以返回另一个返回类型同样是void 的函数的调用结果:

     void do_swap(int &v1, int &v2)
     {
         int tmp = v2;
         v2 = v1;
         v1 = tmp;
         // ok: void function doesn't need an explicit return
     }
     void swap(int &v1, int &v2)
     {
         if (v1 == v2)
             return false; // error: void function cannot return a value
         return do_swap(v1, v2); // ok: returns call to a void function

     }

返回任何其他表达式的尝试都会导致编译时的错误。

7.3.2. 具有返回值的函数

return 语句的第二种形式提供了函数的结果。任何返回类型不是 void 的函数必须返回一个值,

而且这个返回值的类型必须和函数的返回类型相同,或者能隐式转化为函数的返回类型。

尽管 C++ 不能确保结果的正确性,但能保证函数每一次 return 都返回适当类型的结果。例如,下面的程序就不能通过编译:

     // Determine whether two strings are equal.
     // If they differ in size, determine whether the smaller
     // one holds the same characters as the larger one
     bool str_subrange(const string &str1, const string &str2)
     {
         // same sizes: return normal equality test
         if (str1.size() == str2.size())
             return str1 == str2;    // ok, == returns bool
         // find size of smaller string
         string::size_type size = (str1.size() < str2.size())
                                  ? str1.size() : str2.size();
         string::size_type i = 0;
         // look at each element up to size of smaller string
         while (i != size) {
             if (str1[i] != str2[i])
                 return;   // error: no return value
         }
         // error: control might flow off the end of the function without a return
         // the compiler is unlikely to detect this error
      }

while 循环中的 return 语句是错误的,因为它没有返回任何值,编译器将检查出这个错误。

第二个错误源于函数没有在 while 循环后提供 return 语句。调用这个函数时,如果一个string 是另一个string 的子集,

执行会退出 while 循环。这里应该有一个 return 语句来处理这种情况。编译器有可能检查出也有可能检查不出这种错误。

执行程序时,不确定在运行阶段会出现什么问题。

<Beware>:

在含有 return 语句的循环后没有提供 return 语句是很危险的,因为大部分的编译器不能检测出这个漏洞,运行时会出现什么问题是不确定的。

主函数 main 的返回值
返回类型不是 void 的函数必须返回一个值,但此规则有一个例外情况:允许主函数 main 没有返回值就可结束。

如果程序控制执行到主函数 main 的最后一个语句都还没有返回,那么编译器会隐式地插入返回 0 的语句。

关于主函数 main 返回的另一个特别之处在于如何处理它的返回值。在已知,可将主函数 main 返回的值视为状态指示器。

返回 0 表示程序运行成功,其他大部分返回值则表示失败。非 0 返回值的意义因机器不同而不同,为了使返回值独立于机器,

cstdlib 头文件定义了两个预处理变量,分别用于表示程序运行成功和失败:

     #include <cstdlib>
     int main()
     {
         if (some_failure)
             return EXIT_FAILURE;
         else
             return EXIT_SUCCESS;
     }

我们的代码不再需要使用那些依赖于机器的精确返回值。相应地,这些值都在 cstdlib 库中定义,我们的代码不需要做任何修改。

返回非引用类型
函数的返回值用于初始化在调用函数处创建的 临时对象

在求解表达式时,如果需要一个地方储存其运算结果,编译器会创建一个没有命名的对象,

这就是临时对象

在英语中,C++ 程序员通常用 temporary 这个术语来代替 temporary object。

用函数返回值初始化临时对象与用实参初始化形参的方法是一样的。

如果返回类型不是引用,在调用函数的地方会将函数返回值复制给临时对象。

当函数返回非引用类型时,其返回值既可以是局部对象,也可以是求解表达式的结果。

例如,下面的程序提供了一个计数器、一个单词 word 和单词结束字符串 ending,当计数器的值大于 1 时,返回该单词的复数版本:

     // return plural version of word if ctr isn't 1
     string make_plural(size_t ctr, const string &word,
                                    const string &ending)
     {
         return (ctr == 1) ? word : word + ending;
     }
我们可以使用这样的函数来输出单词的单数或复数形式。

这个函数要么返回其形参 word 的副本,要么返回一个未命名的临时 string 对象,这个临时对象是由字符串wordending 的相加而产生的。

这两种情况下,return 都在调用该函数的地方复制了返回的 string 对象。

返回引用

当函数返回引用类型时,没有复制返回值。相反,返回的是对象本身。

例如,考虑下面的函数,此函数返回两个 string 类型形参中较短的那个字符串的引用:

     // find longer of two strings
     const string &shorterString(const string &s1, const string &s2)
     {
         return s1.size() < s2.size() ? s1 : s2;
     }

形参和返回类型都是指向 const string 对象的引用,调用函数和返回结果时,都没有复制这些 string 对象。

千万不要返回局部对象的引用
<Beware>:

理解返回引用至关重要的是:千万不能返回局部变量的引用。

当函数执行完毕时,将释放分配给局部对象的存储空间。此时,对局部对象的引用就会指向不确定的内存。考虑下面的程序:

     // Disaster: Function returns a reference to a local object
     const string &manip(const string& s)
     {
          string ret = s;
          // transform ret in some way
          return ret; // Wrong: Returning reference to a local object!
     }

这个函数会在运行时出错,因为它返回了局部对象的引用。当函数执行完毕,字符串 ret 占用的储存空间被释放,

函数返回值指向了对于这个程序来说不再有效的内存空间。

<Tips>:

确保返回引用安全的一个好方法是:请自问,这个引用指向哪个在此之前存在的对象?

引用返回左值

返回引用的函数返回一个左值。因此,这样的函数可用于任何要求使用左值的地方:

     char &get_val(string &str, string::size_type ix)
     {
         return str[ix];
     }
     int main()
     {
         string s("a value");
         cout << s << endl;   // prints a value
         get_val(s, 0) = 'A'; // changes s[0] to A

         cout << s << endl;   // prints A value
         return 0;
     }

给函数返回值赋值可能让人惊讶,由于函数返回的是一个引用,因此这是正确的,该引用是被返回元素的同义词。

如果不希望引用返回值被修改,返回值应该声明为 const

     const char &get_val(...
千万不要返回指向局部对象的指针
函数的返回类型可以是大多数类型。特别地 ,函数也可以返回指针类型。

和返回局部对象的引用一样,返回指向局部对象的指针也是错误的。

一旦函数结束,局部对象被释放,返回的指针就变成了指向不再存在的对象的悬垂指针。


7.3.3. 递归

直接或间接调用自己的函数称为递归函数。一个简单的递归函数例子是阶乘的计算。数n 阶乘是从 1 到n 的乘积。例如,5 的阶乘就是 120。

     1 * 2 * 3 * 4 * 5 = 120

解决这个问题的自然方法就是递归:

     // calculate val!, which is 1*2 *3 ... * val
     int factorial(int val)
     {
         if (val > 1)
             return factorial(val-1) * val;
          return 1;
     }

递归函数必须定义一个终止条件;否则,函数就会“永远”递归下去,这意味着函数会一直调用自身直到程序栈耗尽。

有时候,这种现象称为“无限递归错误”。对于函数 factorialval 为 1 是终止条件。

另一个例子是求最大公约数的递归函数:

// recursive version greatest common divisor program
     int rgcd(int v1, int v2)
     {
         if (v2 != 0)                // we're done once v2 gets to zero
             return rgcd(v2, v1%v2); // recurse, reducing v2 on each call
         return v1;
     }

这个例子中,终止条件是余数为 0。如果用实参 (15, 123) 来调用 rgcd 函数,结果为 3。表 7.1 跟踪了它的执行过程。

表 7.1. rgcd(15, 123) 的跟踪过程
v1

v2

Return

15

123

rgcd(123, 15)

123

15

rgcd(15, 3)

15

3

rgcd(3, 0)

3

0

3


最后一次调用:

     rgcd(3,0)

满足了终止条件,它返回最大公约数 3。该值依次成为前面每个调用的返回值。

这个过程称为此值向上回渗(percolate),直到执行返回到第一次调用 rgcd 的函数。

<Note>:

主函数 main 不能调用自身。

7.4. 函数声明

正如变量必须先声明后使用一样,函数也必须在被调用之前先声明。与变量的定义类似,函数的声明也可以和函数的定义分离;

一个函数只能定义一次,但是可声明多次。

函数声明由函数返回类型、函数名和形参列表组成。形参列表必须包括形参类型,但是不必对形参命名。

这三个元素被称为函数原型,函数原型描述了函数的接口。

<Note>:

函数原型为定义函数的程序员和使用函数的程序员之间提供了接口。在使用函数时,程序员只对函数原型编程即可。

函数声明中的形参名会被忽略,如果在声明中给出了形参的名字,它应该用作辅助文档:

     void print(int *array, int size);
在头文件中提供函数声明
回顾前面章节,变量可在头文件中声明,而在源文件中定义。同理,函数也应当在头文件中声明,并在源文件中定义。

把函数声明直接放到每个使用该函数的源文件中,这可能是大家希望的方式,而且也是合法的。

但问题在于这种用法比较呆板而且容易出错。

解决的方法是把函数声明放在头文件中,这样可以确保对于指定函数其所有声明保持一致。

如果函数接口发生变化,则只要修改其唯一的声明即可。

<Note>:

定义函数的源文件应包含声明该函数的头文件。

将提供函数声明头文件包含在定义该函数的源文件中,可使编译器能检查该函数的定义和声明时是否一致。

特别地,如果函数定义和函数声明的形参列表一致,但返回类型不一致,编译器会发出警告或出错信息来指出这种差异。

7.4.1. 默认实参

默认实参是一种虽然并不普遍、但在多数情况下仍然适用的实参值。

调用函数时,可以省略有默认值的实参。

编译器会为我们省略的实参提供默认值。

默认实参是通过给形参表中的形参提供明确的初始值来指定的。

程序员可为一个或多个形参定义默认值。

但是,如果有一个形参具有默认实参,那么,它后面所有的形参都必须有默认实参。

例如,下面的函数创建并初始化了一个 string 对象,用于模拟窗口屏幕。此函数为窗口屏幕的高、宽和背景字符提供了默认实参:

     string screenInit(string::size_type height = 24,
                       string::size_type width = 80,
                       char background = ' ' );

调用包含默认实参的函数时,可以为该形参提供实参,也可以不提供。

如果提供了实参,则它将覆盖默认的实参值;否则,函数将使用默认实参值。

下面的函数 screenInit 的调用都是正确的:

     string screen;
     screen = screenInit();       // equivalent to screenInit (24,80,' ')
     screen = screenInit(66);     // equivalent to screenInit (66,80,' ')
     screen = screenInit(66, 256);       // screenInit(66,256,' ')
     screen = screenInit(66, 256, '#');

函数调用的实参按位置解析,默认实参只能用来替换函数调用缺少的尾部实参。

例如,如果要给 background 提供实参,那么也必须给 height width 提供实参:

     screen = screenInit(, , '?'); // error, can omit only trailing arguments
     screen = screenInit( '?');    // calls screenInit('?',80,' ')
注意第二个调用,只传递了一个字符值,虽然这是合法的,但是却并不是程序员的原意。

因为 '?' 是一个 charchar 可提升为最左边形参的类型,所以这个调用是合法的。

最左边的形参具有 string::size_type 类型,这是 unsigned 整型。
在这个调用中,char 实参隐式地提升为 string::size_type 类型,并作为实参传递给形参 height

<Beware>:

因为 char 是整型,因此把一个 char 值传递给 int 型形参是合法的,反之亦然。

这个事实会导致很多误解。例如,如果函数同时含有 char 型和 int 型形参,则调用者很容易以错误的顺序传递实参。

如果使用默认实参,则这个问题会变得更加复杂。

设计带有默认实参的函数,其中部分工作就是排列形参,使最少使用默认实参的形参排在最前,最可能使用默认实参的形参排在最后。


默认实参的初始化式

默认实参可以是任何适当类型的表达式:

     string::size_type screenHeight();
     string::size_type screenWidth(string::size_type);
     char screenDefault(char = ' ');
     string screenInit(
         string::size_type height = screenHeight(),
         string::size_type width = screenWidth(screenHeight()),
         char background = screenDefault());
如果默认实参是一个表达式,而且默认值用作实参,则在调用函数时求解该表达式。

例如,每次不带第三个实参调用函数 screenInit 时,编译器都会调用函数 screenDefaultbackground 获得一个值。

指定默认实参的约束

既可以在函数声明也可以在函数定义中指定默认实参。

但是,在一个文件中,只能为一个形参指定默认实参一次。下面的例子是错误的:

     // ff.h
     int ff(int = 0);

     // ff.cc
     #include "ff.h"
     int ff(int i = 0) { /* ... */ } // error

<Note>;

通常,应在函数声明中指定默认实参,并将该声明放在合适的头文件中。

如果在函数定义的形参表中提供默认实参,那么只有在包含该函数定义的源文件中调用该函数时,默认实参才是有效的。











  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值