Это мой код на С++:
#include <iostream>
using namespace std;
typedef struct Node
{
int data;
Node* next;
}Node;
class LinkedList
{
private:
Node* first;
Node* last;
public:
LinkedList() {first = last = NULL;};
LinkedList(int A[], int num);
~LinkedList();
void Display();
void Merge(LinkedList& b);
};
// Create Linked List using Array
LinkedList::LinkedList(int A[], int n)
{
Node* t = new Node;
if (t == NULL)
{
cout << "Failed allocating memory!" << endl;
exit(1);
}
t->data = A[0];
t->next = NULL;
first = last = t;
for (int i = 1; i < n; i++)
{
t = new Node;
if (t == NULL)
{
cout << "Failed allocating memory!" << endl;
exit(1);
}
t->data = A[i];
t->next = NULL;
last->next = t;
last = t;
}
}
// Deleting all Node in Linked List
LinkedList::~LinkedList()
{
Node* p = first;
Node* tmp;
while (p != NULL)
{
tmp = p;
p = p->next;
delete tmp;
}
}
// Displaying Linked List
void LinkedList::Display()
{
Node* tmp;
for (tmp = first; tmp != NULL; tmp = tmp->next)
cout << tmp->data << " ";
cout << endl;
}
// Merge two linked list
void LinkedList::Merge(LinkedList& b)
{
// Store first pointer of Second Linked List
Node* second = b.first;
Node* third = NULL, *tmp = NULL;
// We find first Node outside loop, smaller number, so Third pointer will store the first Node
// Then, we can only use tmp pointer for repeating process inside While loop
if (first->data < second->data)
{
third = tmp = first;
first = first->next;
tmp->next = NULL;
}
else
{
third = tmp = second;
second = second->next;
tmp->next = NULL;
}
// Use while loop for repeating process until First or Second hit NULL
while (first != NULL && second != NULL)
{
// If first Node data is smaller than second Node data
if (first->data < second->data)
{
tmp->next = first;
tmp = first;
first = first->next;
tmp->next = NULL;
}
// If first Node data is greater than second Node data
else
{
tmp->next = second;
tmp = second;
second = second->next;
tmp->next = NULL;
}
}
// Handle remaining Node that hasn't pointed by Last after while loop
if (first != NULL)
tmp->next = first;
else
tmp->next = second;
// Change first to what Third pointing at, which is First Node
first = third;
// Change last pointer from old first linked list to new last Node, after Merge
Node* p = first;
while (p->next != NULL)
{
p = p->next;
}
last = p;
// Destroy second linked list because every Node it's now connect with first linked list
// This also prevent from Double free()
b.last = NULL;
b.first = NULL;
}
int main()
{
int arr1[] = {4, 8, 12, 14, 15, 20, 26, 28, 30};
int arr2[] = {2, 6, 10, 16, 18, 22, 24};
int size1 = sizeof(arr1) / sizeof(arr1[0]);
int size2 = sizeof(arr2) / sizeof(arr2[0]);
LinkedList l1(arr1, size1);
LinkedList l2(arr2, size2);
l1.Display();
l2.Display();
// Merge two linked list, pass l2 as reference
l1.Merge(l2);
l1.Display();
return 0;
}
Я новичок в C++, и в этом коде я практикую объединение двух связанных списков. Это на самом деле отлично работает. Я успешно объединил два связанных списка в отсортированном порядке.
Но есть люди, которые говорят, что я должен следовать Правилу трех в C++. Какие реализуют: Деструктор, Конструктор копирования и Оператор присваивания копирования.
Я смотрел много видео об этом. Я понимаю, что это в основном обработка Shallow Copy, особенно когда мы не хотим, чтобы два разных объекта указывали на один и тот же адрес памяти. Но моя проблема в том, что я до сих пор не знаю, как реализовать это в классе, который работает со связанным списком, как мой код выше.
Кто-то сказал, что в моем main() этот код: l1.Merge(l2); как-то неверен, потому что у меня нет явного конструктора копирования.
И если вы посмотрите на мою функцию Merge() в последней строке, если бы я этого не сделал: b.last = NULL; и b.first = NULL; , которые просто уничтожают указатель второго связанного списка, компилятор выдает мне предупреждение: Double free() обнаружено.
Итак, я думаю, что мой вопрос:
- Как этот код:
l1.Merge(l2);может иметь какое-то отношение к конструктору копирования? - Произошло ли
Double free()из-за того, что я не применяю правило трех? Если да, то как их решить? - Как написать правило трех на основе моего кода? Когда или как их использовать?
- Основываясь на этом Кодексе, что-то не так? Нужна ли мне по-прежнему Правило трех, если моя программа хочет только объединить связанный список?
Спасибо. Я надеюсь, что кто-то может объяснить мне, как будто мне 10 лет. и надеюсь, что кто-то может написать мне код.
Как написать правило трех на основе моего кода? Когда или как их использовать? Каждый раз, когда ваш класс выделяет память, которой он владеет, и использует необработанные указатели, вам нужно будет следовать правилу 3 или 5. — person Kevinkun schedule 16.07.2021
Двойное освобождение() произошло из-за того, что я не применяю правило трех? Да, двойное освобождение является вероятным результатом невыполнения правила трех. — person Kevinkun schedule 16.07.2021
@drescherjm Можете ли вы быть более конкретным? Моего конструктора по умолчанию недостаточно для этого? — person Kevinkun schedule 16.07.2021
@Kevinkun l1 = l2; — и — LinkedList l3 = l1; — Вы пробовали этот простой тест? Ваша программа main, похоже, избегает необходимости проверять копирование и присваивание. Вы увидите, что все разваливается или работает правильно, просто написав эти тестовые строки. — person Kevinkun schedule 16.07.2021
@Kevinkun Кстати, вы должны были внедрить правило трех способов до написания Merge или любой подобной функции. Может быть, вставить и удалить, но все более сложное следует отложить после того, как вы подтвердите, что копирование, присваивание и удаление (правило 3) работают правильно. Прямо сейчас вы добавляете функции поверх неисправного фундамента. В вашем случае, сразу после того, как вы внедрили конструктор для вставки элементов массива в список, следующим делом будет реализация копирования и присваивания (и уничтожения). — person Kevinkun schedule 16.07.2021
Возможная реализация состоит в том, чтобы просто явно удалить оператор копирования и оператор присваивания, если они вам не нужны. Таким образом, если ваш код позже попытается их использовать, вы получите ошибку времени компиляции. И поскольку вы уничтожаете l2 в merge, я бы использовал ссылку &&, чтобы сделать это явным. — person Kevinkun schedule 16.07.2021
Вы должны реализовать деструктор. Вам не нужно реализовывать два других, вы можете сделать их удаленными вместо этого, и в этом случае ваши связанные списки не будут копироваться (и компилятор будет кричать на вас, если вы случайно попытаетесь их скопировать). — person Kevinkun schedule 16.07.2021
@SergeBallesta Можете ли вы объяснить, почему я должен использовать &&? и как его использовать? — person Kevinkun schedule 16.07.2021
stackoverflow.com/questions/12606574/ — person Kevinkun schedule 16.07.2021
Вы просто реализуете конструктор копирования и оператор присваивания копии для итерации входного списка, создания копии каждого узла и вставки их в целевой список. У вас уже есть работающий деструктор. В случае оператора присваивания копии обычно можно использовать идиому копирования-подкачки, чтобы реализовать его с помощью конструктора копирования для избегайте повторений.
Тогда вам неправильно сказали. Ваш код
Merge()не имеет ничего общего с конструктором копирования.Правильный. Поскольку вы перемещаете узлы из входного списка в целевой список, вам нужно сбросить входной список, чтобы он больше не указывал на перемещенные узлы. В противном случае деструктор входного списка попытается их освободить, как и деструктор целевого списка.
Это не имеет к этому никакого отношения.
Не в вашем конкретном примере, поскольку вы не выполняете никаких операций копирования. Но, в целом, несоблюдение правила трех может привести к двойному освобождению, да.
См. код ниже.
Нет. Только когда вы хотите сделать копии списков.
С учетом сказанного, вот реализация, которая включает Правило Трех:
Демо
Вот это да. Спасибо за ваше объяснение! — person Kevinkun; 16.07.2021
Итак, для этой функции:
LinkedList::LinkedList(const LinkedList &src)иLinkedList& LinkedList::operator=(const LinkedList &rhs)это будет выполняться только в том случае, если я выполню копирование из существующего объекта в новый объект в своем коде? Нравитсяl1 = l2илиLinkedList l2(l1)? — person Kevinkun; 16.07.2021Да, это конструктор копирования и оператор присваивания копии, они вызываются только при копировании. И если вы действительно хотите быть тщательным, рассмотрите Правило пяти, которое добавляет конструктор перемещения и оператор присваивания перемещения для кражи ресурсов без создания копий. Семантика перемещения была введена в C++11. — person Kevinkun; 16.07.2021
В этом коде применено несколько сомнительных практик, а также есть ошибка.
Во-первых, баг. Когда вы создаете список, он
newсобирает все свои узлы и отслеживает их с помощью указателей. Когда вы назначаете список другому, вы буквально копируете значения указателя. Мало того, что теперь вы потеряли узлы назначенного списка (потому что вы перезаписали их) и получили утечку памяти (потому что теперь нет указателя, указывающего на выделенные узлы), вы также теперь имеете одинаковые указатели в двух разных списках, указывающие на те же узлы. Когда списки уничтожаются, оба из них пытаютсяdeleteсвоих узлов, и в итоге вы освобождаете одну и ту же память дважды. Юк.Решение этой ошибки заключается в реализации оператора присваивания.
Затем сомнительные практики:
using namespace std;(Почему использование пространства имен std считается плохой практикой? )LinkedListв теле конструктора вместо того, чтобы передавать значения непосредственно их конструктору в списке инициализации. (Что это за странный член двоеточия ( : ) синтаксис в конструкторе?)int[]) — это объявление указателя. Просто знайте это.newне может вернутьNULL! Бесполезно проверять его возвращаемое значение. Если он не может выделить, он просто выдаст исключение.NULL— неподходящая константа для использования. Вы можете использоватьnullptr, это эквивалент C++NULL, за исключением того, что он безопасен для типов.newиdeleteсложно сделать правильно (как вы сами поняли). Возможно, вам будет интересно использоватьstd::unique_ptrилиstd::shared_ptr, чтобы облегчить нагрузку. Они бы поймали жука.Теперь, пожалуйста: не пишите на C++, как на C с классами. Я понимаю, что вы, возможно, не сталкивались со всеми функциями, которые я здесь представил, но в любом случае теперь вы о них знаете
Спасибо за сомнительные практики! Я понимаю с вашим объяснением об утечках памяти. Но какой код создания является ошибкой? Вы имеете в виду, что этот код создания неверен:
LinkedList l1(arr1, size1);иLinkedList l2(arr2, size2);? и какой именно код я должен использовать с оператором присваивания? — person Kevinkun; 16.07.2021Потому что код на самом деле работает отлично. Я обрабатываю утечки памяти с помощью Передачи по ссылке и уничтожаю
l2, назначаяb.first = NULLиb.last=NULL, поэтому при вызове деструктора он не будет дважды освобождаться. Моя проблема в том, что я хочу знать, может быть, есть хорошая практика в моем коде с использованием правила трех? — person Kevinkun; 16.07.2021Для комментария 1: ошибка, о которой я говорил, возникает, когда вы назначаете
LinkedList. Несмотря на то, что вы не определили оператор присваивания, компилятор определил для вас его, который, как оказалось, делает не то, что надо. (Примечание: вы не используете оператор присваивания в опубликованном вами коде. Вот почему в некоторых комментариях предлагалось пометить его какdeleted (кстати, что-то совершенно отличное от освобождения памяти), поэтому он никогда не будет вызываться. Однако ошибка все еще существует, она просто не представился). — person Kevinkun; 16.07.2021Для комментария 2: использование правила трех обычно является хорошей практикой, когда вы не можете следовать правилу нуля. То есть, когда компилятор предоставляет конструкторы и операторы присваивания, они не будут делать то, что вам нужно. Суть здесь в том, что чем меньше кода вы пишете, тем лучше, поэтому позвольте компилятору написать его за вас, если можете. Отредактировано: ответ был частично неправильным, потому что я думал, что вы говорите о какой-то другой части кода. — person Kevinkun; 16.07.2021
Кроме того, вот совет: вы также можете вызвать конструктор для
Node, когда выnewего. Компилятор сгенерировал для вас тот, который делает то, что вы хотите в этом случае (посмотрите агрегатную инициализацию). — person Kevinkun; 16.07.2021@Kevinkun Моя проблема в том, что я хочу знать, может быть, есть хорошая практика в моем коде с использованием правила трех? — Я думаю, вы не видели мой комментарий в основном разделе. Простое задание, и ваш связанный список развалится. Единственный способ предотвратить это — запретить копирование и присваивание во время компиляции. Кроме того, ваша программа
main, которую вы опубликовали, не использовала ваш код в достаточной степени, чтобы обнаружить эти ошибки — она в основном направлена на то, чтобы не находить проблемы, и вы не должны тестировать эти проблемы таким образом. — person Kevinkun; 16.07.2021@PaulMcKenzie Я видел твой комментарий. Итак, если в моей
mainпрограмме нет кода, выполняющего копирование или присваивание, как вы сказали, мне не нужно реализовывать правило трех? или, исходя из моего кода, есть ли какая-то часть, которую я могу реализовать с помощью правила трех? — person Kevinkun; 16.07.2021Конечно. Если вы не застрелитесь, вполне нормально держать пистолет наведенным на ногу. Но вам следует избегать этого. — person Kevinkun; 16.07.2021
Вам необходимо обеспечить это во время компиляции, явно объявив конструктор копирования и оператор присваивания с помощью спецификатора
= delete. Если вы этого не сделаете, то ваш код легко развалится в местах, которых вы никогда не ожидаете. Например, если вы поместите свой классLinkedListв контейнер, который выполняет копирование за кулисами (например,std::vector), у вас будут проблемы. Если вы передаете или возвращаете значение, у вас проблемы. Если компилятор сам должен сделать копию, а вы даже не подозреваете об этом, у вас проблемы. Таким образом, вы либо делаете класс копируемым, либо отключаете копирование. — person Kevinkun; 16.07.2021Не используйте
unique_ptrилиshared_ptrдля реализации связанного списка, если только вам не нравится переполнение стека. — person Kevinkun; 16.07.2021@PaulMcKenzie Хорошо. Но, последний вопрос, Люди сказали, что я должен следовать Правилу Трех для моего кода выше. Дело в том, что я не понимаю, какой код я неправильно реализовал. Потому что все, что я знаю, это то, что код работает идеально. Итак, если есть, может быть, вы можете указать, какую часть моего кода я должен реализовать с помощью конструктора копирования или оператора присваивания? — person Kevinkun; 16.07.2021
@Kevinkun — Что, если я решу использовать ваш класс связанного списка? Вы не представляете, как я буду его использовать. Он должен быть безопасно копируемым и назначаемым, иначе он в основном бесполезен. Опять же, ваша программа
mainсклонна не показывать ошибки. Любой может написать ошибочные классы и написать код, который не показывает скрытых ошибок. Как только кто-то, независимый от исходного программиста, начинает использовать класс, все ошибки выявляются. — person Kevinkun; 16.07.2021Я уже написал код:
l1 = l2;. Только это само по себе показывает ошибки. ИлиLinkedList l3 = l1;илиstd::vector<LinkedList> vL; vL.push_back(l1);— person Kevinkun; 16.07.2021