【问题标题】:Is it possible to avoid managing memory manually in this situation in c++?在这种情况下,是否可以避免在 C++ 中手动管理内存?
【发布时间】:2018-06-01 18:26:24
【问题描述】:

我有一个 Storage 类,其中包含 Things 的列表:

#include <iostream>
#include <list>
#include <functional>

class Thing {
    private:
        int id;
        int value = 0;
        static int nextId;
    public:
        Thing() { this->id = Thing::nextId++; };
        int getId() const { return this->id; };
        int getValue() const { return this->value; };
        void add(int n) { this->value += n; };
};
int Thing::nextId = 1;

class Storage {
    private:
        std::list<std::reference_wrapper<Thing>> list;
    public:
        void add(Thing& thing) {
            this->list.push_back(thing);
        }
        Thing& findById(int id) const {
            for (std::list<std::reference_wrapper<Thing>>::const_iterator it = this->list.begin(); it != this->list.end(); ++it) {
                if (it->get().getId() == id) return *it;
            }
            std::cout << "Not found!!\n";
            exit(1);
        }
};

我从一个简单的std::list&lt;Thing&gt; 开始,但随后在插入和检索时会复制所有内容,我不希望这样,因为如果我得到一个副本,更改它不会再反映在原始对象上。在寻找解决方案时,我发现了 std::reference_wrapper on this SO question,但现在我遇到了另一个问题。

现在是使用它们的代码:

void temp(Storage& storage) {
    storage.findById(2).add(1);
    Thing t4; t4.add(50);
    storage.add(t4);
    std::cout << storage.findById(4).getValue() << "\n";
}

void run() {
    Thing t1; t1.add(10);
    Thing t2; t2.add(100);
    Thing t3; t3.add(1000);

    Storage storage;
    storage.add(t3);
    storage.add(t1);
    storage.add(t2);

    temp(storage);

    t2.add(10000);

    std::cout << storage.findById(2).getValue() << "\n";
    std::cout << storage.findById(4).getValue() << "\n";
}

我的main() 只是调用run()。我得到的输出是:

50
10101
Not found!!

虽然我一直在寻找:

50
10101
50

问题

看起来当函数返回时,本地声明的对象t4 不再存在,这是有道理的。我可以通过动态分配它来防止这种情况,使用new,但是我不想手动管理内存......

如何在不删除temp() 函数且无需手动管理内存的情况下修复代码?

如果我只是按照一些建议使用std::list&lt;Thing&gt;,那么t4temp 的问题肯定会不复存在,但会出现另一个问题:例如,代码将不再打印10101 .如果我继续复制东西,我将无法更改存储对象的状态。

【问题讨论】:

  • @Hamsterrific 不要使用new 创建新对象。现代代码使用std::make_uniquestd::make_shared。见stackoverflow.com/questions/106508/…
  • “所以唯一的解决方案就是在这里真正使用指针”不是真的。只需创建一个std::list&lt;Thing&gt; 并让列表拥有Things,问题就消失了
  • 不要混淆指针和动态分配。考虑所有权。谁负责照顾和喂养并最终释放物体。
  • @Hamsterrific 惯用的解决方案是将Thing 存储在Storage 中,然后获取并使用对该元素的引用,因为Storage 确实拥有Things。按照现在的设计,Storage 似乎实际上共享所有权。 Storage 必然需要延长 Things 的生命周期,直到它自己的生命周期结束,但您似乎还希望 run 函数将其保持在本地。在这种情况下,std::list&lt;std::shared_ptr&lt;Thing&gt;&gt; 将满足您的目的。 std::shared_ptr 可以被复制和分配,最后一个副本将在 Thing 被销毁时清理它。
  • 由于未定义行为的概念,C++ 可能是通过反复试验来学习的最糟糕的语言。有时你会做一些非常糟糕的事情,但程序“工作”——即产生预期的结果——但一旦你添加了一段完全不相关的代码,它就会神奇地停止工作。给自己买一本好书,系统地学习。

标签: c++ pointers memory-management reference


【解决方案1】:

存储中的事物的所有者是谁?

您的实际问题是所有权。目前,您的Storage 并不真正包含Things,而是留给Storage 的用户来管理您放入其中的对象的生命周期。这与 std 容器的理念非常矛盾。所有standard C++ containers 都拥有您放入其中的对象,并且容器管理它们的生命周期(例如,您只需在向量上调用v.resize(v.size()-2),最后两个元素就会被销毁)。

为什么要引用?

您已经找到了使容器不拥有实际对象的方法(通过使用reference_wrapper),但没有理由这样做。在一个名为Storage 的类中,我希望它包含对象而不仅仅是引用。此外,这为许多令人讨厌的问题打开了大门,包括未定义的行为。例如这里:

void temp(Storage& storage) {
    storage.findById(2).add(1);
    Thing t4; t4.add(50);
    storage.add(t4);
    std::cout << storage.findById(4).getValue() << "\n";
}

您将t4 的引用存储在storage 中。问题是:t4s 的生命周期只到该函数结束,您最终会得到一个悬空引用。您可以存储这样的引用,但它并没有什么用处,因为您基本上不能对它做任何事情。

引用不是很酷吗?

目前你可以推送t1,修改它,然后观察Storage中的东西的变化,如果你想模仿Java,这可能没问题,但是在c++中,我们习惯于容器在当你推动一些东西(还有一些方法可以在适当的位置创建元素,以防你担心一些无用的临时性)。是的,当然,如果你真的想要,你可以让一个标准容器也保存引用,但是让我们绕个小弯......

谁来收集这些垃圾?

也许考虑一下 Java 是垃圾收集的,而 C++ 有析构函数会有所帮助。在 Java 中,您习惯于在垃圾收集器启动之前浮动引用。在 C++ 中,您必须非常了解对象的生命周期。这听起来可能很糟糕,但事实证明,完全控制对象的生命周期非常有用。

垃圾?什么垃圾?

在现代 C++ 中,您不必担心忘记delete,而是欣赏拥有RAII 的优势。在初始化时获取资源并知道何时调用析构函数可以对基本上任何类型的资源进行自动资源管理,这是垃圾收集器梦寐以求的(想想文件、数据库连接等)。

“如何在不删除 temp() 函数且无需手动管理内存的情况下修复代码?”

对我有很大帮助的一个技巧是:每当我发现自己认为我需要手动管理资源时,我都会停下来问“其他人不能做这些肮脏的事情吗?”。我找不到一个标准的容器来满足我开箱即用的需要,这是非常罕见的。在你的情况下,让std::list 做“脏”的工作。

没有模板就不能是C++吧?

我实际上建议您将Storage 设为模板,如下所示:

template <typename T>
class Storage {
private:
    std::list<T> list;
//....

然后

Storage<Thing> thing_storage;
Storage<int> int_storage;

分别是包含Things 和ints 的Storages。这样,如果您想使用引用或指针进行实​​验,您仍然可以实例化 Storage&lt;reference_wrapper&lt;int&gt;&gt;

我错过了什么吗?...也许是参考资料?

我将无法更改存储对象的状态

鉴于容器拥有对象,您宁愿让用户引用容器中的对象。例如,一个向量将是

auto t = std::vector<int>(10,0);  // 10 element initialized to 0
auto& first_element = t[0];       // reference to first element
first_element = 5;                // first_element is an alias for t[0]
std::cout << t[0];                // i dont want to spoil the fun part

要使您的Storage 使用此功能,您只需让findById 返回一个引用。作为演示:

struct foo {
    private: 
        int data;
    public:
        int& get_ref() { return data;}
        const int& get_ref() const { return data;}
};

auto x = foo();
x.get_ref = 12;

TL;DR

如何避免人工资源管理?让别人为你做这件事,称之为自动资源管理:P

【讨论】:

  • 如果 OP 想要拥有某种共享所有权,可以使用 std::list&lt;std::shared_ptr&lt;Thing&gt;&gt; list;
  • @Eljay 当然可以,但是我没有看到任何提示;)。无论如何,我建议将Storage 设为模板并让用户选择他们想要的内容
  • 非常感谢!!这非常有帮助,+1。事实上,我将不得不重新考虑我的对象之间的所有权。当我问这个问题时,我编写了这些 ThingStorage 类来创建一个最小的可验证示例,但实际上我有更多的类和更多的关系,所以我还不能说我可以只委托所有权控制到一个班级。 (不过我会尽力而为,只是来自Java我不习惯考虑这些事情)。无论如何,我从你的回答中学到了很多:)
  • @Hamsterrific 以防万一我无法说服你,这是一个关于 downsides of garbage collection 的视频;) 只是开玩笑,绝不是对 java 的咆哮,我只是想强调主要区别,如果有帮助很高兴
  • 绝对哈哈哈。再次感谢!很棒的视频:P
【解决方案2】:

t4 是一个临时对象,在退出temp() 时被销毁,而您存储在storage 中的内容成为悬空引用,导致 UB。

目前还不太清楚您要达到什么目的,但如果您想保持 Storage 类与它一样,您应该确保存储在其中的所有引用至少一样长-作为storage 本身生活。您发现这是 STL 容器保留其元素的私有副本的原因之一(其他可能不太重要的原因是,在某些情况下消除了额外的间接性和更好的局部性)。

附:拜托,你能停止写那些this-&gt; 并了解构造函数中的初始化列表吗? >_

【讨论】:

  • 你的“附言”看起来有点粗鲁和居高临下,好像每个人都应该隐含地意识到成员初始化器列表和隐含的this-&gt;。它不考虑那些没有时间接触语言的那些部分的人。相反,解释这些概念或直接访问可以解释它们的来源会更有帮助和尊重。
  • 我不得不说,我同意这个评论。 OP显然已经尽力了,没必要让他觉得自己渺小。
【解决方案3】:

根据我的估计,就您的代码实际上看起来在做什么而言,您肯定使您的代码过于复杂。考虑一下这段代码,它与您的代码所做的所有事情相同,但样板代码少得多,而且对您的使用更安全:

#include<map>
#include<iostream>

int main() {
    std::map<int, int> things;
    int & t1 = things[1];
    int & t2 = things[2];
    int & t3 = things[3];
    t1 = 10;
    t2 = 100;
    t3 = 1000;
    t2++;
    things[4] = 50;
    std::cout << things.at(4) << std::endl;
    t2 += 10000;
    std::cout << things.at(2) << std::endl;
    std::cout << things.at(4) << std::endl;
    things.at(2) -= 75;
    std::cout << things.at(2) << std::endl;
    std::cout << t2 << std::endl;
}

//Output:
50
10101
50
10026
10026

请注意,这里发生了一些有趣的事情:

  • 因为t2 是一个引用,并且插入到映射中不会使引用无效,所以可以修改t2,这些修改将反映在映射本身中,反之亦然。
  • things 拥有插入其中的所有值,并且由于 RAII、std::map 的内置行为以及它遵循的更广泛的 C++ 设计原则,它将被清理。不用担心没有清理对象。

如果您需要保留自动处理 id 递增的行为,独立于最终程序员,我们可以考虑使用以下代码:

#include<map>
#include<iostream>

int & insert(std::map<int, int> & things, int value) {
    static int id = 1;
    int & ret = things[id++] = value;
    return ret;
}

int main() {
    std::map<int, int> things;
    int & t1 = insert(things, 10);
    int & t2 = insert(things, 100);
    int & t3 = insert(things, 1000);
    t2++;
    insert(things, 50);
    std::cout << things.at(4) << std::endl;
    t2 += 10000;
    std::cout << things.at(2) << std::endl;
    std::cout << things.at(4) << std::endl;
    things.at(2) -= 75;
    std::cout << things.at(2) << std::endl;
    std::cout << t2 << std::endl;
}

//Output:
50
10101
50
10026
10026

这些代码 sn-ps 应该让您大致了解该语言的工作原理,以及您需要了解的可能在我编写的代码中不熟悉的原则。我的一般建议是找到一个好的 C++ 资源来学习语言的基础知识,并从中学习。一些不错的资源can be found here.

最后一件事:如果 Thing 的使用对您的代码很重要,因为您需要在地图中保存更多数据,请考虑改为:

#include<map>
#include<iostream>
#include<string>

//Only difference between struct and class is struct sets everything public by default
struct Thing {
    int value;
    double rate;
    std::string name;
    Thing() : Thing(0,0,"") {}
    Thing(int value, double rate, std::string name) : value(value), rate(rate), name(std::move(name)) {}
};

int main() {
    std::map<int, Thing> things;
    Thing & t1 = things[1];
    t1.value = 10;
    t1.rate = 5.7;
    t1.name = "First Object";
    Thing & t2 = things[2];
    t2.value = 15;
    t2.rate = 17.99999;
    t2.name = "Second Object";

    t2.value++;
    std::cout << things.at(2).value << std::endl;
    t1.rate *= things.at(2).rate;
    std::cout << things.at(1).rate << std::endl;

    std::cout << t1.name << "," << things.at(2).name << std::endl;
    things.at(1).rate -= 17;
    std::cout << t1.rate << std::endl;
}

【讨论】:

  • 谢谢,这是对其他答案的一个很好的补充。这很有帮助,但是完全删除这些类有点破坏了目的,因为我构建的是一个反映我的实际代码的最小示例,有更多的类和代码行,而且我认为我无法擦除所有这些在我的真实情况下。无论如何,谢谢,我 +1 :)
  • @Hamsterrific 我在最后添加了一个部分,显示地图中类的使用。
  • Hamsterrific,我敢打赌 map 可以摆脱其中一些代码行。
【解决方案4】:

根据 François Andrieux 和 Eljay 所说的(如果我先到那里的话,我会说的话),我会这样做,如果你想改变你的对象已添加到列表中。所有reference_wrapper 的东西只是传递指针的一种奇特方式。终会流泪。

好的。这是代码(现在根据 OP 的要求进行编辑):

#include <iostream>
#include <list>
#include <memory>

class Thing {
    private:
        int id;
        int value = 0;
        static int nextId;
    public:
        Thing() { this->id = Thing::nextId++; };
        int getId() const { return this->id; };
        int getValue() const { return this->value; };
        void add(int n) { this->value += n; };
};
int Thing::nextId = 1;

class Storage {
    private:
        std::list<std::shared_ptr<Thing>> list;
    public:
        void add(const std::shared_ptr<Thing>& thing) {
            this->list.push_back(thing);
        }
        std::shared_ptr<Thing> findById(int id) const {
            for (std::list<std::shared_ptr<Thing>>::const_iterator it = this->list.begin(); it != this->list.end(); ++it) {
                if (it->get()->getId() == id) return *it;
            }
            std::cout << "Not found!!\n";
            exit(1);
        }
};

void add_another(Storage& storage) {
    storage.findById(2)->add(1);
    std::shared_ptr<Thing> t4 = std::make_shared<Thing> (); t4->add(50);
    storage.add(t4);
    std::cout << storage.findById(4)->getValue() << "\n";
}

int main() {
    std::shared_ptr<Thing> t1 = std::make_shared<Thing> (); t1->add(10);
    std::shared_ptr<Thing> t2 = std::make_shared<Thing> (); t2->add(100);
    std::shared_ptr<Thing> t3 = std::make_shared<Thing> (); t3->add(1000);

    Storage storage;
    storage.add(t3);
    storage.add(t1);
    storage.add(t2);

    add_another(storage);

    t2->add(10000);

    std::cout << storage.findById(2)->getValue() << "\n";
    std::cout << storage.findById(4)->getValue() << "\n";
    return 0;
}

现在的输出是:

50
10101
50

根据需要。在Wandbox 上运行它。

请注意,您在此处所做的实际上是引用计数您的Things。 Things 本身永远不会被复制,并且会在最后一个 shared_ptr 超出范围时消失。只有shared_ptrs 被复制,并且他们设计 被复制,因为这是他们的工作。以这种方式做事几乎与传递引用(或包装的引用)一样有效,而且更安全。刚开始时,很容易忘记引用只是变相的指针。

【讨论】:

  • 谢谢。这看起来真的很好,是对其他答案的一个很好的补充。您能否编辑您的答案以使用 shared_ptrs 使 temp() 函数保持预期行为?
  • 您可以这样做 - 任何人都可以编辑帖子,然后对其进行同行评审。只需在 cmets 框中解释您为什么这样做。因为没看懂,我把它留了下来。 ... ... ... 我会得到支持吗? :)
  • 哦,我知道我可以提出修改建议 :) 但是因为我刚刚听说shared_ptrs,我不知道该怎么做(代码),所以这就是我问的原因你进一步扩大你的答案......
  • 好吧,虽然实际上差不多。我给temp 起了一个更合理的名字。
  • 啊,我明白了,这只是一回事。我想也许会有一些技巧或并发症。非常感谢!! :)
【解决方案5】:

鉴于您的Storage 类不拥有 Thing 对象,并且每个Thing 对象都是唯一计数的,为什么不将Thing* 存储在list 中?

class Storage {
 private:
   std::list<Thing*> list;
 public:
   void add(Thing& thing) {
     this->list.push_back(&thing);
   }
   Thing* findById(int id) const {
     for (auto thing : this->list) {
       if (thing->getId() == id) return thing;
     }
     std::cout << "Not found!!\n";
     return nullptr;
   }
};

编辑:请注意,Storage::findById 现在返回 Thing*,这允许它通过返回 nullptr(而不是 exit(1))优雅地失败。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-06
    • 1970-01-01
    • 2023-03-24
    • 1970-01-01
    • 1970-01-01
    • 2023-03-29
    • 1970-01-01
    相关资源
    最近更新 更多