Пытаюсь понять стек и кучу в C++

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

Вот что я получил до сих пор...

<сильный>1. Куча:

Куча — это общая и динамически выделяемая область. Доступ к нему может получить любая часть нашего процесса с правильным указателем и знанием содержимого (типа и длины). Если мы попытаемся использовать неправильные указатели (тривиальный адрес или указатель на освобождение), это приведет к ошибкам сегментации. Доступ к содержимому большего размера, чем было выделено, также приведет к ошибке сегментации (например, попытка прочитать массив большего размера, чем было выделено). Неиспользуемые области должны быть освобождены «вручную», чтобы избежать утечек памяти.

<сильный>2. Стек:

Стек — это часть памяти, в которой размещаются параметры и локальные переменные. Размер стека ограничен. Стек работает по принципу LIFO (Last In First Out).

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

Пример:

void MyFunction()
{
    int *HeapArray = new int[10];
    // HeapArray is assigned 40 bytes from the heap
    // *HeapArray is assigned 4 bytes from the stack in a 32 bit environment

    int StackArray1[10];
    // StackArray is assigned 40 bytes from the stack

    int StackArray2[20];
    // StackArray is assigned 80 bytes from the stack

    HeapArray = StackArray2;
    // segmentation fault because StackArray it too large

    delete HeapArray;
    // this will deallocate the area assigned in the heap
    // omitting delete would result in memory leaks
    // the pointer itself *HeapArray continues to exist in the stack

    HeapArray = StackArray1;
    // segmentation fault because HeapArray is pointing to deallocated memory

    MyFunction();
    // this will result in a stack overflow

}

Вопросы:

Q1. Определение локальной переменной, которая слишком велика для стека, или наличие бесконечной рекурсивной функции, как в моем примере выше, приводит к ошибке сегментации. Почему это не говорит «переполнение стека»? Это потому, что стек «переполняется в кучу» и создает ошибки сегментации?

Q2. Предполагая пример корзины и пластин, которые я дал для стека: при использовании extern содержимое копируется в новую область поверх последней пластины или создается какой-то указатель?


person Daniel P    schedule 02.06.2013    source источник


Ответы (2)


Код, который вы разместили, полон ошибок, но не только тех, которые вы перечислили в комментариях. При этом простые массивы в стиле C не могут быть назначены. Итак, следующая строка не копирует содержимое массива с правой стороны в массив с левой стороны.

HeapArray = StackArray2;

C и C++ допускают неявное преобразование массива в указатель, указывающий на первый элемент массива; обычно это называется затухание до указателя на первый элемент. Следовательно, приведенный выше оператор заставляет указатель HeapArray указывать на начало StackArray2. Затем, когда вы вызываете delete для HeapArray, вы пытаетесь delete памяти, которая не была newed. Это поведение undefined и приведет к сбою вашей программы (если вам повезет).

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

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


Чтобы ответить на ваши вопросы -

1. Нет гарантии, что переполнение стека или ошибочное удаление всегда будут предсказуемым образом завершаться ошибкой. Это также зависит от используемого компилятора. Если я закомментирую весь код в MyFunction(), за исключением рекурсивного вызова самого себя, g++ 4.8 не выдаст предупреждений и завершится ошибкой сегментации. Однако VS2012 выдает предупреждение

предупреждение C4717: «MyFunction»: рекурсивно на всех путях управления, функция вызовет переполнение стека во время выполнения

и терпит неудачу во время выполнения, заявляя

Необработанное исключение по адресу 0x00062949 в Test.exe: 0xC00000FD: переполнение стека (параметры: 0x00000001, 0x00802FA4)

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

2 - Глобальные переменные (те, к которым вы extern получаете доступ из другой единицы перевода) не сохраняются в стеке. Они хранятся в определенной реализацией области памяти, отличной от стека.

person Praetorian    schedule 02.06.2013
comment
Спасибо за ответ, я совершенно забыл, что вещи, используемые с extern, на самом деле являются глобальными и что они находятся в сегменте данных. - person Daniel P; 02.06.2013
comment
О моем примере кода: мне еще нужно привыкнуть к работе с указателями. Что было бы лучшим способом скопировать массив в этом случае? memcpy? std::copy? - person Daniel P; 02.06.2013
comment
@DanielP В C++ редко когда нужно вызывать memcpy. Вызовите std::copy, и ваша реализация стандартной библиотеки должна вызвать memcpy за кулисами для тривиально копируемых типов. Наконец, в случае массивов вам действительно следует использовать std::array, если вы знаете размер во время компиляции, или std::vector, если вам нужно установить размер во время выполнения. Если вы действительно не можете позволить себе несколько дополнительных указателей, которые приносит с собой std::vector, всегда есть std::unique_ptr<T[]>. - person Praetorian; 03.06.2013

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

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

Еще одно замечание: стек вызовов содержит также записи активации вызовов функций, которые используются для корректного возврата из них.

person Jack    schedule 02.06.2013
comment
Спасибо за ответ, я только что понял, что мой второй вопрос немного странный, поскольку на самом деле он касается глобальных переменных. - person Daniel P; 02.06.2013