跳转到内容

C++ 编程

来自

返回值

[编辑 | 编辑源代码]

在声明函数时,必须根据它将返回的类型声明它,这需要三个步骤,在函数声明中、函数实现中(如果不同)以及在相同函数的函数体中使用 return 关键字。

带有结果的函数

你可能已经注意到,一些函数会产生结果。其他函数执行操作但不会 return 值。

从函数中获取值的另一种方法是使用指针或引用作为参数,或者使用全局变量。

从函数中获取多个值

返回类型决定了容量,任何类型都可以,从数组或 std::vector 到结构体或类,它只受你选择的返回类型的限制。

这引发了一些问题
  • 如果你调用一个函数但没有对结果做任何操作(即不将其赋值给变量或用作较大表达式的部分),会发生什么?
  • 如果你将一个没有结果的函数用作表达式的部分,比如 newLine() + 7,会发生什么?
  • 我们可以编写产生结果的函数,还是只能使用 newLine 和 printTwice 之类的东西?

对第三个问题的答案是“是的,你可以编写返回值的函数”。现在我会让你自己尝试回答另外两个问题。当你对 C++ 中合法或非法操作有任何疑问时,第一步是询问编译器。但是你应该注意两个问题,我们在介绍编译器时已经提到过:首先,编译器可能像任何其他软件一样存在错误,因此,并非所有在 C++ 中被禁止的源代码都能够被编译器正确拒绝,反之亦然。另一个问题更加危险:你可以用 C++ 编写程序,这些程序 C++ 实现不需要拒绝,但其行为没有被语言定义。不用说,运行这样的程序可能会,有时也会对运行它的系统造成有害的影响,或者产生损坏的输出!

例如

int MyFunc(); // returns an int
SOMETYPE MyFunc(); // returns a SOMETYPE

int* MyFunc(); // returns a pointer to an int
SOMETYPE *MyFunc(); // returns a pointer to a SOMETYPE
SOMETYPE &MyFunc(); // returns a reference to a SOMETYPE

如果你已经理解了指针声明的语法,那么声明一个返回指针或引用的函数就应该显得合乎逻辑。上面的代码片段展示了如何声明一个返回引用或指针的函数;下面概述了此类函数的定义(实现)看起来会是什么样子。

SOMETYPE *MyFunc(int *p) 
{ 
  //... 

  return p; 
} 

SOMETYPE &MyFunc(int &r) 
{ 
  //... 

  return r; 
}

return 语句会导致执行从当前函数跳转到调用当前函数的任何函数。可以返回可选的结果(return 变量)。一个函数可以有多个 return 语句(但返回相同类型)。

语法
return;
return value;

在函数体中,return 语句不应return 指针或引用,该指针或引用在内存中拥有在函数中声明的局部变量的地址,因为一旦函数退出,所有局部变量都将被销毁,并且你的指针或引用将指向内存中的某个位置,而你不再拥有它,因此你无法保证它的内容。如果指针引用的对象被销毁,则指针被称为悬垂指针,直到它被赋予一个新的值;对这种指针值的任何使用都是无效的。拥有这样的悬垂指针是危险的;指向局部变量的指针或引用不能被允许从这些局部(也称为自动)变量生存的函数中逃逸。

但是,在函数体中,如果你的指针或引用在内存中拥有一个数据类型、结构体或使用 new 运算符动态分配内存的类的地址,那么返回该指针或引用将是合理的。

SOMETYPE *MyFunc()  //returning a pointer that has a dynamically 
{           //allocated memory address is valid code 
  int *p = new int[5]; 

  //... 

  return p; 
}

在这种情况下,一种更好的方法是return 一个对象,例如智能指针,它可以管理内存;使用广泛分布的 newdelete(或 mallocfree)调用进行显式内存管理既繁琐、冗长又容易出错。至少,返回动态分配资源的函数应该被仔细记录。有关更多详细信息,请参阅本书中有关内存管理的部分。

const SOMETYPE *MyFunc(int *p) 
{
  //... 

  return p; 
}

在这种情况下,返回的指针指向的 SOMETYPE 对象可能不会被修改,如果 SOMETYPE 是一个类,那么只能在 SOMETYPE 对象上调用const 成员函数。

如果这样的const return 值是指向类的指针或引用,那么我们不能在该指针或引用上调用非 const 方法,因为这将违反我们不更改它的协议。

注意
作为一般规则,方法应该是const,除非无法使它们成为const。在习惯语义的同时,你可以使用编译器来告知你在何时方法可能不是const -- 如果你声明一个需要是非 const 的方法为const,它会(通常)报错。

静态返回值

[编辑 | 编辑源代码]

当一个函数返回一个静态定位的变量(或指向它的指针)时,必须记住,每次调用使用它的函数时,都有可能覆盖它的内容。如果你想保存该函数的返回值,你应该手动将其保存在其他地方。大多数这样的静态返回值使用全局变量

当然,当你在其他地方保存它时,你应该确保实际将该变量的值复制到另一个位置。如果返回值是一个结构体,你应该创建一个新的结构体,然后将结构体的成员复制过去。

一个这样的函数例子是 标准 C 库 函数 localtime

返回“代码”(最佳实践)

[编辑 | 编辑源代码]

有两种行为

注意
选择和一致地使用这种做法有助于避免简单的错误。个人喜好或组织规定可能会影响决策,但一般的经验法则是,你应该遵循你在当前工作的 代码库 中做出的任何选择。但是,在任何特定情况下做出不同选择的都有正当理由。

正数表示成功
[编辑 | 编辑源代码]

这是“逻辑”的思考方式,因此也是几乎所有初学者都使用的方式。在 C++ 中,这采用布尔值 true/false 测试的形式,其中“true”(也为 1 或任何非零数)表示成功,“false”(也为 0)表示失败。

这种结构的主要问题是所有错误都返回相同的值(false),因此你必须拥有某种外部可见的错误代码才能确定错误发生的位置。例如

 bool bOK;
 if (my_function1())
 {
     // block of instruction 1
     if (my_function2())
     {
         // block of instruction 2
         if (my_function3())
         {
              // block of instruction 3
              // Everything worked
              error_code = NO_ERROR;
              bOK = true;
         }
         else
         {
              //error handler for function 3 errors
              error_code = FUNCTION_3_FAILED;
              bOK = false;
         }
     }
     else
     {
         //error handler for function 2 errors
         error_code = FUNCTION_2_FAILED;
         bOK = false;
     }
 }
 else
 {
     //error handler for function 1 errors
     error_code = FUNCTION_1_FAILED;
     bOK = false;
 }
 return bOK;

如你所见,my_function1 的 else 块(通常是错误处理)可能离测试本身很远;这是第一个问题。当你的函数开始增长时,通常很难同时看到测试和错误处理。

这个问题可以通过 源代码编辑器 的功能(例如折叠)来弥补,或者通过测试函数是否返回“false”而不是 true 来弥补。

 if (!my_function1()) // or if (my_function1() == false) 
 {
     //error handler for function 1 errors

     //...

这也可能使代码看起来更像“0 表示成功”的范式,但可读性稍差。

这种结构的第二个问题是它往往会破坏逻辑测试(my_function2 缩进了一层,my_function3 缩进两层),从而导致可读性问题。

这里的一个优点是,你遵循了 结构化编程 原则,即一个函数只有一个入口和一个出口。

Microsoft Foundation Class Library(MFC)是使用这种范式的标准库的一个例子。

0 表示成功
[编辑 | 编辑源代码]

这意味着,如果一个函数返回 0,则该函数已成功完成。任何其他值都表示发生了错误,返回的值可能指示发生了什么错误。

这种范式的优点是错误处理更接近测试本身。例如,之前的代码变为

 if (0 != my_function1())
 {
     //error handler for function 1 errors
     return FUNCTION_1_FAILED;
 }
 // block of instruction 1
 if (0 != my_function2())
 {
     //error handler for function 2 errors
     return FUNCTION_2_FAILED;
 }
 // block of instruction 2
 if (0 != my_function3())
 {
     //error handler for function 3 errors
     return FUNCTION_3_FAILED;
 }
 // block of instruction 3
 // Everything worked
 return 0; // NO_ERROR

在此示例中,此代码更具可读性(并非总是如此)。但是,此函数现在具有多个退出点,违反了结构化编程的原则。

C 标准库 (libc) 是使用此范式的标准库的示例。

注意
有些人认为使用函数会导致性能损失。在这种情况下,只需使用内联函数,让编译器完成工作。小函数意味着可视性、易于调试和易于维护。

华夏公益教科书