JavaRush /Курсы /C++ SELF /Watches: просмотр переменных, выражений, контейнеров

Watches: просмотр переменных, выражений, контейнеров

C++ SELF
34 уровень , 3 лекция
Открыта

1. Основы watches: зачем, что это и как не утонуть в значениях

Когда программа ведёт себя неправильно, первое желание новичка — поставить десяток std::cout << "тут я жив" << '\n'; и надеяться, что баг испугается и убежит. Иногда это работает, но чаще получается «спам в консоли» и ноль понимания, где и почему значение стало неправильным. Watches решают именно эту боль: они позволяют держать важные значения «перед глазами» на каждом останове — без переписывания кода и без захламления вывода.

Важно понять простую мысль: отладчик хорош не тем, что показывает «все переменные вообще», а тем, что помогает проверять гипотезы. У вас должна появляться фраза вида: «Кажется, индекс выходит за границы», или «Сумма почему-то обнуляется», или «В эту функцию передали пустую строку». Watches — это способ быстро сделать такую гипотезу проверяемой.

Для примеров мы продолжим условное учебное консольное приложение MiniBudget — простейший «учёт расходов»: храним расходы в std::vector, добавляем записи, считаем сумму, ищем по категории. Никаких классов, ООП и магии — только то, что у вас уже было: struct, функции, vector, string, циклы.

Что такое watch

Watch (наблюдение) — это выражение, которое отладчик будет пытаться вычислять при каждой остановке и показывать вам значение. Ключевое слово тут «выражение», а не только «переменная». Watch может быть sum, а может быть i < v.size(), а может быть v[i] (но с нюансами), а может быть items.size().

Это отличается от окна Locals тем, что Locals обычно показывает только то, что «само попалось» в текущем scope: параметры функции, локальные переменные. Watch же вы выбираете сами, и он «живёт» дольше: вы можете остановиться в другом месте, а watch останется (хотя может стать not available, если выражение сейчас не в области видимости).

Полезная аналогия: Locals — это «что лежит на столе у текущего сотрудника», а Watch — это «камеры наблюдения», которые вы поставили туда, куда вам реально интересно смотреть. И да, как и с камерами, можно переборщить и утонуть в потоке.

Главное правило: одна гипотеза → несколько наблюдений → вывод

Когда отладка превращается в хаос, почти всегда причина в том, что человек добавил 25 watches «на всякий случай». Отладчик начинает показывать тонну чисел, и мозг выключается. Поэтому держим в голове дисциплину: одна гипотеза → несколько наблюдений → вывод.

Обычно достаточно 3–8 выражений. Например, если вы отлаживаете цикл по vector, то типичный минимальный набор выглядит так:

Что подозреваем Что поставить в watch Зачем
Выход за границы
i, v.size(), i < v.size()
Быстро увидеть момент, когда условие «сломалось»
Неправильная сумма
i, sum, added
Понять, на каком шаге сумма стала не той
Неверная ветка if cond, части условия Увидеть, какая часть выражения даёт false

Сейчас это кажется «очевидным», но на практике люди часто делают наоборот: ставят watch на v[i], но не ставят i и v.size(). Итог — внезапное cannot evaluate или падение, и снова ощущение, что «магия не работает».

2. Мини-примеры: циклы, std::vector и std::string

Watch-отладка ошибки выхода за границы и off-by-one в сумме

Когда вы считаете сумму в цикле, классическая ошибка — граница i < n вместо i <= n (или наоборот). На глаз код выглядит нормально, а результат «чуть не тот». Это идеальный кейс для watches: мы хотим увидеть, докуда реально дошёл i, и как менялась сумма.

Вот пример функции в нашем MiniBudget, которая суммирует первые n расходов (условно — «топ-N»):

#include <cstddef>
#include <vector>

int sum_first_n(const std::vector<int>& amounts, std::size_t n) {
    int sum = 0;
    for (std::size_t i = 0; i < n; ++i) { // баг: если n > amounts.size()
        sum += amounts[i];
    }
    return sum;
}

Что ставим в watches, если видим странный результат или падение:

i, n, amounts.size(), i < amounts.size(), sum

И вот тут появляется очень важная привычка: смотрите не только на значение, но и на «логическое здоровье» условий. Watch i < amounts.size() — это маленький «датчик инварианта». Пока он true, мы относительно спокойны. Как только стал false, вы нашли точку, где программа начинает жить опасно.

Заметьте, мы не обязаны сразу чинить код. Сначала мы фиксируем факт: n оказался больше размера массива, цикл дошёл до «лишнего» индекса, и дальше всё закономерно.

Watches для std::vector: как безопасно смотреть элементы

С контейнерами есть приятный бонус: многие IDE умеют красиво раскрывать std::vector (как список элементов). Но даже если IDE не очень умная, можно смотреть нужные части руками. Главное — не устроить себе второй баг прямо во время отладки.

Плохая привычка: ставить watch v[i], когда i иногда бывает «плохим». Тогда отладчик пытается вычислить выражение, а вы получаете либо ошибку вычисления, либо лезете в UB-зону (в зависимости от того, как и где отладчик это делает).

Хорошая привычка: сначала ставить «страховочные» watches, а уже потом элементы.

Например, у нас есть список расходов:

#include <string>

struct Expense {
    std::string category;
    int amount;
};

И поиск по категории:

#include <cstddef>
#include <string>
#include <vector>

int find_first_by_category(const std::vector<Expense>& items, const std::string& cat) {
    for (std::size_t i = 0; i < items.size(); ++i) {
        if (items[i].category == cat) {
            return static_cast<int>(i);
        }
    }
    return -1;
}

Если поиск «иногда не находит, хотя должно», то watches, которые реально помогают:

i, items.size(), cat

Также полезно добавлять: items[i].category (но только когда уверены, что i < items.size()), и ещё очень полезно items[i].category == cat как отдельное выражение.

Тут вы быстро увидите типичную ситуацию: например, в данных категория "Food " (с пробелом), а вы ищете "Food". И это как раз тот момент, когда человек обычно час спорит с монитором, а watch показывает правду за 5 секунд.

Watches для std::string: длина, индексы и «где сломалось сравнение»

Строки очень любят ломать новичков, потому что визуально "abc" и "abc " — почти одно и то же (особенно если пробел в конце). Watches позволяют увидеть это прямо в момент сравнения.

Допустим, у нас есть функция «нормализации» категории: хотим убрать ведущие/концевые пробелы (пока примитивно, без сложных алгоритмов):

#include <string>

std::string trim_one_space_right(std::string s) {
    if (!s.empty() && s.back() == ' ') {
        s.pop_back();
    }
    return s;
}

Если после «нормализации» что-то всё равно не совпадает, watches могут быть такими:

s, s.size(), !s.empty()

А также s.back() (осторожно: только если !s.empty()).

Очень частая картина в отладке строк — watch показывает s.size() == 0, а вы всё равно пытаетесь смотреть s[0] или s.back(). Это тот самый момент, когда отладчик как бы шепчет: «Не трогай строку, она пустая». Стоит слушать.

3. Нюансы: scope, stack frames и логические watches

Watch и область видимости

Когда вы добавляете watch, иногда он становится серым/красным/not available. Новичок обычно думает: «отладчик сломан». На самом деле чаще всего причина скучная: переменная вышла из scope.

Пример: переменная temp живёт только внутри if.

#include <iostream>

int main() {
    int x = 10;

    if (x > 0) {
        int temp = x * 2;
        std::cout << temp << '\n'; // 20
    }

    return 0;
}

Если вы поставили watch на temp, он будет доступен только пока выполнение внутри блока { ... }. Как только вы вышли из if, переменной больше нет, и watch честно это показывает.

Это не баг, а полезное напоминание: локальные переменные реально «живут» ровно в своём блоке. Кстати, это же знание потом сильно помогает понимать, почему некоторые ошибки «то есть, то нет».

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

Watches и stack frames

После лекции про call stack становится понятнее один важный нюанс: watch-выражение вычисляется в контексте текущего frame. Если вы переключились на другой frame в стеке, то «те же» имена могут означать другое.

Представим, что add_expense() читает строку, парсит число через parse_amount(), и там что-то пошло не так:

#include <string>

int parse_amount(const std::string& s) {
    int x = 0;
    // допустим, тут где-то ошибка парсинга
    return x;
}

void add_expense(const std::string& line) {
    int amount = parse_amount(line);
    (void)amount;
}

Если вы остановились внутри parse_amount, watch s — это параметр parse_amount. Если вы переключитесь на frame выше (в add_expense), watch line — это другой параметр, хотя по смыслу он «про то же». А если у вас в двух функциях есть переменная x, то watch x легко станет «лотереей», если вы не следите за frame.

Поэтому маленькое правило дисциплины: прежде чем доверять watches, убедитесь, что вы смотрите правильный stack frame.

Логические watches: следим за инвариантами

Часто проблема не в конкретном значении, а в нарушении правила. Например: «индекс всегда меньше size()», «сумма не должна быть отрицательной», «категория не должна быть пустой».

Для этого удобно ставить watch не на «сырые данные», а на булевы выражения. Это превращает отладку в поиск момента, когда правило стало false.

Мини-пример с инвариантом «amount должен быть > 0»:

#include <vector>

int total_positive(const std::vector<int>& a) {
    int sum = 0;
    for (int x : a) {
        sum += x; // подозрительно, если x бывает отрицательным
    }
    return sum;
}

Полезные watches здесь: x, sum, и выражение x > 0. Если x > 0 внезапно стало false, то вопрос уже не «почему сумма не та», а «почему в контейнер попали отрицательные расходы».

Так вы постепенно начинаете отлаживать не «значения», а контракты. И это очень взрослая привычка, даже если вы пока начинающий.

4. Сценарий: отлаживаем MiniBudget через watches

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

Пусть у нас есть функция суммирования:

#include <vector>

int total(const std::vector<int>& a) {
    int sum = 0;
    for (std::size_t i = 0; i < a.size(); ++i) {
        if (a[i] > 0) {
            sum += a[i];
        }
    }
    return sum;
}

Оказалось, что иногда расходы вводятся как отрицательные (например, возврат), а пользователь ожидает, что «по модулю» тоже будет учитываться. Логика программы может быть неверной по требованиям, но для нас важнее механика отладки.

Ставим breakpoint на строку sum += a[i];, и в watches добавляем:

i, a.size(), a[i], a[i] > 0, sum

Дальше делаем Continue до первого останова и смотрим: a[i] > 0 часто false, значит эта ветка пропускает часть значений. Вот и причина «меньше ожидаемой»: программа суммирует только положительные.

Ключевой момент: мы пришли к выводу по наблюдаемым фактам, а не по догадке «наверное цикл неправильно работает». И это именно то, ради чего существует watch-окно.

Небольшая схема процесса (по-человечески, без фанатизма):

flowchart TD
    A[Гипотеза: сумма 'теряется' в цикле] --> B[Ставим breakpoint в подозрительном месте]
    B --> C[Добавляем 3-8 watches: i, size, элемент, условие, sum]
    C --> D[Continue/Step и смотрим момент изменения]
    D --> E[Фиксируем факт: какая ветка или какое значение ломает ожидание]

5. Типичные ошибки при работе с Watches

Ошибка №1: превращать watch-окно в «свалку всего подряд».
Когда вы добавляете 20–30 выражений, вы начинаете смотреть не на программу, а на шум. В итоге мозг перестаёт замечать главное: где именно значение изменилось и какое правило нарушилось. Лучше держать дисциплину: один набор watches под одну гипотезу, потом чистим и ставим новый.

Ошибка №2: watch на v[i] без контроля границ.
Это классика жанра: человек подозревает выход за границы и первым делом добавляет v[i]. Но если i уже неправильный, то выражение само по себе становится опасным и бесполезным. Гораздо умнее сначала наблюдать i, v.size() и i < v.size(), и только когда инвариант соблюдается — смотреть v[i].

Ошибка №3: забывать про область видимости.
Watch может «пропасть», потому что переменная больше не существует: вы вышли из блока {} или переключили stack frame. Это не проблема отладчика, а честное отражение модели языка: локальная переменная живёт ровно там, где вы её объявили. Если watch стал недоступен — сначала проверьте, где вы стоите и какой frame выбран.

Ошибка №4: делать выводы, не привязывая наблюдение к шагу и строке.
Иногда студент видит sum = 42 и сразу решает, что «вот тут ошибка». Но без привязки к конкретной строке и моменту изменения это просто число. Хорошая практика — замечать, на каком шаге и после какой строки значение стало другим. Watches хороши тем, что позволяют видеть динамику на каждом останове, но вывод всё равно должен быть привязан к конкретному месту выполнения.

Ошибка №5: пытаться отлаживать без гипотезы.
Это похоже на попытку лечить простуду, глядя на градусник каждые 5 секунд, но не понимая, что именно вы проверяете. Если вы не можете сформулировать проверяемое утверждение вроде «индекс не выходит за границы» или «условие должно стать true на этой итерации», то watches не спасут — они покажут числа, но не дадут смысла. Сначала формулируем гипотезу, потом выбираем наблюдения.

1
Задача
C++ SELF, 34 уровень, 3 лекция
Недоступна
Сумма маршрута
Сумма маршрута
1
Задача
C++ SELF, 34 уровень, 3 лекция
Недоступна
Безопасная корзина
Безопасная корзина
1
Задача
C++ SELF, 34 уровень, 3 лекция
Недоступна
Пропавшая строка
Пропавшая строка
1
Задача
C++ SELF, 34 уровень, 3 лекция
Недоступна
Скидка исчезла
Скидка исчезла
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ