Я новичок в С++. Я пытаюсь реализовать связанные списки и делаю это с помощью классов. Чтобы распределить узлы, я использую malloc()
и выделяю память для каждого узла внутри цикла for
(для второго-второго последнего узла).
Но когда я пытаюсь распечатать значения с помощью функции printList()
, адреса памяти изменяются, и я больше не могу получить доступ к сохраненному ранее значению.
Код, о котором идет речь:
#include <stdio.h>
#include <stdlib.h>
class Node{
private:
// Attributes
int value;
Node* nextNode;
public:
// Empty Constructor
Node(){
value = 0;
}
// Value-and-address Constructor
Node(int localValue, Node* localAddress){
value = localValue;
nextNode = localAddress;
}
// Value Setter
void setValue(int localValue){
value = localValue;
}
// Address Setter
void setAddress(Node* localAddress){
nextNode = localAddress;
}
// Value Getter
int getValue(){
return value;
}
// Next Node Address Getter
Node* getNextNode(){
return nextNode;
}
};
class linkedList{
private:
Node* firstNode;
Node* lastNode;
int sizeOfNode = sizeof(Node);
int lengthOfList = 0;
int iter;
Node* container = (Node*)malloc(sizeof(Node)); // Container for last node
public:
// Constructor
linkedList(int sizeOfList, int* localArray){
// Creation of first Node
firstNode = (Node*)malloc(sizeOfNode);
*firstNode = Node(localArray[0], container);
Node* previousNode = previousNode; // Current Node
lengthOfList ++;
for(int i = 1; i < sizeOfList-1; i++){
iter = i;
Node* currentNode = (Node*)malloc(sizeOfNode);
printf("\n Next Node Location: %p", currentNode);
(*previousNode).setAddress(currentNode);
printf("\n Current Node Location: %p", (*previousNode).getNextNode());
*currentNode = Node(localArray[i], container);
previousNode = currentNode;
lengthOfList ++;
}
// Creation of last Node
iter++;
lastNode = (Node*)malloc(sizeOfNode);
*lastNode = Node(localArray[iter], container);
lengthOfList ++;
}
void printList(){
// First Print
Node* currentNode = firstNode;
for(int i = 0; i < lengthOfList; i++){
printf("\n%d: %d\n", i, (*currentNode).getValue());
printf("Next Node Location: %p\n", (*currentNode).getNextNode());
currentNode = (*currentNode).getNextNode();
}
}
};
int main(){
int passedArray[] = {1, 2, 3, 4};
int sizeOfPassedArray = sizeof(passedArray)/sizeof(int);
linkedList list1 = linkedList(sizeOfPassedArray, passedArray);
list1.printList();
}
При запуске выдается следующее:
Next Node Location: 00000261A1E6F940
Current Node Location: 00000261A1E6F940
Next Node Location: 00000261A1E6F700
Current Node Location: 00000261A1E6F700
0: 1
Next Node Location: 00000261A1E6F780
1: 0
Next Node Location: 0000000000000000
Причина, по которой я думаю, что адрес изменяется при выходе из цикла, заключается в том, что:
Вывод печатает другой адрес.
Я все еще могу нормально получить доступ к firstNode
.
В чем проблема и как ее решить?
Отладчик показывает segmentation fault
в строке return value
в функции getValue()
, если это актуально, но это только со второй итерации цикла printList()
и далее.
А учитывая использование malloc
и других вещей, если вы действительно серьезно относитесь к изучению C++, тогда, пожалуйста, купите несколько хороших книг по C++. Они могут быть дорогими, но то, чему они вас научат, прослужит очень долго, и оно того стоит.
что ты собираешься Node* previousNode = previousNode
делать? previousNode
значение null приводит к сбою godbolt.org/z/bocrzeejh
@AlanBirtles Нет, это не нулевой указатель, он неопределенный, потому что код инициализируется previousNode
своим собственным неинициализированным значением.
Используйте умные указатели и другие современные возможности C++.
@Someprogrammerdude да, я имел в виду, что нулевой указатель вызывает сбой, а не то, что он гарантированно будет нулевым
Динамическое распределение, как вы получаете от malloc
и семьи или new
, имеет динамическое время жизни. Выделенный объект (объект используется здесь в контексте C++, а не в обычном контексте ООП) будет сохраняться до тех пор, пока он не будет освобожден вручную. Если он не освобожден до того, как будет потеряна последняя ссылка на него, объект подвергся «утечке».
Вы имели в виду Node * previousNode = firstNode;
?
Пожалуйста, рассмотрите возможность включить предупреждения компилятора.
практически никогда не следует использовать malloc()
в C++. Если вам нужно использовать необработанные указатели, а не интеллектуальные указатели, используйте new
и delete
.
Простой ответ на ваш заглавный вопрос — «нет». Когда вы выделяете с помощью malloc()
, оно не уничтожается, пока вы не вызовете free()
.
Инициализация Node* previousNode = firstNode
поможет. А еще std::vector<int>
это, я думаю, список желаний.
Где вы изучаете C++, который не отвечает на этот фундаментальный вопрос? Где вы изучаете C++, где в первую очередь рекомендуется использовать malloc
? Ответ - нет, конечно.
@AlanBirtlesAL не могу поверить, что пропустил это, исправление решило проблему, спасибо
@Dúthomhas, да, я как-то это пропустил, спасибо
@john Это не так, но на самом деле я перешел с C на C++ (основы на обоих языках) и (по своей вине) не проверил, почему не рекомендуется использовать malloc в C++, извините
Хотя в данном случае это не проблема, до недавнего обновления Visual Studio 2022 new
был ограничен 2 ГБ, а malloc
— нет, поэтому malloc
использовался для больших выделений.
Ладно, есть над чем подумать конструктивно. У вас есть основная идея, всего несколько вещей, которые помогут прояснить ситуацию в вашей голове.
«Узел» должен быть очень простым классом. Идея сеттеров и геттеров хороша, но они обычно нужны вам только тогда, когда их изменение требует включения функциональности, влияющей на другие части класса. В случае узла в связанном списке сам узел не должен быть общедоступным, поэтому никакие сеттеры или геттеры действительно не нужны:
struct node
{
int value;
node * next;
};
Но если вы действительно хотите добавить сеттеры и геттеры, также рекомендуется быть осторожным со своим словарным запасом. «Адрес» — это значение, хранящееся в указателе. Вы не сохраняете какой-либо старый адрес, вы сохраняете адрес следующего узла, который мы можем сократить до «nextNode» (или нет: «nextNode» тоже подойдет).
В любом случае старайтесь придерживаться одного и того же словарного слова при обращении с предметом:
class node
{
int _value;
node * _next;
public:
int getValue() const { return _value; }
void setValue( int value ) { _value = value; }
node * getNext() const { return _next; }
void setNext( node * next ) { _next = next; }
};
Это делает все простым и легким для отслеживания (это означает, что вам будет легче запутаться при дальнейшем использовании класса).
Обратите также внимание, что я не написал имя класса с заглавной буквы. Можно, конечно, но тогда будьте последовательны: (Node
и LinkedList
) или (node
и linkedList
).
⟶ I personally prefer snake_case: linked_list
, but most people seem to prefer camelCase or PascalCase. Meh.
Кроме того, вы заметите, что я добавил начальное подчеркивание к закрытым переменным-членам. Вам не обязательно это делать, но многие люди любят украшать их таким образом — это помогает вам (программисту) помнить, что это частное поле, и доступ к нему следует осуществлять только теми методами, у которых есть веские основания для прикосновения. их напрямую. Другие распространенные способы их украшения включают m_value
и value_
.
⟶ Keep in mind that leading underscores are not valid for global object names.
Двусвязный список бывает двух видов:
head
игнорируется, а его следующий указатель указывает на фактический первый узел в списке. Это происходит за счет использования большего пространства для каждого списка (за счет двух узлов с неиспользуемыми значениями), но значительно упрощает жизнь при написании кода для вставки и удаления узлов из списка.У вас односвязный список, но ситуация меняется лишь немного. Голова по-прежнему работает так же, как и в двусвязном списке (какой бы вариант вы ни выбрали), но хвост теперь полезен только для быстрого добавления данных в список и, следовательно, всегда должен быть просто указателем на фактический последний узел. в списке независимо от того, является ли head указателем.
⟶ Many singly-linked lists do not have a tail pointer, since their needs only require adding a node to the head and rarely the tail. Again, use case matters!
Поскольку указатель хвоста всегда должен указывать на фактический хвостовой узел списка, выделять container
нет необходимости. Опять же, словарный запас здесь имеет значение. Ожидается, что слово «контейнер» будет относиться к структуре, управляющей всей коллекцией. (Вы уже назвали этот «linkedList».)
Поскольку указатель tail
у вас уже есть, вам не нужен еще один волшебный пустой узел для управления хвостом. Просто убедитесь, что tail
всегда указывает на последний узел в списке, и все в порядке.
Будьте осторожны и не используйте переменные для хранения значений, которые легко получить от компилятора. Все это лишь запутывает то, что происходит на самом деле. Следовательно:
int sizeOfNode = sizeof(Node);
совершенно ненужно. Если вам когда-нибудь понадобится размер объекта, просто используйте sizeof
. Использование sizeOfNode
заставляет читателя задуматься, не происходит ли что-то необычное, чего он или она не может сразу увидеть, поскольку вы прилагаете усилия для сохранения значения, разумно предположить, что для этого есть причина, и единственной причиной будет если есть место, где sizeOfNode
не равно sizeof(Node)
. Это не так, поэтому это добавляет сложности без всякой причины.
Слово «iter» относится к итератору, который является указателем (или притворяется им). Кроме того, сама переменная используется только в функции. Опять же, не объявляйте ничего, что не является строго необходимым для хранения данных в определении класса. Подобные переменные следует объявлять в функции, в которой они используются, как можно ближе к тому месту, где они используются.
Для простого целочисленного индекса в массиве часто используются такие имена переменных, как i
и j
и k
и n
.
Ваш пересмотренный класс связанного списка может выглядеть так:
class linkedList
{
node * _firstNode; // or head or headNode etc
node * _lastNode; // or tail or tailNode etc
int _lengthOfList;
public:
linkedList() : _firstNode{nullptr}, _lastNode{nullptr}, _lengthOfList{0} { }
linkedList( int length, int * array )
: _firstNode{nullptr}, _lastNode{nullptr}, _lengthOfList{0}
{
...
}
int getLength() const { return _lengthOfList; }
};
Обратите внимание, что у нас есть конструктор по умолчанию (который просто дает вам пустой список) и конструктор, который принимает массив целых чисел для построения списка. Символ :
указывает на «список инициализаторов членов», который позволяет нам легко присвоить всем объектам-членам класса начальное значение, что и должно быть. (В противном случае вам пришлось бы присвоить начальные значения непосредственно в теле конструкции, что тоже работает, но все равно, что будет плавать в вашей лодке.)
linkedList()
{
_firstNode = nullptr;
_lastNode = nullptr;
_lengthOfList = 0;
}
Обратите также внимание на добавление метода получения для получения текущей длины списка. Сеттера нет. Для изменения длины необходимо добавить или удалить узлы.
⟶ Standard C++ containers call this “size”. So you could name _lengthOfList
just _size
and the getter int size() const { return _size; }
and everyone who reads your code will understand that it refers to the number of elements in your linked list.
Прежде чем беспокоиться о конструкторе связанного списка массива ⟶, у вас должен быть метод простого добавления одного узла в конец списка. Что-то вроде:
class linkedList
{
public:
...
void append( int value )
{
...
}
...
};
Этот метод должен делать не что иное, как прикреплять новый узел к концу списка. Это необходимо:
malloc()
, либо еще лучше, поскольку это C++, используя new
)_value
значение аргумента_nextNode
вновь выделенного узла на nullptr
_lastNode->_nextNode
для вашего нового узла (таким образом связывая ваш узел с концом списка)_lastNode
так, чтобы он указывал на новый узел (чтобы _lastNode
продолжал указывать на фактический последний узел в списке)_lengthOfList
(чтобы сохранить точность)Подобные вещи следует нарисовать на листе бумаги. Вам следует использовать бумагу и карандаш каждый раз, когда вы что-то делаете, чтобы связать или разъединить узлы в своем списке.
Предостережения для вашего односвязного списка:
_firstNode
может быть nullptr
. Если это так, вы должны установить как его значение, так и значение _lastNode
, чтобы они указывали на новый узел._firstNode
нет nullptr
, вам нужно только обновить _lastNode
.Опять же, рисование этого на бумаге поможет вам увидеть эти проблемы!
Теперь, когда у вас есть функция для добавления узлов, написать конструктор связанного списка массива ⟶ очень легко:
linkedList( int length, int * array )
: _firstNode{nullptr}, _lastNode{nullptr}, _lengthOfList{0}
{
for (int n = 0; n < length; n++)
append( array[n] );
}
Это было на удивление легко, не так ли?
Когда вы научитесь использовать C++ и разберетесь с подобными структурами, жизнь станет проще. Помните, всегда можно построить себе модель того, что вы пытаетесь сделать, будь то скрепки или карандаш и бумага (или что-то еще, что кажется полезным).
В данном случае самыми основными блоками связанного списка являются:
Все остальное — просто сладкий сахар, который поможет вам сделать эти вещи.
Visual Studio реализует std::list путем инициализации списка одним узлом, который указывает на фактические первый и последний узлы списка, и этот узел вместе с двусвязным списком является циклическим. В сборке выпуска нет проверок итераторов, поэтому программа может выполнить резервное копирование из std::list::begin() или перейти из std::list::end() и получить доступ к этому внутреннему узлу (его данные будут ноль или nullptr).
Очень круто, но это выше головы ОП.
Это правда, но это исходный код (сложный из-за всех шаблонов), который может просмотреть каждый, если у него есть Visual Studio: Program Files (x86)\Microsoft Visual Studio xx.x\VC\include\list. Мне нравится их реализация std::list::splice() и реализация std::list::sort() в VS 2022.
Это больше похоже на ответ Проверка кода , а не на Переполнение стека.
Да, Махмуд Файез прямо ответил на главный вопрос, который ФП сформулировал, потому что он не понимал основной проблемы. Этот ответ не предназначен для ответа на текст заголовка; оно предназначено для того, чтобы помочь ОПу решить его реальную проблему(ы).
Спасибо за совет, учту эти моменты
Попробую для вас обобщить:
malloc
никогда не будет вызывать конструктор вашего класса, поскольку он в основном используется для C, а не для C++.new
вместо mallocmalloc
или new
должна быть освобождена с помощью free
или delete
соответственноfree
никогда не вызовет деструктор вашего экземпляракак только вы закончите работу со своим экземпляром, удалите его.
удачи и добро пожаловать в мир C++
Что касается
malloc
(который вам действительно не следует использовать в C++), то то, что вы выделяете из кучи, имеет время жизни остальной части процесса.