Уничтожается ли выделение памяти с помощью malloc() внутри цикла при выходе из цикла?

Я новичок в С++. Я пытаюсь реализовать связанные списки и делаю это с помощью классов. Чтобы распределить узлы, я использую 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

Причина, по которой я думаю, что адрес изменяется при выходе из цикла, заключается в том, что:

  1. Вывод печатает другой адрес.

  2. Я все еще могу нормально получить доступ к firstNode.

В чем проблема и как ее решить?

Отладчик показывает segmentation fault в строке return value в функции getValue(), если это актуально, но это только со второй итерации цикла printList() и далее.

Что касается malloc (который вам действительно не следует использовать в C++), то то, что вы выделяете из кучи, имеет время жизни остальной части процесса.

Some programmer dude 01.08.2024 19:49

А учитывая использование malloc и других вещей, если вы действительно серьезно относитесь к изучению C++, тогда, пожалуйста, купите несколько хороших книг по C++. Они могут быть дорогими, но то, чему они вас научат, прослужит очень долго, и оно того стоит.

Some programmer dude 01.08.2024 19:51

что ты собираешься Node* previousNode = previousNode делать? previousNode значение null приводит к сбою godbolt.org/z/bocrzeejh

Alan Birtles 01.08.2024 19:54

@AlanBirtles Нет, это не нулевой указатель, он неопределенный, потому что код инициализируется previousNode своим собственным неинициализированным значением.

Some programmer dude 01.08.2024 19:55

Используйте умные указатели и другие современные возможности C++.

user12002570 01.08.2024 19:56

@Someprogrammerdude да, я имел в виду, что нулевой указатель вызывает сбой, а не то, что он гарантированно будет нулевым

Alan Birtles 01.08.2024 19:57

Динамическое распределение, как вы получаете от malloc и семьи или new, имеет динамическое время жизни. Выделенный объект (объект используется здесь в контексте C++, а не в обычном контексте ООП) будет сохраняться до тех пор, пока он не будет освобожден вручную. Если он не освобожден до того, как будет потеряна последняя ссылка на него, объект подвергся «утечке».

user4581301 01.08.2024 20:13

Вы имели в виду Node * previousNode = firstNode;?

Dúthomhas 01.08.2024 20:19

Пожалуйста, рассмотрите возможность включить предупреждения компилятора.

Drew Dormann 01.08.2024 20:33

практически никогда не следует использовать malloc() в C++. Если вам нужно использовать необработанные указатели, а не интеллектуальные указатели, используйте new и delete.

Barmar 01.08.2024 20:42

Простой ответ на ваш заглавный вопрос — «нет». Когда вы выделяете с помощью malloc(), оно не уничтожается, пока вы не вызовете free().

Barmar 01.08.2024 20:45

Инициализация Node* previousNode = firstNode поможет. А еще std::vector<int> это, я думаю, список желаний.

Stas Simonov 01.08.2024 20:48

Где вы изучаете C++, который не отвечает на этот фундаментальный вопрос? Где вы изучаете C++, где в первую очередь рекомендуется использовать malloc? Ответ - нет, конечно.

john 01.08.2024 21:05

@AlanBirtlesAL не могу поверить, что пропустил это, исправление решило проблему, спасибо

Anshumaan Mishra 02.08.2024 20:05

@Dúthomhas, да, я как-то это пропустил, спасибо

Anshumaan Mishra 02.08.2024 20:06

@john Это не так, но на самом деле я перешел с C на C++ (основы на обоих языках) и (по своей вине) не проверил, почему не рекомендуется использовать malloc в C++, извините

Anshumaan Mishra 02.08.2024 20:07

Хотя в данном случае это не проблема, до недавнего обновления Visual Studio 2022 new был ограничен 2 ГБ, а malloc — нет, поэтому malloc использовался для больших выделений.

rcgldr 02.08.2024 23:29
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
17
119
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ладно, есть над чем подумать конструктивно. У вас есть основная идея, всего несколько вещей, которые помогут прояснить ситуацию в вашей голове.

Узел

«Узел» должен быть очень простым классом. Идея сеттеров и геттеров хороша, но они обычно нужны вам только тогда, когда их изменение требует включения функциональности, влияющей на другие части класса. В случае узла в связанном списке сам узел не должен быть общедоступным, поэтому никакие сеттеры или геттеры действительно не нужны:

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 всегда указывает на последний узел в списке, и все в порядке.

sizeOfNode - это...?

Будьте осторожны и не используйте переменные для хранения значений, которые легко получить от компилятора. Все это лишь запутывает то, что происходит на самом деле. Следовательно:

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).

rcgldr 02.08.2024 23:26

Очень круто, но это выше головы ОП.

Dúthomhas 02.08.2024 23:28

Это правда, но это исходный код (сложный из-за всех шаблонов), который может просмотреть каждый, если у него есть Visual Studio: Program Files (x86)\Microsoft Visual Studio xx.x\VC\include\list. Мне нравится их реализация std::list::splice() и реализация std::list::sort() в VS 2022.

rcgldr 02.08.2024 23:31

Это больше похоже на ответ Проверка кода , а не на Переполнение стека.

Barmar 03.08.2024 00:03

Да, Махмуд Файез прямо ответил на главный вопрос, который ФП сформулировал, потому что он не понимал основной проблемы. Этот ответ не предназначен для ответа на текст заголовка; оно предназначено для того, чтобы помочь ОПу решить его реальную проблему(ы).

Dúthomhas 03.08.2024 00:57

Спасибо за совет, учту эти моменты

Anshumaan Mishra 03.08.2024 21:10
Ответ принят как подходящий

Попробую для вас обобщить:

  1. malloc никогда не будет вызывать конструктор вашего класса, поскольку он в основном используется для C, а не для C++.
  2. для динамического выделения экземпляров классов в C++ вы должны использовать new вместо malloc
  3. выделенная память с помощью malloc или new должна быть освобождена с помощью free или delete соответственно
  4. использование free никогда не вызовет деструктор вашего экземпляра

как только вы закончите работу со своим экземпляром, удалите его.

удачи и добро пожаловать в мир C++

Mahmoud Fayez 03.08.2024 23:00

Другие вопросы по теме