【问题标题】:c++ templates and destructor problemsc++模板和析构函数问题
【发布时间】:2018-09-08 15:00:22
【问题描述】:

如何处理 C++ 中模板类的内存泄漏问题?

在这段代码中,我定义了 4 个模板类:

  • node 类和linked_list 类组成一个双向链表
  • item 类和bag 类只是组成了另一个双向链表

这些模板类旨在处理各种类的对象。 在main函数中,我先创建了一个linked_list<string>和一个bag<int>,一切正常。

但是当我尝试创建bag<linked_list<string>> 时,问题就出现了。 我试图回溯看看发生了什么,我看到在bag 类中的函数push_back 中,调用了linked_list 的析构函数,它删除了输入v 中的所有数据。我不知道为什么会这样。

请注意,我重写了所有类的析构函数,并在主函数中调用了className.~className() 以防止内存泄漏。 它确实可以防止来自ls_1bag_1 的内存泄漏。

我不知道我的代码的哪一部分是错误的。有人可以帮帮我吗?

#include <iostream>
#include <stdlib.h>
#include <string>
using namespace std;


//node and linked_list class make up a doubly linked list
template<class T> class node {
public:
    T value;
    node<T> * next;
    node<T> * previous;
    node<T>() { next = nullptr; previous = nullptr;  }
    node<T>(T v) { value = v; next = nullptr; previous = nullptr; }
    ~node<T>() { delete next; }
};

template<class T> class linked_list { //doubly linked list
public:
    node<T> * head;
    node<T> * tail;
    linked_list<T>() { head = nullptr; tail = nullptr;  }
    ~linked_list<T>() { delete head; }

    void push_front(T v) { //insert an item to the front
        node<T> * p = new node<T>(v);
        p->next = head;
        head = p;

        if (tail == nullptr) {
            tail = p;
        }
    }
};

//item and bag class just make up another doubly linked list
template<class X> class item {
public:
    X value;
    item<X> *next;
    item<X> *previous;
    item<X>(X v) { value = v; next = nullptr; previous = nullptr; }
    ~item<X>() { delete next; }
};

template<class X> class bag { //just another doubly linked list
public:
    item<X> *last;
    item<X> *first;
    int num_items;
    int size() { return num_items; }
    bag() { last = nullptr; first = nullptr; num_items = 0; } 
    ~bag() { delete first; }

    void push_back(X v) {  //insert an item to the back
        item<X> * p = new item<X>(v);

        if (num_items == 0) {
            last = first = p;
        }
        else {
            last->next = p;
            p->previous = last;
            last = p;
        }

        num_items++;
        last->next = nullptr;
    }
};

int main() {
    //When using built-in classes (like strings) as input
    //there's no problem at all
    linked_list<string> ls_1;
    ls_1.push_front("David");
    ls_1.push_front("John");

    bag<int> bag_1;
    bag_1.push_back(1);
    bag_1.push_back(2);

    //Problems arise here when using user defined classes (linked_list) as input
    //I traced back and found out that a destructor has been called
    //that erases all the data in the input. Donno how that happens
    bag<linked_list<string>> bag_string;
    bag_string.push_back(ls_1);

    //These lines are to prevent the memory leaking
    //I overwrote destructors for linked_list and node class
    //otherwise there's still memory leaking
    ls_1.~linked_list(); 
    bag_1.~bag();
    bag_string.~bag();

    _CrtDumpMemoryLeaks();

    getchar();
    getchar();
}

【问题讨论】:

  • 不要害怕给代码一些喘息的空间,这不像我们为每行代码支付 0.05 美元。它有助于阅读。
  • 请注意,我重写了所有类的析构函数,并在主函数中调用了className.~className() 以防止内存泄漏。显式调用析构函数不是你应该如何防止内存泄漏。
  • linked_listnode 中都没有指定previous,所以从任何合理的定义来看,它都不是一个“双向链表”,item 是@987654341 的完全相同@
  • 您的类的 3/5/0 规则被打破(复制构造函数,...)。

标签: c++ templates destructor copy-constructor


【解决方案1】:

实现nodelinked_listitembag 复制构造函数和赋值或将它们声明为已删除。编译器生成的默认版本不进行深度复制,这会导致复制后同一对象出现多个deletes。

阅读the rule of three/five/zero了解完整详情。


有点跑题了,但是创建一个列表节点 delete 它的兄弟姐妹是一个经典的陷阱:对于足够长的列表,它最终会递归调用 ~node&lt;T&gt;() 直到它耗尽堆栈。这就是节点指针不能是智能指针的原因。

解决方法是为节点设置默认析构函数,并让列表循环销毁节点,而不是递归。


您可能还喜欢使用完整的列表节点作为列表的头部,当它为空时指向自身。这完全消除了 nullptr 检查逻辑。

【讨论】:

  • 您的第二点,称为Rule of Three
  • @Drise C++11 中的 5 规则。
  • @Drise 可能。
【解决方案2】:

我试图回溯看看发生了什么,我看到在class bag的函数push_back中,调用了linked_list的析构函数,删除了输入v中的所有数据

是的,这是因为您的bag::push_back() 采用了它的参数按值。这意味着它会创建您在 main 中创建的 ls_1 的副本。您尚未指定如何“复制”列表,因此编译器会自动生成此函数(复制构造函数)。它可以做到这一点,因为你的linked_list 只包含两个指针,所以编译器假设(因为你没有另外告诉它)复制指针是生成linked_list 的副本所必需的。不幸的是,这是不正确的。

您现在有两个管理相同内容的列表:main() 中的原始ls_1push_back() 中的函数参数v - 它们都包含相同的指针。

然后在 item 构造函数中再次发生同样的事情:您制作了列表的本地副本,其中包含与前两个相同的指针。

您现在有多个指向相同数据的列表对象。一旦数据死亡,每个人都会尝试销毁数据。这会导致未定义的行为


要纠正这个问题,您需要弄清楚列表的复制应该如何工作。这(部分)是rule linked in the other comment 的含义:如果您的类的析构函数不是微不足道的(即编译器生成的版本不够用,很可能是因为您需要释放诸如分配的内存之类的资源),您应该/必须始终关心如何处理您的课程被复制。您需要指定(或禁止)可能调用类似复制行为的各种机制(赋值、复制构造函数以及新 C++ 中的移动版本)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2010-10-28
    • 1970-01-01
    • 2013-01-22
    • 2011-11-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多