[БЕЗ_ЗВУКА] Всем привет. Я Антон Полднев, и мы начинаем «Черный пояс по C++». Вас ждет много практики, я надеюсь, не только в этом курсе, но и в дальнейшей жизни. И поэтому мы начинаем с важной и полезной темы — с инструментов поиска ошибок в ваших программах. Как мы уже не раз обсуждали, C++ декларирует принцип нулевого оверхеда. То есть если вам нужно решить какую-то задачу, C++ будет пытаться решить ее максимально эффективно. Например, если вы обращаетесь к какому-то элементу массива, C++ просто прибавит это число, номер индекса к указателю, и не будет проверять, не вышли ли вы за границу массива. Как же попросить C++ меньше доверять разработчику и перепроверять за ним, все ли у него в программе хорошо? Давайте рассмотрим для начала самый простой пример, в котором мы выйдем за границы вектора, и подумаем, что с этим делать, как эту ошибку можно обнаруживать. Итак, вот у нас программа с пустым main. Давайте создадим в ней вектор целых чисел, какой-нибудь самый простой из одной единицы или даже восьмерки, чтобы было понятнее. Зарезервируем две ячейки памяти и обратимся к элементу номер один, то есть к последнему элементу, которого на самом деле нет, потому что вектор из одного элемента. Давайте мы заведем переменную с индексом и обратимся к v [i]. Соберем программу. Она, конечно же, скомпилируется, потому что здесь все абсолютно корректно: я обращаюсь, я вызываю оператор квадратные скобки от int. Это вполне допустимая операция, поэтому все компилируется, но при запуске у меня выводится какой-то ноль. По идее у меня в векторе только восьмерка, откуда взялся ноль? Потому что у меня зарезервирована память еще на одну ячейку, и в ней сейчас оказался ноль. При этом программа не упала, потому что это память нашего вектора. Как такие ошибки можно находить? Особенно неприятно, если такое обращение выход за границы вектора закопано где-то внутри программы. Вы просто видите по косвенным признакам, что программа работает неверно и вам нужно как-то эту ошибку найти. Вы, конечно, можете расчехлить отладчик и в отладчике найти, что там где-то внутри происходит не то обращение к памяти. Но нельзя ли попросить компилятор такие ошибки отлавливать и явно вам писать «Ты обратился за границы вектора»? Для начала что можно сделать, если вы ничего дополнительного не знаете, не знаете материала грядущих лекций? Вы просто можете найти все обращения к элементам вектора и обернуть их в if'ы. То есть вывести v[i], только если i корректно. То есть i < v.size. Если i < v.size, то выводим, иначе говорим «Ой» (Oops). Давайте соберем эту программу, там есть небольшой warning про то, что мы сравниваем знаковые и беззнаковые. Ну давайте сделаем компилятору хорошо и сделаем i беззнаковым. Это как бы не проблема. Собираю. Запускаю. И вижу «Ой». Хорошо, мы, казалось бы, ошибку нашли, но не будем же мы, разработчики, если у нас в программе что-то идет не так, все обращения к элементам вектора заворачивать в такие if'ы. Конечно, нет. Хочется чего-то более простого. Есть у вектора метод add, который упрощает эту задачу и сам проверяет, что индекс корректен. Если некорректен, то выбрасывает исключения. Вот я заменил оператор квадратные скобки на вызов метода add, компилирую программу, запускаю, и у меня в метод add выкинул исключения — исключения типа out_of_range, и говорит, что индекс, который 1 ≥ size, который тоже 1, а поэтому все плохо. Казалось бы, мы решили проблему, но все-таки нет. Мы же не будем, опять же, для поиска ошибки заменять все операторы квадратные скобки на метод add везде, во всей программе. Это совершенно неудобно. Нужно как-то, оставив оператор квадратные скобки, вот здесь у меня был оператор квадратные скобки, попросить компилятор все равно проверять выход за границы вектора. Причем это должно делаться как-то просто, а не поиском по тексту и какой-то текстовой заменой в коде программы. Оказывается, это можно сделать на этапе препроцессора. Нужно включить специальные макроопределения в препроцессоре — дебажные дефайны. Как мы это сделаем? Мы пойдем в настройки проекта Project, Properties, C/C++ Build, Settings, вот сюда мы идем и здесь заходим в препроцессор. В препроцессоре у нас здесь Defined Symbols, −D, и мы добавляем два символа. Первый символ — это подчеркивание _GLIBCXX_DEBUG — вот так он выглядит. И его почти всегда достаточно, но мы еще добавим похожий символ _GLIBCXX_DEBUG_PEDANTIC. Еще более жесткие проверки. Вот так выглядит этот дефайн. Его мы тоже включаем. И у нас включено два дефайна. Мы их применяем, пересобираем программу. Собираем. Все компилируется. Запускаем. И при запуске он ругается, то есть он нашел какую-то проблему, давайте ее прочитаем. Мы попытались обратиться к элементу контейнера и вышли за границы. Индекс 1, но в векторе всего один элемент. И тут какое-то сообщение, из которого не очень понятно, никакой дополнительной информации оно, кажется, не несет, кроме адреса этого объекта. Итак, мы знаем, что мы обратились к первому элементу. Мы знаем, что контейнер имел размер 1. И мы это сделали без изменения кода программы. Мы буквально добавили всего лишь одну опцию в компиляцию. Но как же нам найти конкретную строчку в программе, в которой произошла проблема? В этом сообщении нет никакого указания на строчку. Есть указание на строчку внутри файла с описание вектора, это строчка 417, но это не про наш main.cpp. Поэтому нужно как-то строчку эту обнаружить. Как мы это сделаем? Мы запустим отладчик, самый обычный отладчик, но опять же по санитайзеру. Давайте запустим программу. Программа запустилась, но как-то странно упала. Тут написано, что у нас вызвался abort в операторе квадратные скобки у вектора в строчке 417, а этот оператор вызвался в main.cpp в строчке 10. Вот main.cpp, строчка 10, и вот здесь у меня вызвался этот самый оператор квадратные скобки. Мы нашли проблему. Мы нашли то место в программе, в котором мы выходим за границы вектора. Отлично. Давайте с этим простым примером мы закончим. И перейдем к чуть более интересному примеру — как раз та самая большая программа, как найти ошибки в большой программе. Итак, мы этот DEBUG завершаем и открываем решение задачи «Демографические показатели», которые нас преследуют, кажется, с «Желтого пояса». Вот у нас решение, которое мы сделали более красивым в «Коричневом поясе». Я напомню немного: у нас здесь есть люди, у людей есть возраст, пол и статус — ходит он на работу или не ходит на работу, устроен или не устроен. Про этих людей есть какие-то операторы. И мы хотим собрать статистику по медианному возрасту разных групп людей. Она собирается в такой структуре. Есть какие-то операторы, операторы для дебага, операторы для чего-то еще. Есть функция вычисления медианного возраста, есть функция чтения людей, есть функция вычисления этой самой статистики и функция вывода этой статистики. Казалось бы, решение похоже на то, что было в «Коричневом поясе», но представьте, что где-то ошибка, и мы пытаемся ее найти. Давайте я запущу эту программу, я ее собрал. Я ее запускаю. Мне нужно ввести каких-то людей. Давайте для простоты будет один человек, у которого возраст 0, пол 0, и is employed тоже 0, ничего страшного. Мы запускаем и видим, что что-то упало. Нас, опять же, спасает дебажный дефайн, который включен в этом проекте. И он выглядит как-то по-другому, это явно не про вектор, у нас что-то другое пошло не так. То есть дебажный дефайн позволяет проверять не только выход за границы вектора, но и что-то еще. Давайте почитаем, что здесь написано. Где-то в stl_algo.h в строке 4797 в каком-то алгоритме C++, который мы вызвали, какой-то невалидный итератор range. Диапазон итераторов какой-то неправильный. Где это происходит, что-то про вектор написано. А про какой алгоритм речь, непонятно. Давайте как эту проблему найдем. Как? С помощью отладчика. Запускаем отладчик. Запустили отладчик. Запускаем в нем программу. Программа радостно падает. Мы смотрим на так называемый back trace, на стек вызова функций. Видим, что функцию main он почему-то не показал, но он упал в алгоритме nth_element. И этого нам достаточно, чтобы посмотреть обратно на код программы и найти этот самый nth_element. Этот алгоритм у нас вызывается в ComputeMedianAge. Мы смотрим на вызов nth_element. Видим, что мы передаем итератор начала, итератор конца и итератор середины. А если посмотреть на код nth_element, где у нас все упало, то мы увидим, что здесь есть проверка того, что диапазон от n-того итератора до последнего валидный. Он проверяет, что первого до середины нормальный диапазон, и от середины до последнего нормальный диапазон. И если мы посмотрим в порядок аргументов функции, то увидим, что сначала принимается первый итератор, потом средний, а потом последний. А мы в main.cpp сделали не так, перепутав последний со средним. И вот нам помог дебажный дефайн найти проблему при вызове алгоритма nth_element — стандартного алгоритма в C++. Итак, подведем итоги этого видео. Мы изучили отладочные макроопределения — это специальные директивы препроцессора, которые включают специальные проверки различных инвариантов стандартной библиотеки: выход за границы вектора, валидность диапазонов итератора, которые передаются в стандартные алгоритмы. При этом при отсутствии этих макроопределений программа работает как обычно и с той же скоростью. Когда же вы включаете эти дебажные дефайны, у вас начинают срабатывать эти проверки, и программа работает дольше за счет того, что компилятор не до конца доверяет разработчику. Обращаю ваше внимание еще раз на то, что программы все стабильно и хорошо успешно компилировались, а ошибки находились при выполнении программы. Важно понимать, что отладочные макроопределения, как и следующий инструмент, который мы рассмотрим в следующем видео, работают в процессе выполнения программы и тем самым замедляют выполнение этой программы. Больше подробностей про то, какие ошибки помогают находить отладочные макроопределения и на что еще нужно обращать внимание, вы найдете в текстовом материале. А в следующем видео мы поговорим про еще один инструмент поиска ошибок при выполнении программы.