Отладочная печать — Основы программирования
Кнопка «Проверить» запускает на выполнение тесты, которые, в свою очередь, используют написанный вами код. Это означает, что вы легко можете использовать console.log везде, где хотите, и столько раз, сколько хотите. Весь вывод появится во вкладке OUTPUT . Обратите внимание на одну деталь. Если выполнение кода не дошло до того места, где была вставлена отладочная печать, то она, естественно, не выведется на экран.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Об обучении на Хекслете
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Урок «Как эффективно учиться на Хекслете»
- Вебинар « Как самостоятельно учиться »
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
- 130 курсов, 2000+ часов теории
- 1000 практических заданий в браузере
- 360 000 студентов
Наши выпускники работают в компаниях:
Отладка программ на C для начинающих
Всё последующее в основном написано для ОС Linux и консольной отладки, хотя кое-что можно использовать и в других условиях.
Возможно это прозвучит странно, но начинать писать порграмму стоит с установки системы контроля версий (если ещё не установлена) и создания репозитория. Это нужно для того, чтобы в процессе написания не потерять много времени на попытки вспоминить где, что, как, когда и зачем пишущий исправлял/добавлял. На сегодня наиболее популярные — это svn (subversion), git и mercurial. Последний, лично мне, нравится больше остальных, т.к., субъективно, он проще и удобнее, особенно для личного пользования.
Далее нужно убедиться в наличии команд gdb (отладчик) и strace (монитор системных вызовов). Если их нет, то установить. А при компиляции своей программы не забыть включить добавление отладочной информации.
- dmesg (информация ядра), lspci (устройства на шине PCI), lsmod (список загруженных драйверов) — при работе с драйверами;
- tail /var/log/messages — показывает десяток строк в конце системного лога;
- ps ax — список запущенных процессов (ключи могут быть и другие);
Допустим, что ничего нужного там не нашлось. Тогда стоит выполнить команду «ulimit -c 50000», ulimit (встроенная команда shell, устанавливающая/показывающая ограничения использования ресурсов shell. Подробное описание можно почитать c помощью man или тут), 50000 — взято отбалды, это размер core-файла, который в большинстве случаев создастся после падения программы, запущенной в этой же консоли, и представляет собой дамп памяти упавшей программы. Далее запускаем программу снова, в консоли, где был ulimit. Она опять падает, но уже с созданием коры (обычно). Как вариант, всё это можно проделать и заранее, т.к. если ошибка плавающая, то второй раз может упасть нескоро, бывало люди месяцами ждали.
- gdb core.NNNN
- комментирование всего и раскомментирование маленькими частями с последующей компиляцией и проверкой на отсутствие ошибки;
- вставка отладочной печати (зачастую чуть ли не после каждой строки подозрительного участка кода (memset! memcpy!)) с дальнейшим анализом — после какой печати сломалось.
Как быть если программа входит в бесконечный цикл и система впадает в ступор (бывает и такое)? Выгружаем оконный менеджер и в текстовом режиме на одна из консолей запускается под суперпользователем (root). Для полной уверенности можно ей немного повысить приоритет с помощью nice/renice, а программу запустить на другой консоли/терминале под обычным пользователем. Тогда её, при зацикливании, можно будет снять в консоли суперпользователя (команда «kill») и таки увидеть что она пишет.
Что делать если ничего не помогает? Ответ только один — садиться и внимательно, очень внимательно, изучать код и думать.
Если в программе ошибок нет (не видно на 1001й взгляд и анализ), то может на разделах закончилось место? (true story)
В конце добавлю, что если вы решите перенести кору в другое место, чтобы разобраться там (например, домой), то переносите вместе с теми исходниками и скомпилированной программой как они лежат или вывод gdb будет некорректным или его вообще не будет.
P.S. В исходниках ядра Linux каталог Documentation есть файлик CodingStyle. В нём есть что взять на заметку начинающему C-программисту.
P.P.S. Если бы мне всё это было известно cначала, то мне было бы гораздо проще.
Логирование как способ отлаживать код
Почему так важно запретить самому себе отладку руками?
Когда вы отлаживаете программу, то вы, сами того не осознавая, думаете что за один отладочный сеанс исправите все проблемы, возникшие в рамках этой задачи. Но наша недальновидность не хочет верить в то, что на самом деле там не одна проблема, а несколько. И за один отладочный сеанс не получится решить все эти проблемы.
Поэтому вам надо будет несколько раз запускать этот код в отладочном режиме, проводя часы отладки над одним и тем же куском кода. И это только вы один столько времени потратили над этой частью программы. Каждый член команды, кому «посчастливится» работать с этим кодом, будет вынужден прожить ту же самую историю, которую прожили вы.
Я уже не говорю о том, что люди в командах меняются, команды меняются и так далее. Человеко-часы уходят на одно и то же. Перестаньте делать это. Я серьёзно. Возьмите ответственность за других людей на себя. Помогите им не переживать тот же самый участок вашей жизни.
Проблематика: Сложно отлаживать составной код
Возможный алгоритм решения проблемы:
- Разбить на отдельные части
- Выкидываем отладку, просто запрещаем пользоваться режимом Debug
- Анализируем отдельные части (придумываем для них невалидные ситуации, граничные случаи)
- Пишем тесты на каждую отдельную часть всего алгоритма
- В тестах иногда приходится узнавать промежуточные данные, но…
Отладка нам боле недоступна, поэтому втыкаем Trace в те части, где возникает подозрение на некорректное выполнение алгоритма - По трассировке нужно понять причину проблемы
- Если не понятно, то чаще всего либо стоит написать ещё тест, либо выполнить трассировку на один этап раньше
Таким образом, вы избавляете других людей от отладки этого кода, т.к. если возникнет проблема, то человек посмотрит ваши логи и поймёт в чём причина. Логировать стоит только важные участки алгоритма.
Давайте посмотрим пример. Вы знаете, что по отдельности все реализации интерфейсов работают (т.к. написаны тесты, доказывающие это). Но при взаимодействии всего вместе возникает некорректное поведение. Что вам нужно? Нужно логировать ответы от «корректных» интерфейсов:
void Register(User user) < // на этом участке не нужно ничего логировать, // т.к. всё будет понятно из исключения var isExists = _userRepository.IsUserExists(user); if (isExists) < throw new InvalidOperationException($"User already exists: "); > // а вот это уже можно и залогировать, // т.к. от этого зависят дальнейшие детали алгоритма var roleInOtherService = _removeService.GetRole(user); _log.Trace($"Remote role: ") switch (roleInOtherService) < case ". ": break; . >// тут нам не критично, если пользователю не добавили // определённые привелегии в каком-то удалённом сервисе, // но мы бы хотели знать об этом foreach (var privilege in Privileges) < var isSuccess = _privilegeRemoteService.Grant(user, privilege); if (isSuccess) < _log.Trace($"Add privilege: to User: "); > else < _log.Trace($"Privilege: not added to User: "); > > . >
В этом примере видно, как мы трассируем отдельные части алгоритма для того, чтобы можно было зафиксировать тот или иной этап его выполнения. Посмотрев на логи, станет ясно в чём причина. В реальной жизни весь этот алгоритм стоило бы разбить на отдельные методы, но суть от этого не меняется.
Всё это в разы ускоряет написание кода. Вам не нужно бежать по циклу с F10 для того, чтобы понять на какой итерации цикла возникла проблема. Просто залогируйте состояние нужных вам объектов при определённых условиях на определённой итерации цикла.
В конечном итоге вы оставляете важные логи и удаляете побочные логи, например, где вы узнавали состояние объекта на определённой итерации цикла. Такие логи не помогут вашим коллегам, т.к. вы, скорее всего, уже поняли проблему некорректного алгоритма и исправили её. Если нет, то ведите разработку в вашей ветке до тех пор, пока не найдёте в чём причина и не исправите проблему. А вот логи, которые записывают важные результаты от выполнения других алгоритмов, понадобятся всем. Поэтому их стоит оставить.
Что если нам не нужен этот мусор? В таком случае выполняйте трассировку только в рамках отладки, а затем подчищайте за собой.
Плюсы | Минусы |
---|---|
Есть возможность проследить выполнение алгоритма без отладки | Приходится писать код трассировки |
В production логи будут собирать иногда нужные данные | В production логи будут собирать иногда ненужные данные |
Время на отладку сокращается, т.к. вы часто запускаете код и тут же видите результаты его выполнения (всё как на ладони) | Нужно подчищать за собой. Это иногда приводит к повторному написанию кода трассировки |
Коллеги могут прослеживать выполнение алгоритма по логам | Коллегам приходится смотреть не только на сам алгоритм, но и на трассировку его выполнения |
Логирование — единственный способ отладки недетерминированных сценариев (например, многопоточность или сеть) | В production уменьшается производительность из-за операций I/O и CPU |
Напоследок хотелось бы сказать вот что: думайте своей головой. Не нужно прыгать из крайности в крайность. Порой отладка руками действительно быстрее решит проблему здесь и сейчас. Но как только вы заметили, что что-то быстро, здесь и сейчас не получается, пора переключиться на другой режим.
- Тестирование IT-систем
- Программирование
- Анализ и проектирование систем
- Отладка
Как отлаживать маленькие программы
Пусть у вас есть небольшая программа, которая… не работает. Причем не просто как-то не работает, а у вас есть конкретный тест, конкретный пример, на котором она не работает. (Если у вас такого примера нет, то у меня есть отдельный текст про то, что делать в таком случае.) Как понять, что в программе не так, и как это исправить?
На самом деле, на эту тему есть знаменитый текст «How to debug small programs» и его русский перевод, но на мой взгяд рекомендации, приведенные там, — это излишнее усложнение, не нужное на самом деле в 90% действительно простых программ.
Итак, у вас есть тест, но вы не понимаете, почему программа на нем выдает не тот результат, который нужен. Ну, во-первых, по возможности уменьшите тест. Если в вашей задаче вводится какой-то массив или т.п., то не надо разбираться с программой на массиве длины 10. Попробуйте найти тест длины 3-4, на котором программа тоже не будет работать. Если вводится одно число, но дальше будет цикл до этого числа, то подберите число поменьше. И т.п.
Дальше есть несколько подходов.
Представьте себе, что вы компьютер
Основной, самый главный подход, когда программа действительно небольшая, строк 10-20 максимум, состоит в том, чтобы представить себя на месте компьютера и в уме выполнить программу. Возьмите листочек бумажки (или откройте «Блокнот»), выпишите на нем список переменных, которые есть в вашей программе, оставив у каждой переменной место, куда вы будете записывать их значения. Это будет оперативная память вашего компьютера. (В дальнейшем, когда вы освоитесь, бумажка вам не будет нужна, вы будете держать все нужные значения в уме, и все это выполнение будет происходить весьма быстро.)
Далее выполняйте программу пошагово, с самого начала (ну можете пропустить ввод данных, если вы в нем на 200% уверены). При каждом изменении значения каждой переменной выписывая измененное значение на бумажке. Самое важное тут — это подробно и тщательно делать именно то, что написано в программе. Забудьте (точнее лучше задвиньте на задний план) вашу задачу, забудьте, зачем вы писали этот код. Вы работаете за компьютер, компьютер ничего не знает про то, какая у вас задача, он просто тупо выполняет написанный код. Полезно тщательно проговаривать каждую выполняемую операцию. Не забывайте, что операции — это не только присваивания, это еще и все управляющие конструкции (if’ы, циклы и т.д.); не забывайте, что в циклах на каждой итерации выполняются действия, относящиеся собственно к циклу (проверка условия в while, увеличение индекса цикла в for). Все изменения переменных отражайте на бумажке, каждый раз, когда вам нужно значение какой-то переменной, сверяйтесь с бумажкой.
При этом надо все-таки где-то глубоко в уме все-таки помнить, какую задачу вы решаете, и какой код зачем написан, чтобы, как только реальное выполнение кода отойдет от того, что вы имели в виду, сразу это и заметить.
Пример. Задача «Переставить элементы в обратном порядке». Типичный код, который тут многие пишут, примерно такой:
# a - массив, который вы считали for i in range(len(a)): t = a[i] a[i] = a[len(a) - i] a[len(a) - i] = t
Вы запускаете программу на тесте 1 2 3 , и она падает. Хорошо, давайте представим, что мы выполняем эту программу за компьютер. У нас есть массив a , в котором записано 1 2 3 (и это записано у нас на бумажке), и переменные i и t . Поехали.
Начинается цикл for , переменная i становится равна 0 (на бумажке рядом с именем переменной i пишем 0 ).
Команда t = a[i] . Чему у нас равно i ? Смотрим на бумажку, i равно 0 . Чему равно a[i] ? Смотрим на бумажку, a[i] это a[0] это 1 . Это значение записывается в t . Записываем на бумажке рядом с t единицу.
Команда a[i] = a[len(a)-i] . Чему у нас равно i ? Нулю. Чему равно len(a) ? Трем. Чему равно len(a)-i ? Трем. Чему равно a[3] ? Ой, выход за пределы массива (не забываем, что элементы в массиве нумеруются с нуля; полезно на бумажке под значениями элементов массива подписать их индексы).
Вот собственно мы нашли первую ошибку. Обратите внимание, что мы специально тщательно и подробно все проговаривали; если бы вы действовали поверхностно, то вы могли бы сразу сказать: « a[len(a)-i] — это последний элемент массива (ведь я именно для этого писал этот код), поэтому это просто 3 ». И вы не заметили бы ошибку. Именно поэтому и надо по максимуму забыть, что обозначает этот код, а вместо этого просто четко и подробно выполнять то, что написано, постоянно сверяясь с бумажкой.
Хорошо, давайте исправим ошибку, теперь код такой:
# a - массив, который вы считали for i in range(len(a)): t = a[i] a[i] = a[len(a) - i - 1] a[len(a) - i - 1] = t
Запускаем программу — и она выдает 1 2 3 , т.е. как будто массив не изменился. Это все равно неправильно, поэтому поехали еще раз.
Начинается цикл for , переменная i становится равна 0 (на бумажке рядом с именем переменной i пишем 0 ).
Команда t = a[i] . Чему у нас равно i ? Смотрим на бумажку, i равно 0 . Чему равно a[i] ? Смотрим на бумажку, a[i] это a[0] это 1 . Это значение записывается в t . Записываем на бумажке рядом с t единицу.
Команда a[i] = a[len(a)-i-1] . Чему у нас равно i ? Нулю. Чему равно len(a) ? Трем. Чему равно len(a)-i-1 ? Двум. Чему равно a[2] ? Трем. Это значение записывается в a[i] . Что такое a[i] ? У нас i равно 0 , поэтому это нулевой элемент массива. Зачеркиваем единичку, которая записана на бумажке на нулевом месте в массиве, записываем туда 3.
Команда a[len(a) — i — 1] = t . Чему у нас равно t ? Единице. Что такое a[len(a) — i — 1] ? Выражение в квадратных скобках мы только что считали (но надо как минимум внимательно проверить, что выражение тут написано то же, а лучше пересчитать), поэтому это a[2] . Значит, в a[2] записываем 1. Зачеркиваем число 3, которое раньше было написано в a[2] , записываем туда 1.
Продолжаем. Итерация цикла закончилась, начинается новая итерация цикла. i становится равно 1 .
Продолжаем все делать так же тщательно и подробно. Я не буду дальше все это расписывать, но (особенно если вы еще не видите ошибки в коде выше) можете продолжить и все-таки найти ошибку.
Добавьте отладочный вывод
Второй полезный подход — добавить в программу вывод на экран промежуточных значений переменных в ключевых местах программы. Тут надо суметь понять, что такое «ключевые места» и какие переменные выводить. Обычно, например, если у вас в программе есть какие-то циклы, то полезно добавить вывод как минимум в конце каждой итерации, и выводить те переменные, которые меняются в цикле, в том числе для циклов for — переменную цикла. Если у вас сложная конструкция из if’ов, то добавить вывод внутрь каждого if’а, чтобы видеть, в какой именно if зашла программа. Если у вас несколько функций или тем более рекурсия, то добавить отладочный вывод типа «вошли в такую-то функцию» (зачастую с указанием параметров функции) и «вышли из такой-то функции». Если у вас просто сложные вычисления, длинная формула или несколько формул — то добавить вывод промежуточных результатов вычислений; возможно, для этого придется длинную формулу разбить на части (и это заодно сделает ее понятнее).
После этого запускаете программу на том тесте, на котором она не работает, смотрите на отладочный вывод, проверяете все значения и находите первое значение, которое неверно. Таким образом вы довольно хорошо локализуете проблему, вы будете знать, что ошибка, например, в вычислении той или иной переменной. Дальше уже действуйте по ситуации: если программа не очень сложная, то эта переменная меняется в одном-двух местах, и вы сразу можете посмотреть, как она считается и почему значение получается неправильным. Тем более что вы, наверное, вывели на экран не только значение этой переменной, но и значения других переменных, от которых она зависит, поэтому сразу можете подставить эти значения в формулу и проверить.
Если же программа более сложная, эта переменная меняется в нескольких местах, или вы вывели на экран недостаточно информации, чтобы легко расследовать проблему, то добавьте еще отладочного вывода, чтобы более подробно видеть, откуда взялось это значение. И повторите.
Может быть и другая причина проблемы — не неправильное значение переменной, а, например, заход не в тот if или слишком раннее или слишком позднее окончание цикла, и т.д. Но в любом случае, как только вы добавили отладочный вывод, вы стали намного лучше понимать, что происходит в программе, и вам будет намного легче найти ошибку.
Рассмотрим в качестве примера тот же код, который мы разбирали выше. Добавим в него отладочный вывод примерно такой:
# a — массив, который вы считали for i in range(len(a)): t = a[i] a[i] = a[len(a) — i — 1] a[len(a) — i — 1] = t print(«i=», i, «a выполните-программу-пошагово-в-среде-разработки»>Выполните программу пошагово в среде разработки
Многие среды разработки позволяют вам выполнить код пошагово. По сути, это аналог первого указанного выше способа, но всю работу за вас делает компьютер, вам остается только многократно нажимать на одну и ту же кнопку. Это, безусловно, полезный во многих случаях способ, но на самом деле не стоит им злоупотреблять. В частности, когда речь идет про действительно небольшую программу, то использование пошаговой отладки зачастую оказывается более сложным и долгим, чем отладка любым из описанных выше способов, особенно когда вы уже освоили выполнение программы в уме.
Поговорите с уточкой
Еще одна стандартная рекомендация — это взять резиновую уточку (ну на самом деле любую игрушку, или даже можно и без игрушки), и подробно по порядку объяснить ей, что делает ваша программа, что делает в ней каждая строчка и почему всё написано так, а не иначе.
Вот прямо так и говорите: мне надо переставить элементы массива в обратном порядке. Для этого мне надо взять первый элемент массива и поменять местами с последним, потом второй с предпоследним и так далее. Как я это пишу в программе? Мне надо менять много пар элементов, поэтому я делаю цикл for . В цикле for переменная i обозначает номер элемента, который я буду менять местами с симметричным. Она должна меняться от 0 до… и вот тут вы понимаете, где ошибка в программе.
Это весьма полезный прием, если вы сумеете его освоить. Ключевой момент, что тут надо делать — надо подробно и про каждый элемент кода объяснить, зачем вы это делаете и почему код написан именно так. В частности, в примере выше фраза «она должна меняться от 0 до…» возникла потому, что вы видите, что в коде написать range(len(a)) , значит, надо объяснить уточке, почему написано именно так.
Важно все тщательно и подробно проговаривать; точно так же, как и при выполнении программы в уме вы должны последовательно выполнять все действия, а не «срезать углы» словами «а, это будет последний элемент массива», так и здесь не надо «срезать углы», пропуская объяснения тех или иных моментов и полагая, что они понятны и очевидны.
Помимо собственно поиска ошибок, метод уточки еще помогает вам обнаружить и осознать места в программе, которые вы не до конца понимаете, и самим в них подробнее разобраться (это близко и связано с поиском ошибок, но бывает полезно и в других ситуациях). Но про это и вообще про осознание кода я, пожалуй, напишу когда-нибудь еще один пост.
Мой курс по алгоритмическому программированию (и подготовке к олимпиадам) для школьников, студентов и всех желающих — algoprog.ru