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. Звучит как имена двух гномов из подвала линковщика, но на самом деле всё просто.
Смысл такой:
| Элемент | Что это | Как понимать |
|---|---|---|
|
количество аргументов | “сколько строк нам передали” |
|
массив 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-ов. Лучше идти маленькими шагами: сначала один-два формата, линейный проход, предсказуемые правила, а расширять уже потом.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ