在之前的文章中已经提到过,当使用按值传递时会创建参数的一个副本到函数中。对于基本类型(复制成本较低),这是可以的。但对于类类型(如 std::string ),复制通常成本较高。我们可以通过使用(const)引用传递(或按地址传递)来避免进行昂贵的复制。

这篇文章主要介绍一些在关于使用引用返回和地址返回的基础知识。

下面是一个使用引用返回的程序:

#include <iostream>
#include <string>

const std::string& getProgramName() // 返回一个常量引用
{
    static const std::string s_programName { "Calculator" }; // 具有静态存储期,在程序结束时销毁

    return s_programName;
}

int main()
{
    std::cout << "This program is named " << getProgramName();

    return 0;
}

返回引用的对象必须在函数返回后存在

使用引用返回有一个重要的注意事项:程序员必须确保被引用的对象在返回引用的函数之后仍然存在,否则将会出现返回的引用悬空的情况(引用一个已被销毁的对象),使用该引用会导致为定义行为。

在上面的程序中,因为 s_programName 使用了static关键字修饰,具有静态持续时间,它的生命周期 s_programName 将存在到程序结束。当 main() 访问返回的引用时,它实际上是在访问 s_programName ,这是可以的,因为 s_programName 不会在之后被销毁。

现在让我们修改上述程序,以展示当我们的函数返回一个悬垂引用时会发生什么:

#include <iostream>
#include <string>

const std::string& getProgramName()
{
    const std::string programName { "Calculator" }; // 现在是一个非静态局部变量,在函数结束时销毁

    return programName;
}

int main()
{
    std::cout << "这个程序的名称是 " << getProgramName(); // 未定义行为

    return 0;
}

程序的结果未定义。当 getProgramName() 返回时,返回一个绑定到局部变量 programName 的引用。然后,因为 programName 是一个具有自动持续时间的局部变量,所以在函数结束时销毁 programName 。这意味着返回的引用现在是悬垂的,在 main() 函数中使用 programName 会导致未定义的行为。

对象通过引用返回时必须超出返回引用的函数的作用域,否则将会产生悬空引用。不要通过引用返回非静态的局部变量或者临时变量。


临时对象的生命周期延长(lifetime extension)机制,不能跨越函数边界生效。

临时对象:

  • 临时对象是在表达式求值过程中创建的,但没有名字的对象。
  • 例如,std::string("Hello") 会创建一个临时的 std::string 对象。

lifetime extension:

  • 在某些特定情况下,C++编译器会延长临时对象的生命周期,以避免悬空引用;

考虑下面这个返回临时变量的引用的例子:

#include <iostream>

const int& returnByConstReference()
{
    return 5; // 返回临时对象的常量引用
}

int main()
{
    const int& ref { returnByConstReference() };

    std::cout << ref; // 未定义行为

    return 0;
}

在上述程序中, returnByConstReference() 返回一个整型字面量,但函数的返回类型是 const int& 。这导致创建并返回一个绑定到包含值 5 的临时对象的临时引用。

由于 returnByConstReference() 返回的是临时对象的引用,而临时对象在函数返回后立即被销毁,因此 ref 变成了一个悬空引用(dangling reference)。

当你在一个函数内部创建一个临时对象,并返回该临时对象的引用时,这个引用在函数外部使用时,其引用的对象可能已经被销毁,从而导致未定义行为。

所谓的函数边界指的是函数的开始和结束。在函数内部创建的局部变量和临时对象,在函数结束时通常会被销毁。换句话说,如果你在一个函数中创建了一个临时对象,并返回了对它的引用,那么不要期望这个引用在函数外部仍然有效。因为临时对象的生命周期在函数结束时就结束了。


不要通过引用返回非const静态局部变量

在第一个实例中,通过引用返回一个 const 静态局部变量,以简单的方式说明通过引用返回的机制。然而,通过引用返回非 const 静态局部变量相当不规范,通常应避免。

#include <iostream>
#include <string>

const int& getNextId()
{
    static int s_x{ 0 }; // 注意:变量是非 const 的
    ++s_x; // 生成下一个 id
    return s_x; // 并返回它的引用
}

int main()
{
    const int& id1 { getNextId() }; // id1 是一个引用
    const int& id2 { getNextId() }; // id2 是一个引用

    std::cout << id1 << id2 << '\n';

    return 0;
}

22

程序之后后打印22,这是因为id1和id2引用的是同一个对象静态变量 s_x ),所以当任何内容(例如 getNextId() )修改该值时,所有引用现在都访问的是修改后的值。

此示例可以通过将 id1id2 设置为普通变量(而不是引用)来修复,这样它们将保存返回值的副本而不是 s_x 的引用。

#include <iostream>
#include <string>

const int& getNextId()
{
    static int s_x{ 0 };
    ++s_x;
    return s_x;
}

int main()
{
    const int id1 { getNextId() }; // id1 现在是一个普通变量,并接收 getNextId() 通过引用返回的值的拷贝
    const int id2 { getNextId() }; // id1 现在是一个普通变量,并接收 getNextId() 通过引用返回的值的拷贝

    std::cout << id1 << id2 << '\n';

    return 0;
}

在上面的示例中, getNextId() 返回一个引用,但 id1id2 是非引用变量。在这种情况下,返回的引用的值被复制到普通变量中。因此,这个程序打印:

12


通过引用返回引用参数

如果一个参数是通过引用传递给函数的,那么安全地通过引用返回该参数是没什么问题的,但是为了将一个参数传递给函数,该参数必须在调用者的作用域中存在。

#include <iostream>
#include <string>

// 接受两个 std::string 对象,返回按字母顺序排列时较小的那个
const std::string& firstAlphabetical(const std::string& a, const std::string& b)
{
    return (a < b) ? a : b; // 使用 operator< 进行字典序比较,确定哪个字符串在前
}

int main()
{
    std::string hello { "Hello" };
    std::string world { "World" };

    std::cout << firstAlphabetical(hello, world) << '\n'; // 输出按字母顺序排列时较小的字符串

    return 0;
}

Hello

如果我们使用按值传递和按值返回,我们将为 std::string 创建多达 3 个副本(每个参数一个,返回值一个)。通过使用按引用传递/按引用返回,我们可以避免这些副本。


可以将通过 const 引用传递的右值以 const 引用返回。

当一个 const 引用的参数是一个 rvalue 时,仍然可以通过 const 引用返回该参数。这是因为右值直到它们被创建的表达式结束才会被销毁。

#include <iostream>
#include <string>

std::string getHello()
{
    return "Hello";
}

int main()
{
    const std::string s{ getHello() };

    std::cout << s;

    return 0;
}

在这种情况下, getHello() 返回一个 std::string 的值,这是一个右值。然后使用这个右值来初始化 s 。在初始化 s 之后右值被销毁。


调用者通过引用修改值

当一个参数通过非const引用传递给函数时,函数可以使用该引用来修改参数的值。同样,当从函数返回一个非const引用时,调用者可以使用该引用来修改返回的值。

#include <iostream>

int& max (int& a,int& b)
{
    return (a > b) ? a : b;
}

int main()
{
    int a{ 4 };
    int b{ 10 };

    max(a, b) = 7; 

    std::cout << a << b << '\n';

    return 0;
}

函数最后将会打印:4 7


返回地址

地址返回几乎与引用返回相同,只是返回的是对象的指针而不是对象的引用。返回地址的方式与通过引用返回有相同的注意事项——通过地址返回的对象必须比返回地址的函数的作用域长,否则调用者将收到一个悬空指针。

地址返回比引用返回的主要优势是,如果没有有效的对象返回,我们可以让函数返回 nullptr 。例如,假设我们有一个学生列表,我们想要在列表中搜索。如果我们找到了我们正在寻找的学生,我们可以返回一个指向表示匹配学生的对象的指针。如果没有找到匹配的学生,我们可以返回 nullptr 来表示未找到匹配的学生对象。

感谢阅读,欢迎指正!