JavaRush /Курсы /C++ SELF /argc/argv — базовый разбор аргументов

argc/argv — базовый разбор аргументов

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

1. Аргументы командной строки: зачем и откуда они берутся

Представьте, что ваша программа — это кофемашина. Можно подойти, нажать кнопку и дальше “вести диалог” (интерактивный режим). А можно заранее сказать: “сделай капучино, 300 мл, без сахара” — и уйти ждать результата. Аргументы командной строки — это как раз такие заранее переданные настройки, которые приходят в момент запуска.

В обычной жизни это выглядит так: вы запускаете программу и добавляете к команде параметры.

Например (условно):

app --name Alice --repeat 3

И программа должна понять: имя = Alice, повторить 3 раза.

Почему это удобно? Потому что так строятся почти все “настоящие” консольные утилиты: компиляторы, архиваторы, форматтеры кода, линтеры. И да, ваш будущий проект тоже может так уметь — без магии, просто через argc/argv.

Сигнатура main: argc и argv

Когда вы писали первые программы, main() выглядел так:

int main() {
    // ...
}

Сейчас мы включаем “режим взрослой консольной жизни” и используем другой вариант main:

int main(int argc, char* argv[]) {
    // ...
}

Тут важны две вещи: argc и argv. Звучит как имена двух гномов из подвала линковщика, но на самом деле всё просто.

Смысл такой:

Элемент Что это Как понимать
argc
количество аргументов “сколько строк нам передали”
argv
массив C-строк “где лежат эти строки”

Причём argv — это массив, поэтому к нему обращаются по индексу: argv[0], argv[1], …, argv[argc - 1].

argv — это массив, и у него есть границы

Очень важный момент для безопасности: argv — обычный массив указателей. А массивы, как мы уже знаем, не прощают самоуверенности.

Если argc == 3, то допустимые индексы ровно такие:

  • argv[0]
  • argv[1]
  • argv[2]

Если вы полезете в argv[3] — это выход за границы. А выход за границы — это как шагнуть в темноту: иногда “повезёт”, но чаще вы просто упадёте.

Ещё один практический факт: argv[0] обычно содержит имя программы (или путь к ней). Поэтому разбор пользовательских аргументов чаще всего начинается с i = 1.

Мини-пример “посмотрим, что вообще пришло”:

#include <iostream>

int main(int argc, char* argv[]) {
    for (int i = 0; i < argc; ++i) {
        std::cout << i << ": " << argv[i] << '\n';
    }
}

Если запустить как app hello world, вы увидите примерно:

0: app          // имя программы (часто)
1: hello
2: world

2. Строки аргументов: почему это не std::string

Почему argv[i] — это не std::string, и чем поможет std::string_view

Когда вы читаете argv[i], вы получаете не std::string, а C-строку: char*, которая заканчивается нулевым символом '\0'. Поэтому напрямую сравнивать argv[i] как “строки” нельзя вот так:

// ПЛОХО (сравнение адресов, а не текста)
if (argv[i] == "--help") { /* ... */ }

Это сравнение указателей (адресов в памяти), а не содержимого текста. Иногда новички удивляются: “почему не работает? я же вижу --help!” — а потому что вы сравнили не надпись на бумажке, а место, где лежит бумажка.

Самый удобный способ — превратить argv[i] в std::string_view. Это дёшево (без копирования) и позволяет сравнивать по содержимому.

#include <string_view>

std::string_view arg = argv[i];
if (arg == "--help") { /* ... */ }

Здесь string_view просто “смотрит” на уже существующую C-строку. И это нормально, потому что аргументы командной строки живут, как правило, всё время работы программы.

3. Безопасный разбор: главное правило про границы

Главное правило: не трогай argv[i + 1], пока не проверил границы

Сейчас будет правило, которое спасает вам нервы, время и, возможно, клавиатуру от удара лбом:

Если опция требует значение (например --name Alice), то прежде чем читать argv[i + 1], вы обязаны проверить:

i + 1 < argc

Иначе у вас случится классика жанра: пользователь напишет --name в конце и программа полезет “за край массива”.

Давайте закрепим это маленькой схемой алгоритма “идём слева направо по аргументам”.

flowchart TD
    A[Начать i = 1] --> B{ i < argc ? }
    B -- нет --> Z[Готово]
    B -- да --> C["прочитать arg = argv[i]"]
    C --> D{arg == '--name'?}
    D -- нет --> E[обработать другое / пропустить]
    D -- да --> F{ i+1 < argc ? }
    F -- нет --> G[ошибка: нет значения]
    F -- да --> H["name = argv[i+1]; i++"]
    E --> I[i++]
    H --> I
    I --> B

Да, блок-схема выглядит как мини-лабиринт. Зато она честно показывает идею: проверка границ — до чтения.

4. Практика: флаги, опции и позиционные аргументы

Первый полезный кейс: флаг --help / -h

Теперь сделаем мини-полезную штуку: программа, которая ищет --help или -h в аргументах. Мы пока не проектируем красивый CLI-контракт, мы просто учимся механике: “нашёл флаг — сделал действие”.

#include <iostream>
#include <string_view>

int main(int argc, char* argv[]) {
    for (int i = 1; i < argc; ++i) {
        std::string_view arg = argv[i];

        if (arg == "--help" || arg == "-h") {
            std::cout << "Help requested\n";
            return 0;
        }
    }

    std::cout << "No help flag\n";
}

Обратите внимание: флаг — это опция без значения. Мы не читаем argv[i + 1], не двигаем индекс на два шага, просто проверяем текущий аргумент.

Опция со значением: формат --key value

Дальше — самый частый формат опций, который вы увидите в реальных утилитах: ключ отдельно, значение отдельно.

Пример:

app --name Alice

Тут --name — ключ, Alice — значение, и оно лежит в следующем аргументе.

Сделаем программу, которая принимает --name и печатает приветствие. По умолчанию имя будет "anonymous".

#include <iostream>
#include <string_view>

int main(int argc, char* argv[]) {
    std::string_view name = "anonymous";

    for (int i = 1; i < argc; ++i) {
        std::string_view arg = argv[i];

        if (arg == "--name") {
            if (i + 1 >= argc) {
                std::cout << "Error: --name requires a value\n";
                return 1;
            }
            name = argv[i + 1];
            ++i; // съели значение
        }
    }

    std::cout << "Hello, " << name << '\n';
}

Здесь есть два ключевых приёма.

  • Сначала мы проверили границу i + 1 >= argc. Это защищает от ситуации, когда значение забыли.
  • Потом мы сделали ++i. Это означает: “мы уже обработали следующий аргумент как значение, не надо обрабатывать его ещё раз как отдельный параметр”.

Опция со значением: формат --key=value

Иногда удобнее передавать значение прямо внутри одного аргумента. Тогда не нужно читать argv[i + 1], а значит меньше рисков выйти за границы.

Пример:

app --name=Alice

Разберём этот формат через starts_with и substr. Здесь очень кстати std::string_view.

#include <iostream>
#include <string_view>

int main(int argc, char* argv[]) {
    std::string_view name = "anonymous";
    constexpr std::string_view prefix = "--name=";

    for (int i = 1; i < argc; ++i) {
        std::string_view arg = argv[i];

        if (arg.starts_with(prefix)) {
            name = arg.substr(prefix.size());
        }
    }

    std::cout << "Hello, " << name << '\n';
}

Тут есть тонкий момент: пользователь может написать --name= (пустое значение). Тогда name станет пустой строкой. Это не “ошибка языка”, это вопрос контракта: разрешаете ли вы пустое имя.

Позиционные аргументы: когда аргумент — “не опция”

В реальных программах обычно есть не только опции (--flag, --key value), но и позиционные аргументы. Например, имя файла:

app input.txt --verbose

Частая простая договорённость (именно “договорённость”, не закон Вселенной): всё, что начинается с -, считаем опцией, а всё остальное — позиционным аргументом.

Давайте напишем простейший разбор: найдём “первый позиционный аргумент” как filename.

#include <iostream>
#include <string_view>

int main(int argc, char* argv[]) {
    std::string_view filename = "(none)";

    for (int i = 1; i < argc; ++i) {
        std::string_view arg = argv[i];

        if (!arg.empty() && arg[0] != '-') {
            filename = arg;
            break;
        }
    }

    std::cout << "filename=" << filename << '\n';
}

Почему break? Потому что мы решили: нам нужен только один файл. В другой программе вы могли бы собирать список файлов — но это уже другой контракт и другой парсер.

5. Практический мини-пример: добавляем argc/argv в учебное приложение

Сейчас мы сделаем маленький шаг к “реальному” приложению: соберём структуру настроек и научимся заполнять её из argv. Пусть наше учебное приложение называется studyapp: оно печатает “план мини-сессии” — тему и имя пользователя.

Мы пока не обсуждаем богатый UX, коды выхода и --help как полноценный контракт. Здесь цель другая: научиться безопасно извлекать значения из массива argv и не запутаться в индексах.

Сначала опишем “настройки запуска” как структуру:

#include <string_view>

struct AppArgs {
    std::string_view name = "anonymous";
    std::string_view topic = "C++";
    bool help = false;
};

Теперь напишем функцию parse_args. Обратите внимание: она принимает argc/argv и возвращает заполненную структуру.

#include <string_view>

AppArgs parse_args(int argc, char* argv[]) {
    AppArgs a{};

    for (int i = 1; i < argc; ++i) {
        std::string_view arg = argv[i];

        if (arg == "--help" || arg == "-h") {
            a.help = true;
        } else if (arg == "--name" && i + 1 < argc) {
            a.name = argv[i + 1];
            ++i;
        } else if (arg == "--topic" && i + 1 < argc) {
            a.topic = argv[i + 1];
            ++i;
        }
    }
    return a;
}

И используем в main:

#include <iostream>

int main(int argc, char* argv[]) {
    AppArgs args = parse_args(argc, argv);

    if (args.help) {
        std::cout << "Usage: studyapp [--name NAME] [--topic TOPIC]\n";
        return 0;
    }

    std::cout << "User:  " << args.name << '\n';
    std::cout << "Topic: " << args.topic << '\n';
}

Пример запуска:

studyapp --name Alice --topic "argc/argv"

Вывод будет примерно такой:

User:  Alice
Topic: argc/argv

(Если вы передаёте аргументы через реальную командную строку, кавычки нужны, чтобы тема с пробелами пришла одним аргументом. Но нюансы shell-цитирования мы сегодня сознательно не разбираем.)

Обратите внимание на важное ограничение нашего parse_args: если пользователь напишет --name без значения, мы пока просто “проглотим” это молча (потому что условие i + 1 < argc не выполнится). Это нормально для сегодняшней темы: мы тренируем механику обхода.

6. Типичные ошибки при работе с argc/argv

Ошибка №1: читать argv[i + 1] без проверки i + 1 < argc.
Это самая частая причина падений в простых CLI. Пользователь случайно (или специально) оставляет --name последним аргументом, и программа лезет за границы массива. Лечится это скучно, но эффективно: сначала проверяем границу, потом читаем.

Ошибка №2: забывать ++i после обработки формата --key value.
Если не увеличить i, следующий аргумент (который был значением) попадёт в цикл ещё раз и начнёт интерпретироваться как отдельная опция или позиционный аргумент. В результате вы получаете “фантомные параметры” и странное поведение, которое тяжело отлаживать.

Ошибка №3: начинать разбор с i = 0 и пытаться трактовать argv[0] как опцию.
argv[0] — это обычно имя программы. Бывает, оно начинается с - или содержит странные символы, и ваш “парсер” вдруг решает, что это неизвестная опция. По умолчанию разбор опций начинают с i = 1.

Ошибка №4: сравнивать char* как указатели, а не как текст.
Конструкция if (argv[i] == "--help") сравнивает адреса, а не строки. Правильный путь — сравнивать содержимое: через std::string_view(arg) == "--help" или через std::string(argv[i]) == "--help" (но string_view обычно проще и дешевле).

Ошибка №5: смешивать форматы опций без чёткой логики.
Новичок часто пытается поддержать и --name value, и --name=value, и -nvalue, и ещё “на всякий случай” позиционные параметры — но без строгих правил. В итоге разбор превращается в кашу из if-ов. Лучше идти маленькими шагами: сначала один-два формата, линейный проход, предсказуемые правила, а расширять уже потом.

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