【问题标题】:Why is this function causing memory leak?为什么这个函数会导致内存泄漏?
【发布时间】:2020-02-04 17:32:37
【问题描述】:

我编写了自己的数据结构(链表)并在下面的代码中使用它。当我用 valgrind 分析程序时,链表的 push 和 push_back 方法都会导致内存泄漏。你能帮我找出为什么会这样吗?

链表:

template <typename T>
struct Node {
  T data;
  Node *next;
};

/**
 * @brief Simple Linked List implementation
 * 
 * @tparam T 
 */
template <typename T> class List{
private:

public:

    Node<T> *head;


    /**
     * @brief Amount of nodes in the list
     * 
     */
    int length;
    /**
     * @brief Construct a new List object
     * 
     */
    List(){
        head = NULL;
        length = 0;
    }

    /**
     * @brief Add new node to the list and increase size
     * 
     * @param val 
     */
    void push(T val){
        Node<T> *n = new Node<T>();   
        n->data = val;             
        n->next = head;        
        head = n;              
        length++;
    }

    /**
     * @brief Add new node to the end of the list and increase size
     * 
     * @param val 
     */
    void push_back(T val) {
        Node<T> *n = new Node<T>();
        Node<T> * temp = head;
        n->data = val;
        n->next = nullptr;
        if (head) {
            while (temp->next != NULL) {
                temp = temp->next;
            }
            temp->next = n;
        } else {
            head = n;
        }
        length++;
    }

    /**
     * @brief Remove the node from the list and decrease size
     * 
     * @return T 
     */
    T pop(){
      if(head) {
        T p = head->data;
        head = head->next;
        length--;
        return p;
      }
    }


/**
 * @brief Get n-th item on the list
 * 
 * @param index Index of the item 
 * @return T 
 */
    T get(int index) {
        T value_to_return;
        Node<T> * temp = head;
        if (index == 0) {
            return head->data;
        }
        for (int i = 0; i < index; i++) {
            temp = temp->next;
            value_to_return = temp->data;
        }
        return value_to_return;
    }
};

导致错误的代码:

/**
 * @file file_reader.h
 * @author Dawid Cyron (software@dawidcyron.me)
 * @brief File with functions used for processing required text files
 * @version 0.1
 * @date 2020-01-26
 * 
 * @copyright Copyright (c) 2020
 * 
 */
#include <vector>
#include "bibliography.h"
#include <fstream>
#include <regex>
#include "list.h"
#include "map.h"


/**
 * @brief Function used for sorting list of bibliography
 * 
 * @param bibliography_list List to sort
 */
void sort_bibliography_list(List<bibliography> bibliography_list) {
    Node <bibliography> * current = bibliography_list.head, * index = NULL;
    bibliography temp;

    if (bibliography_list.head == NULL) {
        return;
    } else {
        while (current != NULL) {
            index = current->next;

            while (index != NULL) {
                if (current->data.author.substr(current->data.author.find(" "), current->data.author.length() - 1) > index->data.author.substr(index->data.author.find(" "), index->data.author.length() - 1)) {
                    temp = current->data;
                    current->data = index->data;
                    index->data = temp;
                }
                index = index->next;
            }
            current = current->next;
        }   
    }
}

/**
 * @brief Funciton used for reading the contents of bibliography file
 * 
 * @param filename Name of the file containing bibliography
 * @return std::vector < bibliography > Vector containing bibliography objects (tag, author, book title), alphabetically sorted by surname
 */
List < bibliography > readBibliographyFile(char * filename) {
    std::ifstream bibliography_file(filename);
    std::string line;
    int line_counter = 0;
    bibliography bib;

    List<bibliography> storage_test;

    if (bibliography_file.is_open()) {
        while (getline(bibliography_file, line)) {
            if (line_counter == 0) {
                if (line == "") {
                    std::cout << "Incorrect data format. Exiting" << std::endl;
                    exit(1);
                }
                bib.label = line;
            } else if (line_counter == 1) {
                if (line == "") {
                    std::cout << "Incorrect data format. Exiting" << std::endl;
                    exit(1);
                }
                bib.author = line;
            } else if (line_counter == 2) {
                if (line == "") {
                    std::cout << "Incorrect data format. Exiting" << std::endl;
                    exit(1);
                }
                bib.book = line;
                storage_test.push_back(bib);
                line_counter = 0;
                // Skip the empty line
                getline(bibliography_file, line);
                continue;
            }
            line_counter++;
        }
    }
    sort_bibliography_list(storage_test);
    return storage_test;
}


/**
 * @brief Function used to load references footer
 * 
 * @param references List of references
 * @param output Reference to the output file
 */
void loadReferenceFooter(List<std::string> references, std::ofstream & output) {
    output << "\nReferences\n \n";;
    for (int i = 0; i < references.length; i++) {
        output << references.get(i);
    }
}

/**
 * @brief Function used to replace cite tags with referenes
 * 
 * @param filename Name of the file containing the text
 * @param data Vector of Bibliography objects (tag, author, book title), has to be sorted by surname
 * @param output_filename Name of the file where the content should be saved
 */
void replaceCites(char * filename, List < bibliography > data, char * output_filename) {
    std::ifstream text_file(filename);
    std::string content;
    content.assign((std::istreambuf_iterator < char > (text_file)), (std::istreambuf_iterator < char > ()));
    //std::map < std::string, int > map;
    std::ofstream output(output_filename);
    int cite_counter = 1;
    List<std::string> references;
    Hashtable<std::string, int> hash_table; 
    HashtableItem<std::string, int> * item;

    for (int i=0; i < data.length; i++) {
        std::smatch matches;
        std::regex regex("\\\\cite\\{" + data.get(i).label + "\\}");
        std::regex_search(content, matches, regex);
        if (!matches.empty()) {
            item = hash_table[data.get(i).label];
            if (item != nullptr) {
                content = std::regex_replace(content, regex, "[" + std::to_string(item->Value()) + "]");
            } else {
                content = std::regex_replace(content, regex, "[" + std::to_string(cite_counter) + "]");
                references.push_back("[" + std::to_string(cite_counter) + "] " + data.get(i).author + ", " + data.get(i).book + "\n");
                hash_table.Add(data.get(i).label, cite_counter);
                cite_counter++;
            }
        }
    }
    output << content << std::endl;
    text_file.close();
    loadReferenceFooter(references, output);
    output.close();
}

据我所知,数据结构应该可以正常工作。我尝试创建一个析构函数,遍历链表的所有节点并逐个删除它们,但这也不起作用(实际上,它甚至导致应用程序无法启动)。

【问题讨论】:

  • pop() 也坏了。根本没有析构函数?全班漏内存,不针对push()push_back()
  • minimal reproducible example 是个好东西。通过将问题代码切回直到只剩下错误,通常很容易看出错误是什么。
  • 如果你的作业不是写一个链表,为什么不直接使用std::list 并完成它呢?地图也是如此。
  • 有一点需要注意的是the Rule of Three。如果你有一个owns a resource 的类,你需要确保它被正确复制或禁用复制,因为你永远不知道什么时候你可能会不小心复制然后在以后炸毁。
  • 重申@JesperJuhl 的一些观点,但并非出于对产品的自豪感。我很懒。调试一个检查并告诉你什么时候出现问题的程序要容易得多。

标签: c++ memory-leaks valgrind


【解决方案1】:

内存泄漏是因为没有析构函数来释放分配的Nodes。提问者指出,他们删除了析构函数,因为当他们有一个析构函数时程序崩溃了。这是应该解决的错误,因为使用析构函数是正确的做法。

解决方案

把析构函数放回去,解决析构函数导致程序崩溃的原因。

解决方案的解决方案

List 违反 the Rule of Three。这意味着当复制 List 并且您有两个对象都指向同一个头 Node 时。这个Node 只能是deleted 一次,两个对象都会在List 析构函数中尝试delete。程序迟早会痛苦地死去。

通常您可以将按值传递替换为按引用传递,然后通过deleteing 特殊成员函数来禁止复制。例如。添加

List(const List & src) = delete;
List& operator=(List src) = delete;

List 并等待编译器开始尖叫List 在删除复制特殊功能时被复制。将所有按值传递替换为按引用传递。

不幸的是

List<bibliography> readBibliographyFile(char * filename)

按值返回。 Return by reference of a local variable is doomed。局部变量超出范围并被销毁,留下对无效对象的引用。这意味着您必须以艰难的方式做到这一点:

实现所有三个特殊成员函数:

// destructor
~List()
{
    while (head)
    {
        Node<T> * temp = head->next;
        delete head;
        head = temp;
    }
}

// copy constructor
List(const List & src): head(NULL), length(src.length)
{
    Node<T> ** destpp = &head; // point at the head.
                               // using a pointer to a pointer because it hides 
                               // the single difference between head and a Node's 
                               // next member: their name. This means we don't need 
                               // any special cases for handling the head. It's just 
                               // another pointer to a Node.
    Node<T> * srcnodep = src.head;
    while (srcnodep) //  while there is a next node in the source list
    {
        *destpp = new Node<T>{srcnodep->data, NULL}; // copy the node and store it at 
                                                     // destination
        destpp = &((*destpp)->next); // advance destination to new node
        srcnodep = srcnodep->next; // advance to next node in source list
    }
}

List& operator=(List src) // pass list by value. It will be copied
{
    length = src.length; // Technically we should swap this, but the copy 
                         // is gonna DIE real soon.
    // swap the node lists. use std::swap if you can.
    Node<T> * temp = head;
    head = src.head; 
    src.head = temp;

    // now this list has the copy's Node list and the copy can go out of scope 
    // and destroy the list that was in this List.

    return *this;
}

注意事项

operator= 正在利用Copy and Swap Idiom。它通常不是最快的解决方案,但它很容易编写并且几乎不可能出错。我从复制和交换开始,只有在分析代码的性能显示我必须这样做时才迁移到更快的东西,而这几乎从未发生过。

insertingremoving 列出项目时,复制构造函数中使用的指针到指针的技巧也非常方便。

了解并理解三法则和its friends。没有它,您将无法编写复杂而高效的 C++ 程序。这个作业可能至少部分是为了强迫你学习它。

【讨论】:

    【解决方案2】:

    “为什么这个函数会导致内存泄漏?” - 简单:您使用new 分配内存,而您永远不会使用delete 释放。

    在现代 C++ 中,您通常应该更喜欢使用智能指针(在这种情况下为 std::unique_ptr)和/或容器类,而不是使用 new/delete 进行手动内存管理。

    【讨论】:

    【解决方案3】:

    如果你可以选择你实现的列表类型,这里有一个类似矢量的版本(与你的链表相比)

    #include <cstdlib>
    #include <iostream>
    
    template <typename T>
    struct list final {
      T* values;
      std::size_t capacity, size;
    
      list() : values{nullptr}, capacity{0u}, size{0u} {}
      ~list() { std::free(values); }
      void push_back(T value) {
        if (size == capacity) {
          capacity = (capacity + 1) * 2;
          if (void* mem = std::realloc(values, capacity * sizeof(T))) {
            values = reinterpret_cast<T*>(mem);
          } else {
            throw std::bad_alloc();
          }
        }
        values[size++] = value;
      }
    
      void pop_back() { --size; }
    };
    
    int main() {
      list<int> integers;
      for (int i = 0; i < 10; ++i) {
        integers.push_back(i);
      }
      for (int i = 0; i < integers.size; ++i) {
        std::cout << integers.values[i] << std::endl;
      }
    }
    

    【讨论】:

      【解决方案4】:

      好的,我发现问题出在哪里,我做了一个非常肮脏的修复,但它确实有效。对于任何告诉我更关心代码的人,我会在完成考试后立即改进它,但是在如此严格的截止日期下,我只需要最快的解决方案。您可以在下面找到它。

      我将此功能添加到列表中:

      void clear() {
          while (head) {
              Node<T> * temp = head->next;
              delete head;
              head = temp;
              length--;
          }
      }
      

      然后在我调用的 replaceCites 函数结束时:

      data.clear();
      references.clear();
      

      【讨论】:

      • 不相关:这只会隐藏违反三规则的规则。如果此代码起作用,那是因为您不走运并且环境隐藏了该错误。在void replaceCites(char * filename, List &lt; bibliography &gt; data, char * output_filename)List &lt; bibliography &gt; data 是按值传递的,导致用于将参数复制到data 中。 List` 缺少复制构造函数,因此编译器生成一个简单复制指针的构造函数,现在您有两个 Lists 指向相同的数据。当你有一个析构函数时,这会爆炸,因为副本和原始节点都试图删除节点。
      • 如果你的老师真的懂 C++ 并且很注意,他们会发现这个错误并给你打分。如果他们没有抓住它,请祈祷他们没有注意,否则你正在被一个无能的人教,以后会为此受苦。
      猜你喜欢
      • 2017-07-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-03
      • 1970-01-01
      • 1970-01-01
      • 2020-10-26
      相关资源
      最近更新 更多