在c ++这种情况下是否可以避免手动管理内存?

时间:2018-06-01 18:26:25

标签: c++ pointers memory-management reference

我有一个Map类,其中包含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<Thing> on this SO question,但现在我遇到了另一个问题。

现在使用它们的代码:

std::reference_wrapper

我的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<Thing>t4的问题肯定会不复存在,但会出现另一个问题:代码赢得了&#t; t例如,再打印temp。如果我一直在复制东西,我就无法改变存储对象的状态。

5 个答案:

答案 0 :(得分:7)

谁是存储中Thing的所有者?

您的实际问题是所有权。目前,您的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的引用。问题是:t4的生命周期只有在该函数结束时才会出现悬空参考。你可以存储这样的引用,但它不是很有用,因为你基本上不允许用它做任何事情。

Aren引用了一件很酷的事情?

目前您可以推送t1,修改它,然后在Storage中观察对thingy的更改,如果您想模仿Java,这可能没问题,但在c ++中我们习惯于容器当你推东西时制作一个副本(还有一些方法来创建元素到位,以防你担心一些无用的临时工)。是的,当然,如果你真的想要你可以制作一个标准的容器也可以容纳参考,但是让我们绕道而行......

谁收集了所有垃圾?

在C ++具有析构函数时,考虑Java是垃圾收集可能会有所帮助。在Java中,您习惯于引用浮动,直到垃圾收集器启动。在C ++中,您必须非常了解对象的生命周期。这可能听起来很糟糕,但实际上对于完全控制对象的生命周期非常有用。

<强>垃圾?什么垃圾?

在现代C ++中,你不应该忘记忘记delete,而是要体会到RAII的优点。获取初始化资源并了解析构函数何时被调用可以获得基本上任何类型资源的自动资源管理,垃圾收集器只能梦想(想想文件,数据库连接等)。

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

这对我有很大帮助的一个技巧是:每当我发现自己需要手动管理资源时,我就会停下来询问“其他人不能做脏事吗?”#34;。真的非常罕见,我找不到一个标准容器,它完全符合我的需要。在你的情况下,让std::list做&#34;脏&#34;工作。

如果没有模板,就不能成为C ++,对吗?

我实际上建议你将Storage作为模板,沿着以下方式:

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

然后

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

分别是包含StorageThing的{​​{1}}个。这样,如果您想要使用引用或指针进行实​​验,您仍然可以实例化int

我错过了什么吗?......可能是参考文献?

  

我无法改变存储对象的状态

鉴于容器拥有该对象,您宁愿让用户对容器中的对象进行引用。例如,使用

的向量
Storage<reference_wrapper<int>>

要使您的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

<强> TL; DR

如何避免手动资源管理?让别人为你做,并称之为自动资源管理:P

答案 1 :(得分:1)

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

目前还不是很清楚你想要实现的目标,但是如果你想让Storage类与它保持一致,你应该确保存储在其中的所有引用都在至少与storage本身一样长寿。您发现这是STL容器保留其元素的私有副本的原因之一(其他可能不那么重要,在某些情况下消除了额外的间接和更好的位置)。

P.S。并且,请你停止编写那些this->并了解构造函数中的初始化列表吗? &GT; _&LT;

答案 2 :(得分:1)

根据我的估计,就您的代码实际看起来要做的事情而言,您的代码肯定过于复杂。考虑一下这段代码,它可以完成您的代码所做的所有相同的事情,但使用的样本代码少得多,而且使用方式更加安全:

#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

这些代码段应该让您对语言的工作方式有一个体面的认识,以及您需要了解的代码中可能不熟悉的原则。我的一般建议是找到一个很好的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;
}

答案 3 :(得分:1)

基于FrançoisAndrieux和Eljay所说的(以及我会说的,如果我先到达那里),这就是我要做的方式, if 你想要改变对象你已添加到列表中。所有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上运行。

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

答案 4 :(得分:0)

鉴于您的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))来优雅地失败。