使多态性在C++映射中工作,而不会发生内存泄漏

Getting polymorphism to work in a C++ map without memory leaks?

本文关键字:泄漏 内存 多态性 C++ 映射 工作      更新时间:2023-10-16

自从我在C++编程以来已经有3次了。 我的多态性不起作用:当我将ArmyBaseNavyBase对象添加到map时,map<string, Base> 会将我的和对象转换为Base对象,因此GetDescription()返回一个空字符串,而不是我通过 ArmyBase::SetDescription()NavyBase::SetDescription() 设置的值。 这是极其粗略的伪代码:

class Base
{ protected:
    string STR__Description;  // Pardon my non-standard style
  public:
    virtual
    string GetDescription()
    { return STR__Description;
    }
    void   SetDescription( string str__Description )
    { STR__Description = str__Description;
    }
}
class ArmyBase: public Base
{ public:
    string GetDescription()
    { return STR__Description + " (Army)";
    }
}
class NavyBase: public Base
{ public:
    string GetDescription()
    { return STR__Description + " (Navy)";
    }
}

听起来map<string, Base*>会导致内存泄漏,我宁愿不升级项目中期以使用shared_ptr。 将派生类实例存储在正确"破坏"它们的容器中是否允许我使用指针map进行多态性而不会有内存泄漏的风险?

Base                         base;
ArmyBase                     base_Army;
set<ArmyBase>                set__ArmyBases;
map<string, Base*>::iterator iter_Bases;
map<string, Base*>           map__Bases;
NavyBase                     base_Navy;
set<NavyBase>                set__NavyBases;
...
while( ... )
{   base_Army = ArmyBase();
    base_Navy = NavyBase();
    ...
    set__ArmyBases.insert(                                        base_Army   );
    map__Bases.insert(     pair<string, Base*>( "Boca Raton",    &base_Army ) );
    ...
    set__NavyBases.insert(                                        base_Navy   );
    map__Bases.insert(     pair<string> Base*>( "NAS Pensacola", &base_Navy ) );
    ...
    base = iter_Bases->second;
    std::cout << ..." " << base->GetDescription() << std::endl;
}

map__Bases的期望输出:

Boca Raton ... (Army)
NAS Pensacola ... (Navy)
...

问题是映射条目指向您在堆栈上创建的对象的地址。您可以将这些对象复制到集合中,但不会将副本的地址存储在地图中。当原始对象超出范围时,它们会自动删除,因此您的映射条目将变为无效。

我认为解决问题的最佳方法是将对象分配到堆上并将指针存储在容器中。当然,这要求您小心内存管理。我知道三个选项来处理这个问题:

  1. 手动内存管理:从容器中抹掉对象时将其删除。当然,这是危险且容易出错的,但只要小心,您就可以使其工作。我通常通过将容器包装在另一个将管理对象的类中来做到这一点,即包装器具有 add(Base* base)remove(Base* base) 等方法,它还将在其析构函数中删除容器中的所有对象。当然,您仍然必须注意不要在包装器之外删除此类托管对象。

  2. 智能指针:使用 shared_ptr 或 unique_ptr(取决于所有权语义(并将其存储在容器中。智能指针将负责在从容器中删除对象时删除对象。

  3. 使用自定义分配器。您可以使用分配器对 std 容器进行参数化,分配器应允许容器在从映射中删除对象时删除对象。但是,我从未这样做过,也无法评论这是否是一个好主意。我听说编写自定义分配器很难正确。

我建议 1 和 2。我认为这取决于您实际使用的品味和其他要求,但是如果您使用智能指针选项,请确保选择对您的特定所有权语义进行建模的智能指针(很可能是unique_pointer(。

您应该使用"new"创建和使用对象的指针,以便在 c++ 中使用多态性。您正在经历的情况称为对象切片。

很少的信息可以真正弄清楚你的问题是什么。但是我会从可用的东西中刺伤。

如何检测内存泄漏?如果 ArmyBase/NavyBase 包含非 POD 成员,一个明显的问题是,如果您从 base 中删除,则不会调用其析构函数,因此您需要一个虚拟析构函数来Base

在这种情况下,您建议使用多个不同容器的第二个解决方案几乎肯定是错误的方法。每个派生类型的不同容器首先违背了多态性的整个目的。此外,您的循环根本没有任何意义。 set__ArmyBases/set__NavyBases包含相同的默认构造对象,而map__Bases只包含指向每帧重新初始化的 2 个全局变量 base_Navybase_Army 的指针。

除了代码中大量的拼写错误(这就是为什么您应该始终提供SSCCE的原因(之外,您的代码可以在两个小的更正后工作:

  • 编写一个小的比较函数对象Cmp,该对象按照指向Base的指针来比较说明
  • 将您的set<ArmyBase>set<NavyBase>更改为set<Base*, Cmp>set<Base*, Cmp>

法典:

#include <string>
#include <set>
#include <map>
#include <iostream>
using namespace std;
class Base
{ protected:
    string STR__Description;  // Pardon my non-standard style
  public:
    virtual
    string GetDescription() const
    { return STR__Description;
    }
    void   SetDescription( string str__Description )
    { STR__Description = str__Description;
    }
};
class ArmyBase: public Base
{ public:
    string GetDescription() const
    { return STR__Description + " (Army)";
    }
};
class NavyBase: public Base
{ public:
    string GetDescription() const
    { return STR__Description + " (Navy)";
    }
};
class Cmp
{
public:
    bool operator()(Base const* lhs, Base const* rhs) const
    { return lhs->GetDescription() < rhs->GetDescription(); };
};
int main()
{
    Base                         base;
    ArmyBase                     base_Army;
    set<Base*, Cmp>           set__ArmyBases;
    map<string, Base*>      map__Bases;
    map<string, Base*>::iterator         iter_Bases;
    NavyBase                     base_Navy;
    set<Base*, Cmp>          set__NavyBases;

    base_Army = ArmyBase(); 
    base_Navy = NavyBase();
    set__ArmyBases.insert(                   &base_Army   );
    map__Bases.insert(     pair<string, Base*>( "Boca Raton",    &base_Army ) );

    set__NavyBases.insert(                   &base_Navy   );
    map__Bases.insert(     pair<string, Base*>( "NAS Pensacola", &base_Navy ) );
    for (auto b: map__Bases)
        std::cout << b.first << "..." << b.second->GetDescription() << std::endl;
}

带输出的实时示例。

没有发生对象切片或内存错误。但是,您使用的是指向作用域对象的原始指针(即,当它们超出作用域时,将调用它们的 descructor(。

拥有多态对象容器的推荐方法是使用 std::map<std::string, std:unique_ptr<Base>> 并通过 std::make_unique<NavyBase>("NAS Penscalola") 分配这些容器。

您应该能够使用 Boost 侵入式关联容器获得接近您想要的东西。与标准库容器不同,它们不存储副本,而是存储对对象的引用。这应该允许多态性在从侵入性容器中检索对象时起作用。

从文档中:

侵入

式容器和非侵入式容器之间的主要区别在于,在C++非侵入式容器中存储用户传递的值的副本。

另一方面,侵入式容器不存储传递对象的副本,但它存储对象本身。

   acme_intrusive_list<MyClass> list;
   MyClass myclass;
   list.push_back(myclass);
   //"myclass" object is stored in the list
   assert(&myclass == &list.front());

实际上,这意味着您应该使用一个或多个简单的标准库容器(如list<>(来实际存储派生实例,但将这些实例添加到 Boost.Intrusive 容器中。由于 Boost.Intrusive 容器包含引用,因此如果销毁实际实例,Boost.Intrusive 容器将损坏。