【C++基础】第三十六课:返回类型和return语句

无返回值函数,有返回值函数,返回数组指针

Posted by x-jeff on January 12, 2022

【C++基础】系列博客为参考《C++ Primer中文版(第5版)》C++11标准)一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。

1.前言

return语句终止当前正在执行的函数并将控制权返回到调用该函数的地方。return语句有两种形式:

1
2
return;
return expression;

2.无返回值函数

没有返回值的return语句只能用在返回类型是void的函数中。返回void的函数不要求非得有return语句,因为在这类函数的最后一句后面会隐式地执行return。

通常情况下,void函数如果想在它的中间位置提前退出,可以使用return语句。

一个返回类型是void的函数也能使用return语句的第二种形式,不过此时return语句的expression必须是另一个返回void的函数。强行令void函数返回其他类型的表达式将产生编译错误。

3.有返回值函数

return语句的第二种形式提供了函数的结果。只要函数的返回类型不是void,则该函数内的每条return语句必须返回一个值。return语句返回值的类型必须与函数的返回类型相同,或者能隐式地转换成函数的返回类型。

3.1.值是如何被返回的

返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。

同其他引用类型一样,如果函数返回引用,则该引用仅是它所引用对象的一个别名。

1
2
3
4
const string &shorterString(const string &s1, const string &s2) //去掉函数前的const会引发错误
{
	return s1.size() <= s2.size() ? s1 : s2;
}

其中形参和返回类型都是const string的引用,不管是调用函数还是返回结果都不会真正拷贝string对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

using namespace std;
int gi = 42;

int &func(int i) {
    gi += i;
    return gi;
}

int main() {
    int v;
    v = func(2);//v=44,gi=44
    gi = 50;//v=44,gi=50
    v = 100;//v=100,gi=50
    v=func(2);//v=52,gi=52
}

3.2.不要返回局部对象的引用或指针

函数完成后,它所占用的存储空间也随之被释放掉。因此,函数终止意味着局部变量的引用将指向不再有效的内存区域:

1
2
3
4
5
6
7
8
9
const string &manip()
{
	string ret;
	//以某种方式改变一下ret
	if (!ret.empty())
		return ret;//错误:返回局部对象的引用!
	else
		return "Empty";//错误:"Empty"是一个局部临时量
}

上面的两条return语句都将返回未定义的值,也就是说,试图使用manip函数的返回值将引发未定义的行为。对于第一条return语句来说,显然它返回的是局部对象的引用。在第二条return语句中,字符串字面值转换成一个局部临时string对象,对于manip来说,该对象和ret一样都是局部的。当函数结束时临时对象占用的空间也就随之释放掉了,所以两条return语句都指向了不再可用的内存空间。

同样,返回局部对象的指针也是错误的。一旦函数完成,局部对象被释放,指针将指向一个不存在的对象。

3.3.返回类类型的函数和调用运算符

和其他运算符一样,调用运算符也有优先级和结合律。调用运算符的优先级与点运算符和箭头运算符相同,并且也符合左结合律。

1
2
//得到较短string对象的长度
auto sz = shorterString(s1,s2).size();

3.4.引用返回左值

‼️函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值,其他返回类型得到右值。

1
2
3
4
5
6
7
8
9
10
11
12
char &get_val(string &str, string::size_type ix)
{
	return str[ix]; //get_val假定索引值是有效的
}
int main()
{
	string s("a value");
	cout << s << endl;//输出a value
	get_val(s,0)='A';//将s[0]的值改为A
	cout << s << endl;//输出A value
	return 0;
}

如果返回类型是常量引用,我们不能给调用的结果赋值,这一点和我们熟悉的情况是一样的:

1
shorterString("hi","bye")="X";//错误:返回值是个常量

3.5.列表初始化返回值

C++11新标准规定,函数可以返回花括号包围的值的列表。

1
2
3
4
5
6
7
8
9
10
11
vector<string> process()
{
	// ...
	// expected和actual是string对象
	if (expected.empty())
		return {};//返回一个空vector对象
	else if (expected == actual)
		return {"functionX","okay"};//返回列表初始化的vector对象
	else
		return {"functionX",expected,actual};
}

如果函数返回的是内置类型,则花括号包围的列表最多包含一个值,而且该值所占空间不应该大于目标类型的空间。如果函数返回的是类类型,由类本身定义初始值如何使用。

3.6.主函数main的返回值

之前介绍过,如果函数的返回类型不是void,那么它必须返回一个值。但是这条规则有个例外:我们允许main函数没有return语句直接结束。如果控制到达了main函数的结尾处而且没有return语句,编译器将隐式地插入一条返回0的return语句。

main函数的返回值可以看做是状态指示器。返回0表示执行成功,返回其他值表示执行失败,其中非0值的具体含义依机器而定。为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量,我们可以使用这两个变量分别表示成功与失败:

1
2
3
4
5
6
7
int main()
{
	if(some_failure)
		return EXIT_FAILURE;//定义在cstdlib头文件中
	else
		return EXIT_SUCCESS;//定义在cstdlib头文件中
}

因为它们是预处理变量,所以既不能在前面加上std::,也不能在using声明中出现。

3.7.递归

如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为递归函数(recursive function)

1
2
3
4
5
6
7
//计算val的阶乘
int factorial(int val)
{
	if(val>1)
		return factorial(val-1)*val;
	return 1;
}

如果没有终止条件,函数将不断地调用它自身直到程序栈空间耗尽为止。我们有时候会说这种函数含有递归循环(recursion loop)

⚠️main函数不能调用它自己。

4.返回数组指针

因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用。虽然从语法上来说,要想定义一个返回数组的指针或引用的函数比较烦琐,但是有一些方法可以简化这一任务,其中最直接的方法是使用类型别名:

1
2
3
typedef int arrT[10];//arrT是一个类型别名,它表示的类型是含有10个整数的数组
using arrT=int[10];//arrT的等价声明
arrT* func(int i);//func返回一个指向含有10个整数的数组的指针

4.1.声明一个返回数组指针的函数

要想在声明func时不使用类型别名,我们必须牢记被定义的名字后面数组的维度:

1
2
3
int arr[10];//arr是一个含有10个整数的数组
int *p1[10];//p1是一个含有10个指针的数组
int (*p2)[10]=&arr;//p2是一个指针,它指向含有10个整数的数组

和这些声明一样,如果我们想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后。然而,函数的形参列表也跟在函数名字后面且形参列表应该先于数组的维度。因此,返回数组指针的函数形式如下所示:

1
Type (*function(parameter_list))[dimension]

类似于其他数组的声明,Type表示元素的类型,dimension表示数组的大小。(*function(parameter_list))两端的括号必须存在,就像我们定义p2时两端必须有括号一样。如果没有这对括号,函数的返回类型将是指针的数组。一个具体的例子:

1
int (*func(int i))[10];

4.2.使用尾置返回类型

在C++11新标准中还有一种可以简化上述func声明的方法,就是使用尾置返回类型(trailing return type)。任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效。尾置返回类型跟在形参列表后面并以一个->符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个auto

1
2
//func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组
auto func(int i)->int(*)[10];

4.3.使用decltype

1
2
3
4
5
6
7
int odd[]={1,3,5,7,9};
int even[]={0,2,4,6,8};
//返回一个指针,该指针指向含有5个整数的数组
decltype(odd) *arrPtr(int i)
{
	return (i % 2) ? &odd : &even;//返回一个指向数组的指针
}