Отладка C++ кода: 8 техник, которые сэкономят вам нервы

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Отладка C++ кода: как перестать бояться segmentation fault и полюбить GDB

Вы когда-нибудь тратили три часа на поиск бага, который в итоге оказывался забытым break в switch или лишним ; после if? Если да — добро пожаловать в клуб. Отладка C++ — это искусство, граничащее с мазохизмом. Но, как и любое искусство, его можно освоить.

В этой статье мы разберём не просто «как запустить GDB», а реальные техники, которые превращают отладку из гадания на кофейной гуще в детективную работу Шерлока Холмса. Мы поговорим о современных санитайзерах, реверсивной отладке и даже о том, как отловить баг, который воспроизводится «только в пятницу после обеда».

1. Классика жанра: GDB и LLDB

Начнём с фундамента. GNU Debugger (GDB) и его конкурент LLDB — это швейцарские ножи отладчика. Но большинство использует их на 10% возможностей. Давайте исправим это.

Точки останова с условиями

Вы ставите точку останова в цикле из 10 000 итераций и жмёте continue 9999 раз? Остановитесь. Используйте условные брейкпоинты:

(gdb) break main.cpp:42 if i == 5000

Это сэкономит часы жизни. В LLDB синтаксис похож:

(lldb) breakpoint set --file main.cpp --line 42 --condition 'i == 5000'

Watchpoints — ловим изменение переменной

Представьте: у вас есть переменная, которая меняется неизвестно где. Ставим watchpoint:

(gdb) watch my_variable
(gdb) rwatch my_variable   # сработает на чтение
(gdb) awatch my_variable   # сработает и на чтение, и на запись

Теперь программа остановится ровно в тот момент, когда кто-то коснётся вашей переменной. Магия, правда?

2. Reverse Debugging: путешествие во времени

Самый страшный сон программиста: программа упала, но вы не знаете, как она дошла до этого состояния. Реверсивная отладка (обратная отладка) позволяет «отмотать» выполнение назад.

В GDB для этого нужна поддержка record:

(gdb) record
(gdb) continue
... программа упала ...
(gdb) reverse-step   # шаг назад
(gdb) reverse-continue # продолжить в обратную сторону

В коммерческих инструментах (например, UndoDB или rr от Mozilla) это работает ещё круче. rr (Record and Replay) — бесплатный инструмент, который записывает выполнение программы, а потом позволяет проигрывать его снова и снова:

$ rr record ./my_program
$ rr replay
(gdb) continue
... нашли место падения ...
(gdb) reverse-step

Совет: используйте rr для поиска гонок данных (data races) — он детерминированно воспроизводит каждое выполнение.

3. Санитайзеры: прививка от undefined behavior

Современный C++ (начиная с Clang и GCC 4.8+) имеет встроенные инструменты, которые не снились нашим отцам. AddressSanitizer (ASan), UndefinedBehaviorSanitizer (UBSan) и ThreadSanitizer (TSan) — это must-have.

Пример: ловим утечку памяти

// compile with: g++ -fsanitize=address -g main.cpp -o main
#include <iostream>

void leak() {
    int* p = new int(42);
    // забыли delete
}

int main() {
    leak();
    return 0;
}

Запускаем — и видим:

==12345==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 4 byte(s) in 1 object(s) allocated from:
    #0 0x7f... in operator new(unsigned long)
    #1 0x400... in leak() main.cpp:5
    #2 0x400... in main main.cpp:10

ASan покажет точный стек вызовов, включая номер строки, где произошло выделение. Больше никаких «где-то утекает память».

UBSan против неопределённого поведения

// compile with: g++ -fsanitize=undefined -g main.cpp
int main() {
    int x = INT_MAX;
    int y = x + 1;  // переполнение signed int — UB!
    return 0;
}

UBSan тут же сообщит: runtime error: signed integer overflow. И вы узнаете о баге до того, как он превратится в трудноуловимый глюк.

4. Valgrind: старый, но верный друг

Если санитайзеры — это современная медицина, то Valgrind — это дедовский метод, который до сих пор работает. Он медленнее (программа тормозит в 10-20 раз), но зато не требует перекомпиляции.

$ valgrind --leak-check=full ./my_program

Valgrind отлавливает:

  • Утечки памяти (включая «всё ещё достижимые» блоки)
  • Использование неинициализированной памяти
  • Переполнение буфера
  • Двойное освобождение (double free)

Единственный минус — Valgrind не понимает современные аллокаторы и может выдавать ложные срабатывания на std::vector или std::string. Но для самописных структур он незаменим.

5. Статический анализ: профилактика лучше лечения

Лучший баг — тот, который не попал в код. Статические анализаторы проверяют исходный код без его запуска. Для C++ лучшие бесплатные варианты:

  • Clang-Tidy — встроен в Clang, находит проблемы с производительностью, стилем и потенциальные баги.
  • CPPCheck — лёгкий, быстрый, находит классические ошибки (выход за границы массива, утечки).
  • PVS-Studio — коммерческий, но для открытых проектов бесплатен. Находит 99% багов, включая логические.

Пример: что найдёт Clang-Tidy

// clang-tidy --checks=* main.cpp
void example() {
    std::string s = "hello";
    s[10] = 'x';  // выход за границы — undefined behavior
}

Clang-Tidy выдаст: warning: array index 10 is past the end of the array [clang-diagnostic-array-bounds]. И это без единого запуска программы.

6. Логирование: искусство ставить printf правильно

Блоги

Рекомендаций по книгам