20

我正在观察 std::map::clear() 的奇怪行为。此方法应该在调用时调用元素的析构函数,但是在调用 clear() 后仍然可以访问内存。

例如:

struct A
{
  ~A() { x = 0; }
  int x;
};

int main( void )
{
  std::map< int, A * > my_map;
  A *a = new A();
  a->x = 5;
  my_map.insert( std::make_pair< int, *A >( 0, a ) );

  // addresses will be the same, will print 5
  std::cout << a << " " << my_map[0] << " " << my_map[0]->x << std::endl;

  my_map.clear();

  // will be 0
  std::cout << a->x << std::endl;

  return 0;
}

问题是,为什么变量a在 map::clear() 调用其析构函数后仍然可以访问?delete a;调用后我需要写my_map.clear()还是覆盖内容是否安全a

提前感谢您的帮助,斯内格

4

6 回答 6

23

如果您将指针存储在地图(或列表或类似的东西)上,有责任删除指针,因为地图不知道它们是否是用新创建的。如果您不使用指针,则 clear 函数仅调用析构函数。

哦,还有一件事:调用析构函数(甚至调用删除)并不意味着不能再访问内存。这仅意味着如果您这样做,您将访问垃圾。

于 2009-02-20T10:53:24.197 回答
19

std::map 不管理指针值指向的内存 - 由您自己完成。如果您不想使用智能指针,您可以编写一个通用的 free & clear 函数,如下所示:

template <typename M> void FreeClear( M & amap ) 
    for ( typename M::iterator it = amap.begin(); it != amap.end(); ++it ) {
        delete it->second;
    }
    amap.clear();
}

并使用它:

std::map< int, A * > my_map;
// populate
FreeClear( my_map )

;

于 2009-02-20T11:07:07.240 回答
4

那是因为map.clear()调用映射中包含的数据的析构函数,在你的情况下,指向a. 而这无济于事。

您可能希望在映射中放置某种智能指针a,以便自动回收所占用的内存。

顺便说一句,你为什么把模板参数放在调用中make_pair?模板参数推导在这里应该做得很好。

于 2009-02-20T10:56:14.923 回答
1

当您释放一块堆内存时,其内容不会归零。它们仅可用于再次分配。当然你应该考虑内存不可访问,因为访问未分配内存的影响是不确定的。

实际上阻​​止对内存页面的访问发生在较低级别,而标准库不会这样做。

当你用new分配内存时,你需要自己删除它,除非你使用智能指针。

于 2009-02-20T11:09:03.973 回答
0

任何容器都存储您的对象类型并调用相应的构造函数:每个节点的内部代码可能类似于:

__NodePtr
{
    *next;
    __Ty    Val;
}

当你分配它时,它是通过基于类型构造 val 然后链接来实现的。类似于:

_Ty _Val = _Ty();
_Myhead = _Buynode();
_Construct_n(_Count, _Val);

当您删除它时,它会调用相应的析构函数。

当您存储引用(指针)时,它不会调用任何构造函数,也不会破坏。

于 2009-02-20T11:18:17.103 回答
0

在过去 2 个月的饮食、睡眠和呼吸图上度过,我有一个建议。尽可能让地图分配它自己的数据。由于您在此处强调的原因,它更清洁了。

还有一些微妙的优势,例如如果您将数据从文件或套接字复制到地图的数据,只要节点存在,数据存储就会存在,因为当地图调用 malloc() 分配节点时,它会分配内存对于密钥和数据。(又名 map[key].first 和 map[key].second)

这允许您使用赋值运算符而不是 memcpy(),并且需要对 malloc() 的调用减少 1 次 - 您所做的调用。

IC_CDR CDR, *pThisCDRLeafData;  // a large struct{}

    while(1 == fread(CDR, sizeof(CDR), 1, fp))  {
    if(feof(fp)) {
        printf("\nfread() failure in %s at line %i", __FILE__, __LINE__);
    }
    cdrMap[CDR.iGUID] = CDR; // no need for a malloc() and memcpy() here    
    pThisCDRLeafData  = &cdrMap[CDR.iGUID]; // pointer to tree node's data

需要注意的一些注意事项值得在这里指出。

  1. 不要在添加树节点的代码行中调用 malloc() 或 new,因为您对 malloc() 的调用将返回一个指针,然后地图对 malloc() 的调用已分配一个位置来保存 malloc() 的返回.
  2. 在调试模式下,尝试释放()你的内存时会遇到类似的问题。这两个对我来说似乎都是编译器问题,但至少在 MSVC 2012 中,它们存在并且是一个严重的问题。
  3. 考虑一下在哪里“锚定”您的地图。IE:声明它们的地方。您不希望它们错误地超出范围。main{} 始终是安全的。

    INT _tmain(INT argc, char* argv[])    {
    IC_CDR      CDR, *pThisCDRLeafData=NULL;
    CDR_MAP     cdrMap;
    CUST_MAP    custMap;
    KCI_MAP     kciMap;
    
  4. 我很幸运,很高兴有一个关键映射分配一个结构作为节点数据,并让该结构“锚定”一个映射。虽然 C++ 已经放弃了匿名结构(一个必须撤销的可怕决定),但作为第一个结构成员的映射就像匿名结构一样工作。非常光滑和干净,尺寸效应为零。在函数调用中传递指向叶拥有的结构的指针,或按值传递结构的副本,两者都工作得很好。强烈推荐。

  5. 您可以捕获 .insert 的返回值,以确定它是否在该键上找到了现有节点,或者创建了一个新节点。(代码见#12)使用下标表示法不允许这样做。选择 .insert 并坚持下去可能会更好,特别是因为 [] 符号不适用于多图。(这样做是没有意义的,因为没有“a”键,而是多重映射中具有相同值的一系列键)
  6. 您可以而且应该捕获 .erase 和 .empty() 的返回值(是的,有些东西是函数,需要 () 而有些,比如 .erase,则不需要)
  7. 您可以使用 .first 和 .second 获取任何映射节点的键值和数据值,按照惯例,所有映射都使用它们分别返回键和数据
  8. 为自己节省大量的混乱和打字,并为您的地图使用 typedef,就像这样。

    typedef map<ULLNG, IC_CDR>      CDR_MAP;    
    typedef map<ULLNG, pIC_CDR>     CALL_MAP;   
    typedef struct {
        CALL_MAP    callMap;
        ULNG        Knt;         
        DBL         BurnRateSec; 
        DBL         DeciCents;   
        ULLNG       tThen;       
        DBL         OldKCIKey;   
    } CUST_SUM, *pCUST_SUM;
    typedef map<ULNG,CUST_SUM>  CUST_MAP, CUST_MAP;  
    typedef map<DBL,pCUST_SUM>  KCI_MAP;
    
  9. 使用 typedef 和 & 运算符传递对映射的引用,如

    ULNG DestroyCustomer_callMap(CUST_SUM Summary, CDR_MAP& cdrMap, KCI_MAP& kciMap)

  10. 对迭代器使用“auto”变量类型。编译器将从 for() 循环体的其余部分中指定的类型确定要使用哪种映射 typedef。它是如此的干净,几乎是魔法!

    for(auto itr = Summary.callMap.begin(); itr!= Summary.callMap.end(); ++itr) {

  11. 定义一些清单常量以使 .erase 和 .empty() 的返回更有意义。

    if(ERASE_SUCCESSFUL == cdrMap.erase (itr->second->iGUID)) {

  12. 鉴于“智能指针”实际上只是保持引用计数,请记住,您始终可以保持自己的引用计数,这可能以一种更清晰、更明显的方式。将它与上面的#5 和#10 结合起来,您可以编写一些像这样的漂亮干净的代码。

    #define Pear(x,y) std::make_pair(x,y) //  some macro magic
    
    auto res = pSumStruct->callMap.insert(Pear(pCDR->iGUID,pCDR));
    if ( ! res.second ) {
        pCDR->RefKnt=2;
    } else {
        pCDR->RefKnt=1;
        pSumStruct->Knt += 1;
    }
    
  13. 使用指针挂在为自己分配所有内容的映射节点上,IE:没有指向用户 malloc()ed 对象的用户指针,运行良好,可能更有效,并且可用于改变节点的数据而没有副作用在我的经验中。

  14. 在同一主题上,可以非常有效地使用这样的指针来保存节点的状态,pThisCDRLeafData如上所示。将其传递给改变/更改特定节点数据的函数比传递对映射的引用更干净,并且返回节点所需的键pThisCDRLeafData指向。

  15. 迭代器不是魔术。当您在地图上导航以获取值时,它们既昂贵又缓慢。对于包含一百万个值的映射,您可以每秒大约 2000 万次基于键读取节点。使用迭代器可能会慢 1000 倍。

我认为现在涵盖了它。如果有任何变化或有其他见解要分享,将会更新。我特别喜欢将 STL 与 C 代码一起使用。IE:在任何地方都看不到一堂课。它们只是在我工作的环境中没有意义,这不是问题。祝你好运。

于 2013-08-08T19:07:16.757 回答