Об управлении памятью, повреждении кучи и C++

Итак, мне нужна помощь. Я работаю над проектом на C++. Однако я думаю, что мне как-то удалось испортить свою кучу. Это основано на том факте, что я добавил std::string к классу и присвоил ему значение из другого std::string :

std::string hello = "Hello, world.\n";
/* exampleString = "Hello, world.\n" would work fine. */
exampleString = hello;

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

Тем не менее, я не могу себе представить подобные вещи, так что я подумал, что выкину их там. Я нахожусь в системе Linux valgrind, и, не зная полностью, что я делаю, действительно сообщил, что std::string деструктор был недействительным. Я должен признать, что получил термин «повреждение кучи» из поиска Google; Любые статьи общего назначения на подобные темы также будут оценены.

(Ранее rm -rf ProjectDir сделайте еще раз на C#: D)

РЕДАКТИРОВАТЬ: Я не разъяснил это, но я прошу совета по диагностике такого рода проблем с памятью. Я знаю, что std::string верен, так что это то, что я сделал (или ошибка, но с Select нет проблем). Я уверен, что смогу проверить написанный мной код, и вы, очень умные люди, сразу заметите проблему, но я хочу добавить такой вид анализа кода в свой «набор инструментов», так сказать.

Ответов (12)

Решение

Это относительно дешевые механизмы для возможного решения проблемы:

  1. Следите за моим вопросом о повреждении кучи - я обновляю ответы, когда они вытряхиваются. Первым было уравновешивание new[]и delete[], но вы уже это делаете.
  2. Дайте valgrind больше возможностей; это отличный инструмент, и я бы хотел, чтобы он был доступен под Windows. Я замедляю вашу программу только примерно наполовину, что довольно хорошо по сравнению с эквивалентами Windows.
  3. Подумайте об использовании Google Performance Tools в качестве замены malloc / new.
  4. Вы очистили все свои объектные файлы и начали заново? Возможно, ваш make-файл ... "неоптимальный"
  5. В assert()вашем коде недостаточно информации. Откуда я знаю это, не видя этого? Как и в случае с зубной нитью, assert()в коде никого не хватает. Добавьте функцию проверки для своих объектов и вызовите ее при запуске и завершении метода.
  6. Вы компилируете -wall ? Если нет, сделайте это.
  7. Найдите себе инструмент для удаления ворса, например PC-Lint . Небольшое приложение, подобное вашему, может поместиться на демонстрационной странице PC-lint , а это значит, что вам не нужно покупать!
  8. Убедитесь, что вы удаляете указатели NULL после их удаления. Никому не нравится свисающий указатель. Тот же концерт с объявленными, но нераспределенными указателями.
  9. Прекратите использовать массивы. Вместо этого используйте вектор .
  10. Не используйте необработанные указатели. Используйте умный указатель . Не используйте auto_ptr! Это ... удивительно; его семантика очень странная. Вместо этого выберите один из интеллектуальных указателей Boost или что-то из библиотеки Loki .

Ваш код, как я вижу, не содержит ошибок. Как было сказано, требуется больше контекста.

Если вы еще не пробовали, установите gdb (отладчик gcc) и скомпилируйте программу с помощью -g. Это будет компилироваться в отладочные символы, которые может использовать gdb. После того, как вы установили gdb, запустите его с программой (gdb <your_program>). Это полезный чит-код для использования gdb.

Установите точку останова для функции, вызывающей ошибку, и посмотрите, каково значение exampleString. Также сделайте то же самое для любого параметра, который вы передаете в exampleString. Это должно хотя бы сказать вам, действительны ли std::strings.

Я нашел ответ из этой статьи хорошим руководством по указателям.

Запустите Purify.

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

Он работает на уровне машинного кода, поэтому вам даже не нужно иметь исходный код.

Один из самых приятных конференц-звонков с поставщиками, в котором я когда-либо участвовал, был, когда Purify обнаружили утечку памяти в их коде, и мы смогли спросить: «Возможно ли, что вы не освобождаете память в своей функции foo ()», и услышали изумление в их голосах.

Они думали, что мы отлаживаем богов, но затем мы открыли им секрет, чтобы они могли запустить Purify до того, как нам пришлось использовать их код. :-)

http://www-306.ibm.com/software/awdtools/purify/unix/

(Это довольно дорого, но у них есть бесплатная пробная версия)

Кроме того, я исправил проблему std::string. Как? Заменив его вектором, скомпилировав, а затем снова заменив строку. Там постоянно происходили сбои, и это исправлено, хотя ... не могло. Там есть что-то противное, и я не знаю, что именно.

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

Некоторые места для начала:

Если вы работаете в Windows и используете Visual C++ 6 (я надеюсь, что никто не использует его в наши дни), реализация std::string не является потокобезопасной и может привести к подобным вещам.

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

На моем предыдущем рабочем месте мы использовали Compuware Boundschecker, чтобы помочь в этом. Это коммерчески и очень дорого, поэтому не может быть вариантом.

Вот пара бесплатных библиотек, которые могут пригодиться

http://www.codeguru.com/cpp/misc/misc/memory/article.php/c3745/

http://www.codeproject.com/KB/cpp/MemLeakDetect.aspx

Надеюсь, это поможет. Повреждение памяти - отстойное место!

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

Кроме того, я исправил проблему std::string. Как? Заменив его вектором, скомпилировав, а затем снова заменив строку. Он был последовательно разбивая там, и это фиксируется , даже если это ... не мог. Там есть что-то противное, и я не знаю, что именно. Я действительно хотел проверить один раз, когда я вручную выделяю память в куче:

 this->map = new Area*[largestY + 1];
 for (int i = 0; i < largestY + 1; i++) {
     this->map[i] = new Area[largestX + 1];
 }

и удалив его:

for (int i = 0; i < largestY + 1; i++) {
    delete [] this->map[i];
}
delete [] this->map;

Раньше я не выделял 2d-массив с помощью C++. Вроде работает.

Насколько я могу судить, ваш код правильный. Предполагая, что exampleString - это std::string, имеющая область класса, как вы описываете, вы должны иметь возможность инициализировать / назначать ее таким образом. Может, есть какая-то другая проблема? Может быть, фрагмент реального кода поможет поместить его в контекст.

Вопрос: Является ли exampleString указателем на строковый объект, созданный с помощью new?

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

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

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

Серьезно, я не нашел последовательного способа отследить подобные ошибки. Поскольку существует так много потенциальных проблем, нет простого контрольного списка, который нужно пройти. Однако я бы порекомендовал следующее:

  1. Разберитесь в отладчике.
  2. Начните копаться в отладчике, чтобы увидеть, сможете ли вы найти что-нибудь подозрительное. Проверяйте особенно, что происходит во время exampleString = hello;очереди.
  3. Убедитесь, что происходит сбой в exampleString = hello;строке, а не при выходе из какого-либо закрывающего блока (что может привести к срабатыванию деструкторов).
  4. Проверьте любую магию указателя, которую вы можете использовать. Арифметика указателя, приведение типов и т. Д.
  5. Проверьте все свои распределения и освобождения, чтобы убедиться, что они совпадают (без двойного освобождения).
  6. Убедитесь, что вы не возвращаете никаких ссылок или указателей на объекты в стеке.

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

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

В приведенном вами примере недостаточно контекста, чтобы определить, где может быть ошибка. Если бы кто-нибудь еще попробовал ваш пример, он бы сработал. Итак, в своей программе попробуйте удалить как можно больше лишних вещей, которые вы нам не показывали, и посмотрите, работает ли это тогда. Если это так, то постепенно добавляйте другой код, пока он не начнет давать сбой. Значит, проблема в том, что вы только что добавили.

Обратите внимание: если ваша программа многопоточная, у вас, вероятно, есть более серьезные проблемы. Если нет, то вы сможете сузить круг таким образом. Удачи!

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

Повреждение памяти - одна из самых сложных вещей для устранения неполадок, и обычно эти типы проблем решаются часами / днями в отладчике и замечанием чего-то вроде «эй, указатель X используется после того, как он был удален!».

Если это кому-то поможет, то с приобретением опыта вы станете лучше.

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

Однажды у нас была ошибка, которая ускользнула от всех обычных методов, valgrind, purify и т. Д. Сбой случился только на машинах с большим объемом памяти и только на больших наборах входных данных.

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

1) Найдите причину неисправности. Из вашего примера кода видно, что память для "exampleString" повреждена и поэтому не может быть записана. Продолжим это предположение.

2) Установите точку останова в последнем известном месте, в котором exampleString используется или изменяется без каких-либо проблем.

3) Добавьте точку наблюдения к элементу данных exampleString. В моей версии g ++ строка хранится в _M_dataplus._M_p . Мы хотим знать, когда изменяется этот член данных. Методика GDB для этого:

(gdb) p &exampleString._M_dataplus._M_p
$3 = (char **) 0xbfccc2d8
(gdb)  watch *$3
Hardware watchpoint 1: *$3

Я явно использую Linux с g ++ и gdb здесь, но я считаю, что точки наблюдения за памятью доступны в большинстве отладчиков.

4) Продолжайте, пока не сработает точка наблюдения:

Continuing.
Hardware watchpoint 2: *$3

Old value = 0xb7ec2604 ""
New value = 0x804a014 ""
0xb7e70a1c in std::string::_M_mutate () from /usr/lib/libstdc++.so.6
(gdb) where

Команда gdb where даст обратную трассировку, показывающую, что привело к модификации. Это либо совершенно законная модификация, и в этом случае просто продолжайте, либо, если вам повезет, это будет модификация из-за повреждения памяти. В последнем случае вы сможете просмотреть код, который действительно вызывает проблему, и, надеюсь, исправить ее.

Причиной нашей ошибки был доступ к массиву с отрицательным индексом. Индекс был результатом преобразования указателя на int по модулю размера массива. Ошибка была пропущена valgrind et al. поскольку адреса памяти, выделенные при работе с этими инструментами, никогда не были " > MAX_INT " и поэтому никогда не приводили к отрицательному индексу.