在按引用返回和按地址返回中,我们讨论了按引用返回。特别是,我们指出:“按引用返回的对象必须在函数返回后仍然存在。”这意味着我们不应该按引用返回局部变量,因为局部变量被销毁后,引用将变成悬挂引用。然而,按引用返回通过引用传递的函数参数,或者具有静态存储期的变量(静态局部变量或全局变量)通常是安全的,因为这些变量在函数返回后通常不会被销毁。
例如:
// 接收两个std::string对象,返回按字母顺序排在前面的那个
const std::string& firstAlphabetical(const std::string& a, const std::string& b)
{
return (a < b) ? a : b; // 我们可以使用std::string上的operator<来确定哪个字母顺序在前
}
int main()
{
std::string hello { "Hello" };
std::string world { "World" };
std::cout << firstAlphabetical(hello, world); // hello或world将通过引用返回
return 0;
}
成员函数也可以按引用返回,并且它们遵循与非成员函数相同的规则,以确定何时按引用返回是安全的。然而,成员函数有一个额外的情况需要讨论:返回数据成员的成员函数。
这种情况最常见于获取器访问函数,因此我们将通过获取器成员函数来说明这个主题。但请注意,这个主题适用于任何返回数据成员引用的成员函数。
按值返回数据成员可能代价高昂
考虑以下示例:
#include <iostream>
#include <string>
class Employee
{
std::string m_name{};
public:
void setName(std::string_view name) { m_name = name; }
std::string getName() const { return m_name; } // 获取器按值返回
};
int main()
{
Employee joe{};
joe.setName("Joe");
std::cout << joe.getName();
return 0;
}
在这个示例中,getName()
访问函数按值返回std::string m_name
。
虽然这是最安全的做法,但这也意味着每次调用getName()
时都会创建一个代价高昂的m_name
副本。由于访问函数通常会被频繁调用,因此这通常不是最佳选择。
按左值引用返回数据成员
成员函数也可以按(const
)左值引用返回数据成员。
数据成员与其所属对象的生命周期相同。由于成员函数始终是在某个对象上调用的,而该对象必须在调用者的范围内存在,因此成员函数按(const
)左值引用返回数据成员通常是安全的(因为返回的成员在函数返回时仍然在调用者的范围内存在)。
让我们更新上面的示例,使getName()
按const
左值引用返回m_name
:
#include <iostream>
#include <string>
class Employee
{
std::string m_name{};
public:
void setName(std::string_view name) { m_name = name; }
const std::string& getName() const { return m_name; } // 获取器按const引用返回
};
int main()
{
Employee joe{}; // joe直到函数结束才销毁
joe.setName("Joe");
std::cout << joe.getName(); // 通过引用返回joe.m_name
return 0;
}
现在,当调用joe.getName()
时,joe.m_name
将通过引用返回给调用者,从而避免了创建副本。调用者随后使用这个引用来将joe.m_name
打印到控制台。
由于joe
在main()
函数结束之前一直存在于调用者的范围内,因此对joe.m_name
的引用在整个期间也是有效的。
关键洞察:按(const
)左值引用返回数据成员是安全的。隐式对象(包含数据成员)在函数返回后仍然存在于调用者的范围内,因此返回的引用将是有效的。
返回数据成员的成员函数的返回类型应与数据成员的类型匹配
一般来说,按引用返回的成员函数的返回类型应与被返回的数据成员的类型匹配。在上面的示例中,m_name
的类型是std::string
,因此getName()
返回const std::string&
。
如果返回std::string_view
,则每次调用函数时都需要创建并返回一个临时的std::string_view
,这是不必要的低效做法。如果调用者需要一个std::string_view
,他们可以自行进行转换。
最佳实践:返回引用的成员函数应返回与被返回的数据成员相同类型的引用,以避免不必要的转换。
对于获取器,使用auto
让编译器从被返回的成员推导返回类型是一种确保不发生转换的有用方法:
#include <iostream>
#include <string>
class Employee
{
std::string m_name{};
public:
void setName(std::string_view name) { m_name = name; }
const auto& getName() const { return m_name; } // 使用`auto`从m_name推导返回类型
};
int main()
{
Employee joe{}; // joe直到函数结束才销毁
joe.setName("Joe");
std::cout << joe.getName(); // 通过引用返回joe.m_name
return 0;
}
相关内容:我们在第10.9课——函数的类型推导中讨论了auto
返回类型。
然而,使用auto
返回类型会从文档的角度隐藏获取器的返回类型。例如:
const auto& getName() const { return m_name; } // 使用`auto`从m_name推导返回类型
不清楚这个函数实际返回的是哪种字符串(它可能是std::string
、std::string_view
、C风格字符串,或者其他类型)。
因此,我们通常更倾向于使用显式的返回类型。
右值隐式对象和按引用返回
有一种情况需要我们稍微小心一些。在上面的示例中,joe
是一个左值对象,它一直存在到函数结束。因此,joe.getName()
返回的引用在整个函数结束之前也将是有效的。
但如果我们的隐式对象是一个右值(例如某个按值返回的函数的返回值)呢?右值对象在其被创建的完整表达式结束时被销毁。当右值对象被销毁时,对该右值成员的任何引用都将失效并变成悬挂引用,使用这样的引用将产生未定义行为。
因此,只有在创建右值对象的完整表达式内部,才能安全地使用右值对象成员的引用。
提示:我们在第1.10课——表达式简介中讨论了什么是完整表达式。
警告:右值对象在其被创建的完整表达式结束时被销毁。此时,对该右值对象成员的任何引用都将变成悬挂引用。
只有在创建右值对象的完整表达式内部,才能安全地使用右值对象成员的引用。
让我们探讨一些与之相关的案例:
#include <iostream>
#include <string>
#include <string_view>
class Employee
{
std::string m_name{};
public:
void setName(std::string_view name) { m_name = name; }
const std::string& getName() const { return m_name; } // 获取器按const引用返回
};
// createEmployee()按值返回一个Employee对象(这意味着返回值是一个右值)
Employee createEmployee(std::string_view name)
{
Employee e;
e.setName(name);
return e;
}
int main()
{
// 情况1:可以使用返回的引用访问右值类对象成员(在相同表达式中)
std::cout << createEmployee("Frank").getName();
// 情况2:错误:保存返回的引用以供后续使用
const std::string& ref { createEmployee("Garbo").getName() }; // 当createEmployee()的返回值被销毁时,引用变成悬挂引用
std::cout << ref; // 未定义行为
// 情况3:可以将引用的值复制到局部变量以供后续使用
std::string val { createEmployee("Hans").getName() }; // 复制引用的成员
std::cout << val; // 可以:val独立于引用的成员
return 0;
}
当调用createEmployee()
时,它将按值返回一个Employee
对象。这个返回的Employee
对象是一个右值,它将存在到包含对createEmployee()
调用的完整表达式结束。当这个右值对象被销毁时,对该对象成员的任何引用都将变成悬挂引用。
在情况1中,我们调用createEmployee("Frank")
,它返回一个右值Employee
对象。然后我们在这个右值对象上调用getName()
,它返回对m_name
的引用。这个引用随后被立即用来将名字打印到控制台。此时,包含对createEmployee("Frank")
调用的完整表达式结束,右值对象及其成员被销毁。由于