在C++中,将值作为常量、引用和常量引用返回的含义是什么?

14

我正在学习C ++,但仍然对此感到困惑。在C++中返回值作为常量、引用和常量引用有什么影响?例如:

const int exampleOne();
int& exampleTwo();
const int& exampleThree();
8个回答

27
以下是您所有案例的简介:
• 引用返回:函数调用可以用作赋值语句的左侧。例如,如果您已重载运算符[],则可以使用运算符重载,像这样写:
a[i] = 7;

当使用引用返回值时,需要确保在返回后该对象仍然可用:不应返回对局部变量或临时变量的引用。

• 以常量值形式返回:防止函数在赋值表达式的左侧使用。考虑重载运算符+。可以这样写:

a + b = c; // This isn't right

operator+ 的返回类型为 "const SomeType",允许通过值返回并同时防止在赋值的左侧使用表达式。

以常量值返回还可以避免输入错误,例如:

if (someFunction() = 2)

当您想表达时

if (someFunction() == 2)

如果将someFunction()声明为

const int someFunction()

如果有一个if()的错别字,编译器会发现并提示错误。

• 作为常量引用返回:该函数调用不能出现在赋值语句的左侧,并且您希望避免复制(通过值返回)。例如,假设我们有一个名为Student的类,并且我们想提供一个访问器id()来获取学生的ID:

class Student
{
    std::string id_;

public:

    const std::string& id() const;
};

const std::string& Student::id()
{
    return id_;
}

考虑id()访问器。应该声明为const,以确保id()成员函数不会修改对象的状态。现在考虑返回类型。如果返回类型是string&,那么可以编写类似以下内容的代码:

Student s;
s.id() = "newId";

这并不是我们想要的。

我们本可以返回值,但在这种情况下,通过引用返回更加高效。将返回类型设置为const string&还可以防止id被修改。


哇,Markdown编辑真的搞砸了这里...我不知道有什么方法可以修复它而不删除项目符号。 - aib
我已经用实际的圆点字符(•)替换了弹出格式。我认为这样看起来更好,但随时可以回滚。 - aib
谢谢大家的努力回答问题,但那个答案正中要害。 - user96815
1
-1 抱歉,因为您忽略了值和引用返回类型之间最关键和危险的区别——如果具有引用返回类型的函数返回一个局部变量或临时变量,则它所引用的值将在返回给调用代码之前被销毁,导致程序崩溃。更糟糕的是,您的程序将编译而不会出现错误(甚至在许多编译器上也没有警告)。 - j_random_hacker
3
你的Student::id()示例是安全的,因为_id指的是一个长期存在的成员变量,但下面类似的代码会导致未定义行为(崩溃):"const std::string& Student::id() { return id_ + "xyz"; }" - j_random_hacker
3
是的,通过引用返回临时变量或局部变量是绝对错误的。我试图通过一个合适的例子突出按值返回按引用返回之间的区别,而不是声明哪种用法比另一种更合适。为了完整起见,我已更新了我的答案。 - Debajit

6
基本要理解的是,通过值返回会创建一个对象的新副本;而通过引用返回将返回到现有对象的引用。注意:与指针一样,你可能会有悬空引用。所以,不要在函数中创建一个对象并返回对该对象的引用——当函数返回时,它将被销毁,并返回一个悬空引用。
按值返回:
- 当你有POD(Plain Old Data)时 - 当你想返回一个对象的副本时
按引用返回:
- 当你有性能理由避免返回对象的副本,并且你了解对象的生命周期时 - 当你必须返回某个对象实例,并且你了解对象的生命周期时
常量/常量引用帮助你强制执行代码的契约,并帮助用户的编译器找到使用错误。它们不影响性能。

1
+1,非常好的答案。需要注意的是,按值返回的情况并不像听起来那么昂贵,因为编译器可以使用返回值优化来消除大部分复制操作(而且所有非玩具编译器都实现了这个优化)。 - j_random_hacker

1
  • 返回引用

您可以返回一个指向某个值的引用,比如类成员。这样就不会创建副本。但是,在栈中返回值的引用是不明确的行为,应避免这种情况。

#include <iostream>                                                                                                                                          
using namespace  std;


class A{
private: int a;

public:
        A(int num):a(num){}

        //a to the power of 4.
        int& operate(){
                this->a*=this->a;
                this->a*=this->a;
                return this->a;
        }

        //return constant copy of a.
        const int constA(){return this->a;}

        //return copy of a.
        int getA(){return this->a;}
};

int main(){

        A obj(3);
        cout <<"a "<<obj.getA()<<endl;
        int& b=obj.operate(); //obj.operate() returns a reference!

        cout<<"a^4 "<<obj.getA()<<endl;
        b++;
        cout<<"modified by b: "<<obj.getA()<<endl;
        return 0;
}

b 和 obj.a “指向” 同一个值,因此修改 b 会修改 obj.a 的值。

$./a.out 
a 3
a^4 81
modified by b: 82
  • 返回一个常量值

另一方面,返回一个常量值表示该值不能被修改。需要注意的是,返回的值是一份副本: 例如,

constA()++;

这会导致编译错误,因为constA()返回的副本是常量。但这只是一个副本,它并不意味着A::a是常量。

  • 返回一个const引用

这类似于返回一个const值,除了不返回副本,而是返回对实际成员的引用。然而,它不能被修改。

const int& refA(){return this->a;}

const int& b = obj.refA();
b++;

会导致编译错误。


1
const int exampleOne();

返回某个int的const副本。也就是说,您创建了一个新的int,它可能无法被修改。在大多数情况下,这并不是真正有用的,因为您无论如何都会创建一个副本,所以通常不会关心它是否被修改。那么为什么不只返回一个常规的int呢?

对于更复杂的类型,修改它们可能会产生不良的副作用。(从概念上讲,假设一个函数返回代表文件句柄的对象。如果该句柄是const,则文件是只读的,否则可以被修改。然后在某些情况下,函数返回一个const值是有意义的。但总的来说,返回const值是不常见的。

int& exampleTwo();

这个返回一个 int 的引用。但这并不影响该值的生命周期,因此在以下情况下可能会导致未定义的行为:

int& exampleTwo() {
  int x = 42;
  return x;
}

我们正在返回一个不再存在的值的引用。编译器可能会警告你,但它很可能仍然会编译。但这是毫无意义的,并且迟早会导致奇怪的崩溃。尽管如此,在其他情况下经常使用这种方法。如果该函数是类成员,则可以返回对成员变量的引用,其生命周期将持续到对象超出范围,这意味着当函数返回时函数返回值仍然有效。

const int& exampleThree();

大多数情况下与上面相同,返回对某个值的引用,而不占有它或影响其生命周期。主要区别在于现在您返回对一个const(不可变)对象的引用。与第一种情况不同,这更常用,因为我们不再处理其他人不知道的副本,因此修改可能会对代码的其他部分可见。(您可能拥有一个在定义时是非const的对象,并且允许代码的其他部分通过返回对其的const引用来以const方式访问它的函数。


1
在您的第一个示例中,const 在许多情况下都是重要且有用的。考虑重载+运算符。如果返回类型是“Type”,而不是“const Type”,那么就可以编写像“a + b = c”这样的代码,这是不正确的。 - Debajit
1
不行。如果函数(或运算符)返回int(而不是引用),它是r-value,无法分配。如果它是一个引用,你是正确的,但是operator+通常首先不应返回引用,出于这个原因和其他原因。 - jalf
+1。奇怪的是,像这样高质量的答案还没有浮现到顶部(尚)。顺便说一句,我认为你需要在第四个正文段落中将“返回类到成员变量”更改为“返回成员变量的引用”。 - j_random_hacker
@Debajit:在这种特殊情况下,jalf是正确的,所有赋值运算符都需要一个“可修改的左值”。但在一般情况下,你是对的——可以在由函数返回的非const rvalue上调用其他方法(即使是非const方法)的对象,因此返回一个const值将防止在对象上调用非const方法。 - j_random_hacker
@j_random_hacker:你说得对,我已经编辑过了(感谢你的翻译)。现在再读一遍,我也不确定自己当时想表达什么,但是“引用”在那个上下文中确实有意义。至于const值的问题,我确实提到了它会对更复杂的类型产生影响。 - jalf

1

返回一个常量值并不是一个很常见的习惯用法,因为你返回的是一个新的东西,只有调用者才能拥有它,所以很少出现他们不能修改它的情况。在你的例子中,你不知道他们会用它做什么,那么为什么要阻止他们修改呢?

请注意,在C++中,如果你没有声明某个东西是引用或指针,它就是一个值,所以你将创建一个新的副本而不是修改原始对象。如果你来自其他默认使用引用的语言,这可能不是很明显。

返回一个引用或常量引用意味着它实际上是另一个对象,因此对它的任何修改都将影响到该其他对象。一个常见的惯用语法可能是暴露类的私有成员。

const表示无论什么情况下都不能修改它,因此如果你返回一个const引用,你不能调用任何非const方法或修改任何数据成员。


实现一个包含复杂类的堆栈的pop()方法返回顶部作为const引用是明智的吗?我无法修改该类的任何属性吗? - user96815
STL弹出方法(pop(),pop_back(),pop_front()等)都返回void。你几乎肯定想做同样的事情 - 如果你弹出堆栈的顶部成员,你不能返回对它的引用,因为它已经不存在了。你可能想通过值来返回它。这将起作用,只是在效率(额外复制项)和方便性之间进行微小的权衡,如果你经常想在使用项之前弹出一个项。 - Peter
@Peter:还有一个你没有提到的权衡,C++标准库通过在pop()函数中返回void来承认它,即强异常保证。 - Johann Gerell
@sunbird:实际上,编写一个返回引用的pop()版本的唯一方法是在您的类中保留另一个成员变量,例如称为previous_top_of_stack,并返回对其的引用。正如我在Debajit的答案评论中所说,您不能安全地返回对局部变量或临时变量的引用。 - j_random_hacker

0

从来没有想过,我们可以通过引用返回一个const值,我不认为这样做有价值。 但是,如果您尝试像这样将一个值传递给函数,那么这是有意义的

void func(const int& a);

这样做的好处是告诉编译器不要在内存中复制变量a(当您按值而不是按引用传递参数时会执行此操作)。const是为了避免修改变量a。


我关心的是返回值而不是参数。 - user96815
2
对于指针大小的类型,如 int,const 引用并没有实际意义,因为在调用函数时仍然必须将值(将引用视为指针)推送到堆栈上。所以 "const int&" 本质上与 "int" 相同。将 const 引用传递给函数的好处真正体现在“大型”对象上。 - Johann Gerell

0

你的第一个案例:

const int exampleOne();

对于像 int 这样的简单类型,这几乎永远不是你想要的,因为 const 是无意义的。按值返回意味着复制,你可以自由地将其分配给非 const 对象:

int a = exampleOne(); // perfectly valid.

当我看到这个时,通常是因为编写代码的人试图保持const正确性,这是值得称赞的,但他们并没有完全理解自己所写的内容的含义。然而,在重载运算符和自定义类型的情况下,确实存在一些情况会有所不同。

一些编译器(新版GCC、Metrowerks等)会对简单类型的此类行为发出警告,因此应该避免使用。


在第一个例子中,const并不是毫无意义的。考虑重载+运算符。如果返回类型是“Type”而不是“const Type”,那么就可以编写像“a + b = c”这样的代码,这是不正确的。 - Debajit
+1,因为Debajit上面的评论实际上是不正确的。(说其他非赋值运算符非const方法可以在非const rvalue上调用是正确的,但你的答案已经解决了这个问题。) - j_random_hacker

0

我认为你的问题实际上包含两个问题:

  • 返回const的影响是什么。
  • 返回引用的影响是什么。

为了给你一个更好的答案,我将对这两个概念进行一些解释。

关于const关键字

const关键字意味着该对象不能通过该变量进行修改,例如:

MyObject *o1 = new MyObject;
const MyObject *o2 = o1;
o1->set(...); // Will work and will change the instance variables.
o2->set(...); // Won't compile.

现在,const关键字可以在三个不同的上下文中使用:

  • 向方法的调用者保证您不会修改对象

例如:

void func(const MyObject &o);
void func(const MyObject *o);

在这两种情况下,对对象所做的任何修改都将保持在函数作用域之外,这就是为什么使用关键字 const 可以确保调用者我不会修改它的实例变量。

  • 向编译器确保特定方法不改变对象

如果您有一个类和一些方法通过“获取”或“获取”信息从实例变量中获取信息而不修改它们,则即使使用 const 关键字,我也应该能够使用它们。例如:

class MyObject
{
    ...
public:
    void setValue(int);
    int getValue() const; // The const at the end is the key
};

void funct(const MyObject &o)
{
    int val = o.getValue(); // Will compile.
    a.setValue(val); // Won't compile.
}
  • 最后,(你的情况)返回一个const值

这意味着返回的对象不能直接被修改或改变。例如:

const MyObject func();

void func2()
{
    int val = func()->getValue(); // Will compile.
    func()->setValue(val); // Won't compile.
    MyObject o1 = func(); // Won't compile.
    MyObject o2 = const_cast<MyObject>(func()); // Will compile.
}

关于const关键字的更多信息:C++ Faq Lite - Const Correctness

关于引用

返回或接收引用意味着对象不会被复制。这意味着对值本身所做的任何更改都将在函数范围之外反映出来。例如:

void swap(int &x, int &y)
{
    int z = x;
    x = y;
    y = z;
}
int a = 2; b = 3;
swap(a, b); // a IS THE SAME AS x inside the swap function

因此,返回一个引用值意味着该值可以被更改,例如:

class Foo
{
public:
    ...
    int &val() { return m_val; }
private:
    int m_val;
};

Foo f;
f.val() = 4; // Will change m_val.

关于引用的更多信息:C++ Faq Lite - 引用和值语义 现在,回答你的问题
const int exampleOne();

意味着通过变量返回的对象不能被更改。在返回对象时更有用。

int& exampleTwo();

这意味着返回的对象与函数内部的对象相同,对该对象所做的任何更改都将在函数内部反映

const int& exampleThree();

意味着返回的对象与函数内部的对象相同,不能通过该变量进行修改。


很好的概述,但您忽略了引用和值返回类型之间最重要的区别:返回对本地或临时对象的引用将编译但会导致崩溃,因为该值在到达调用代码之前被销毁。此外,我认为您想在第4个代码片段中使用“func().getValue()”和“func().setValue()”。 - j_random_hacker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接