оператор = перегрузка для проблем связанного списка

0

У меня возникли проблемы с моей реализацией перегруженного = оператора для связанного списка. Класс List содержит указатель Node* head и struct Node содержащий T* data и Node* next, где T - это имя шаблона. У меня возникают проблемы с тем, что происходит в конце операторской функции, где деструктор (в данном случае обрабатываемый makeEmpty) вызывается дважды к концу оператора, один раз после итерации по списку и создания нового списка те же узлы и один раз после выхода из функции оператора.

Вот реализация makeEmpty:

// Does the work of the Destructor
template <typename T>
void List<T>::makeEmpty() {

cout << endl << endl << "DESTRUCTOR CALLED" << endl << endl;
List<T>::Node* tempPtr = head;

if (head != NULL) {

    List<T>::Node* nextPtr = head->next;

    for(;;) {
        if (tempPtr != NULL) {

            delete tempPtr->data;
            tempPtr = nextPtr;

            if (nextPtr != NULL)
                nextPtr = nextPtr->next;

            /*tempPtr = head->next;
            delete head;
            head = tempPtr;*/
        }

        else break;
    }
}

}

Вот оператор = реализация перегрузки:

// Overloaded to be able to assign one list to another
template <typename T>
List<T> List<T>::operator=(const List& listToCopy) {

List<T> listToReturn;
listToReturn.head = NULL;
List<T>::Node* copyPtr = listToCopy.head;

List<T>::Node* thisPtr = head;

if (copyPtr != NULL && thisPtr != NULL) {
    for(;;) {
        if (copyPtr != NULL) {

            T* toInsert = new T(*copyPtr->data);
            listToReturn.insert(toInsert);

            copyPtr = copyPtr->next;
        }

        else{cout << endl << listToReturn << endl << endl; return listToReturn;}
    }
}
// if right-hand list is NULL, return an empty list
return listToReturn;
}

Я немного отлаживался, и кажется, что во второй раз, когда вызван деструктор, глава уничтожаемого списка содержит нечитаемую память. Я даже не знаю, почему деструктор вызывается дважды внутри оператора, так как единственный список, который нужно уничтожить, - это listToReturn после того, как он был возвращен. (может быть, моя логика где-то испорчена... Я слишком долго думал об этом)

Если вам больше нужна информация о коде, я был бы рад предоставить. Как обычно, это для задания, поэтому я только прошу совета, которые могут помочь мне в правильном направлении. Так спасибо всем за то, что они смотрели и помогали!

  • 1
    Вы и этот спрашивающий действительно должны собраться вместе. И включите полное объявление класса шаблона List в ваш вопрос. Я уже вижу, что у вас есть один уровень динамического распределения, который вам не нужен. Нет никаких причин для того, чтобы List<T>::Node::data был динамическим, если только вы не можете придумать вескую причину для выделения Node без данных (и его нет, кстати).
  • 0
    Вы должны вернуть List<T>& в операторе присваивания
Теги:
linked-list
operator-overloading

1 ответ

4

Вы просили указатели и подсказки, поэтому я даю это:

1) Ненужный элемент динамических данных

Ваш List<T>::Node не нуждается в динамическом члене для базового значения данных. Он должен быть конструктивным из const T& и при реализации С++ 11-совместимой идиомы move-construction, T&&. И оба должны инициализировать next член до nullptr

2) Копировать-конструктор для List<T> является мандатом

В соответствии с Правилами из трех, четырех или пяти, ваш класс имеет динамические члены и, как таковые, должен надлежащим образом управлять им в действии оператора копирования и назначения (или скрывать указанные реализации, но явно это не вариант для вы, поскольку это часть вашего задания).

3) Использовать класс-конструктор класса для перегрузки оператора присваивания

Перегружая оператор присваивания, в котором задействованы динамические распределения (и они здесь, поскольку ваш связанный список Node задает его), в идеале должны использовать конструктор копирования и идиому копирования/свопа для управления жизненным циклом. Это имеет ряд преимуществ, два наиболее важных из которых потенциально ускоряют копирование с помощью оптимизирующего компилятора и безопасность исключений, которая поддерживает объекты в их исходном состоянии.

4) List<T>::operator = override должен возвращать ссылку на текущий объект

Текущий объект, которому присваивается (слева от оператора), должен быть результатом возврата по ссылке. Это должен быть объект, который изменен. Это является неотъемлемой частью стандартной реализации для такого оператора. Вы возвращаете копию, но исходный объект остается нетронутым, тем самым полностью побеждая цель оператора присваивания (то есть сторона lvalue фактически не модифицируется в вашей реализации)

Каждый из них подробно рассматривается ниже:


Ненужный элемент динамических данных

Поскольку он не отправлен, я должен быть немного уверен в том, как выглядит List. Я представляю себе что-то вроде этого:

template<class T>
class List
{
private:
    struct Node
    {
        T* data;
        Node* next;
    };
    Node *head;

    // other members and decls...
};

При этом ваши операции вставки и копирования должны обладать значительными преимуществами для управления динамическими выделениями объектов T которых им не нужно. List<T> должен обязательно иметь цепочку Node; но Node должен владеть фактическим T объектом и нести ответственность за его управление; not List<T>. Рассмотрим это вместо этого:

template<class T>
class List
{
private:
    struct Node
    {
        T data;
        Node* next;

        Node(const T& arg) 
            : data(arg), next()
        {}

        Node(const Node& arg)
            : data(arg.data), next()
        {}

    private:
        // should never be called, and therefore hidden. A
        // C++11 compliant toolchain can use the 'delete' declarator.
        Node& operator =(const Node&);
    };
    Node *head;

    // other members and decls...
};

Теперь, когда требуется новый узел для хранения объекта T (например, при операции вставки), можно сделать следующее:

template<typename T>
void List<T>::someFunction(const T& obj)
{ 
    Node *p = new Node(obj);
    // ...use p somewhere...
}

Копировать-конструктор для List<T> является мандатом

Ваш связанный список, по самой своей природе, управляет динамической цепочкой. Поэтому этот класс должен выполнять операции по копированию и присваиванию, причем последний является частью вашего задания и причиной, по которой вы разместили свой вопрос. Дублирование связанного списка довольно прямолинейно, но некоторые по какой-то причине делают его сложнее, чем кажется. Ниже приведен один из моих предпочтительных способов сделать это:

template<typename T>
List<T>::List(const List<T>& arg)
    : head()
{
    Node **dst = &head;
    const Node* src = arg.head;
    while (src)
    {
        *dst = new Node(*src);     // invoke Node copy-construction
        dst = &(*dst)->next;       // move target to new node next pointer
        src = src->next;           // advance source
    }
}

Это использует простую технику указателя на указатель, чтобы удерживать адрес указателя, заполняемого следующим новым узлом. Первоначально он содержит адрес нашего указателя. С каждым добавленным новым узлом он расширен, чтобы удерживать адрес нового добавленного узла next членом. Поскольку Node(const Node&) уже устанавливает next с nullptr (см. Предыдущий раздел), наш список всегда заканчивается должным образом.


Использовать класс-конструктор класса для перегрузки оператора присваивания

List<T>::operator = override должен возвращать ссылку на текущий объект

Когда у нас есть конструктор с твердой копией, мы можем использовать его для переопределения нашего оператора присваивания. Это делается не так очевидно, но я объясню после кода:

template<typename T>
List<T>& List<T>::operator=(List<T> byval)
{
    std::swap(head, byval.head); // we get his list; he gets ours
    return *this;
}

Я уверен, что вы смотрите на это и думаете: "Да?". Это заслуживает некоторого объяснения. Посмотрите внимательно на параметр byval который передается, и подумайте, почему я назвал его так, как я. Это не традиционная const ссылка, которую вы, вероятно, привыкли видеть. Это значение копии правой части выражения присваивания. Таким образом, для его создания компилятор будет генерировать новый List<T>, вызывая для этого конструктор-копию. Результатом этой копии является временный объект byval у нас есть как наш параметр. Все, что мы делаем, это обменные указатели. Подумайте, что это делает. Обменивая главные указатели, мы берем его список, и он берет наш. Но он был копией оригинальной правой части выражения присваивания, а наш, ну, мы хотим, чтобы он был удален. И это именно то, что произойдет, когда деструктор для byval будет запущен после завершения этой функции.

Короче говоря, он делает такой код:

List<int> lst1, lst2;
lst1.insert(1);
lst2.insert(2);
lst1 = lst2; // <== this line

выполните нашу функцию на отмеченной строке. Эта функция сделает копию lst2, передав ее в оператор присваивания, где lst1 будет обменивать указатели на головку с временной копией. Результатом будет lst1 старый узел будет очищен деструктором byval, а новый список узлов будет правильно установлен.

Для этого есть несколько причин. Во-первых, это делает оператор присваивания исключительным. Если исключение перебрасывается (как правило, исключение выделения памяти, но это не имеет значения), утечка памяти не происходит, а исходный объект lst1 остается в исходном состоянии. Во-вторых, компилятор может полностью исключить это, если он выберет, и условия верны.

Во всяком случае, это некоторые идеи и некоторые моменты ошибок в вашей реализации. Надеюсь, вы найдете их полезными.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню