如果我从getter中创建迭代器,程序就会中止

Program abort if I make an iterator from a getter

本文关键字:程序 创建 getter 如果 迭代器      更新时间:2023-10-16

这是一个奇怪的行为,我不明白我有。

我有一个类a,它有一个列表,还有一个getter:

class A
{
  private:
   std::list<OtherClass *> l;
  public:
   std::list<OtherClass *> getL()
   {
     return l;
   }
}

然后,如果我这样做:

A inst;
std::list<OtherClass *>::iterator itB = inst.getL().begin();
std::list<OtherClass *>::iterator itE = inst.getL().end();
for (; itB != itE; ++itB) // Instant ABORT !

但是如果我这样做了:

A inst;
std::list<OtherClass *> l = inst.getL();
std::list<OtherClass *>::iterator itB = l.begin();
std::list<OtherClass *>::iterator itE = l.end();
for (; itB != itE; ++itB) // It works now !
谁能给我解释一下为什么会这样?为什么我要通过这样一个临时变量来不中止呢?

虽然所有其他答案都建议可修改的引用作为返回值,但我将它们设置为const:

const std::list<OtherClass *> &getL() const;

我还将函数本身设置为const,这意味着它不会修改对象本身。这样你就有了一个正确的getter方法(它既不应该修改对象,也不应该返回一个可修改的引用)。

你可能想要引入这样一个getter函数的第二个版本,让可以修改属性(如果我们不想隐藏一些在属性改变时需要执行的代码,比如更新一些相关的东西):

std::list<OtherClass *> &getL();

然而,正如已经指出的,在某些情况下,这个版本并不是您想要的。如果必须在setter方法中执行某些操作,则不希望公开该属性的可修改引用。调用者必须像上面看到的那样调用getter,修改值并调用setter。然而,对于大数据结构,如列表,向量,映射等,这可能是缓慢的,所以你可能想要引入单元素设置:setLAt(int index, OtherClass *value);

到目前为止,所有的答案都告诉你如何正确地做到这一点,但我想我要给你一些更多的细节,为什么你的代码不能工作。正如其他人指出的那样,你的"getter"是按值返回列表。这是(主要是)c++特有的:作为程序员,你必须明确地指定是要通过值传递对象还是通过引用传递对象。其他编程语言,例如Java,将(几乎)总是通过引用传递。假设你像这样赋值一个变量:

MyClass a;
MyClass b = a;

在许多语言中,赋值意味着:使b成为指向a的引用。然后,您将能够调用b上的方法,并且它的行为就像a一样。

另一方面,在c++中,

意味着:"创建第二个对象b,然后将a的所有状态复制到b(忽略MyClass有复制构造函数的可能性,这与本解释无关)。现在对于列表,这意味着每个元素都将被复制到新创建的列表中!(这可能是一个性能问题,除了其他影响)。

另一方面,如果你告诉编译器要引用a:

MyClass& b = a;

那么这个b将确实表现得像一个。没有状态将被复制,并且改变b将改变a

好,现在回到代码示例。在第一个版本中,有以下行:

// Creates an invalid iterator!
std::list<OtherClass *>::iterator itB = inst.getL().begin();

这实际上是一堆不同的东西。对inst.getL()的调用将创建一个新的列表,并将inst的列表成员的所有内容复制到其中。然后,它将获得该副本的迭代器。之后,副本本身将被销毁,迭代器将失效。为什么?因为你没有把列表的副本赋值给任何东西。在c++中,超出作用域的堆栈分配对象(即不是使用new创建的)将被销毁。简单地说,当对象不能再通过其名称访问时,就会发生"超出作用域":

{ // Begin scope
    MyClass o; 
    // Inside the braces, it's possible to refer to o:
    o.doSomething();
} // End scope
o.doSomething() // Will be an error, as o is not "known" anymore

如果丢弃函数的返回值,也会出现这种情况,例如:

inst.getL(); 

这将创建一个列表的副本,然后再次销毁它。

现在,为什么第二个例子可以工作?因为您将列表的副本赋值给临时变量,所以它们保留在作用域中:

std::list<OtherClass *> l = inst.getL();

从"getter"调用获得的临时对象被存储到l中(暂时忽略赋值操作符,RVO等),并且从l获得的所有迭代器现在都将有效,直到l超出作用域。

std::list<OtherClass *>::iterator itB = l.begin(); // valid

所以这可以工作,尽管可能不像您期望的那样:迭代器操作的是列表的副本,而不是实际数据。这有时可能是你想要的——但在你的情况下,你想要一个参考其他答案。

希望这对你有帮助。

将getter更改为返回引用到底层列表

std::list<OtherClass *> &getL()
                        ^

如果没有&符号,每次调用它时,它都会返回列表的副本。因此,itBitE最终是来自不同列表的迭代器。如果这还不够糟糕,那两个列表是临时的,在for循环开始时将被销毁!

如果你使用l,为了匹配这个,你也应该把它变成一个引用变量。

std::list<OtherClass *> &l = inst.getL();

getter返回列表副本。列表在行尾终止,迭代器无效。

您的意思可能是返回一个引用到现有列表:

std::list<OtherClass *> & getL() { return l; }
//                     ^^^

因为每次调用getL()时,都会创建一个新的list,并将返回值中的信息复制到其中。

std::list<OtherClass *>::iterator itE = inst.getL().end();

还需要注意的是,当你这样调用它时,你最终会得到一个临时的list,它会在行尾销毁。使迭代器无效。

尝试更新:

std::list<OtherClass *> getL()

std::list<OtherClass *>& getL()

注意inst.getL().begin();inst.getL().end();每次都返回一个新的list副本