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
一个对象,例如智能指针,它可以管理内存;使用广泛分布的 new
和 delete
(或 malloc
和 free
)调用进行显式内存管理既繁琐、冗长又容易出错。至少,返回动态分配资源的函数应该被仔细记录。有关更多详细信息,请参阅本书中有关内存管理的部分。
const SOMETYPE *MyFunc(int *p)
{
//...
return p;
}
在这种情况下,返回的指针指向的 SOMETYPE 对象可能不会被修改,如果 SOMETYPE 是一个类,那么只能在 SOMETYPE 对象上调用const 成员函数。
如果这样的const return
值是指向类的指针或引用,那么我们不能在该指针或引用上调用非 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,则该函数已成功完成。任何其他值都表示发生了错误,返回的值可能指示发生了什么错误。
这种范式的优点是错误处理更接近测试本身。例如,之前的代码变为
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) 是使用此范式的标准库的示例。