Денис Злобин

Урок 10. Строки. Argc и Argv

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

denis@debian: ./calculator 12 + 2
12 + 2 = 14

Обрати внимание, что 12 - это строка, т.к. 12 состоит из двух символов: 1 и 2. На данный момент мы знаем, что символы в коде программы задаются с помощью типа char:

char a = '1';

А как представить строку в коде программы? Технически строка - это массив символов, значит, строку можно представить как массив символов:

char arr[] = {'1', '2'};

Обрати внимание на пустые квадратные скобки - это не опечатка. Компилятор может вывести за нас размер статического массива на этапе компиляции в том случае, если ты инициализировал массив при объявлении.

Чтобы вычислить размер массива, который был выведен автоматически на этапе компиляции, нужно размер массива поделить на размер одного элемента массива

char arr[] = {'1', '2'};
int size = sizeof(arr) / sizeof(arr[0]);
std::cout << "size of arr: " << size << '\n'; // Выведет 2

Для вычисления размера первого элемента я взял первый элемент массива - если массив был создан, первый элемент будет гарантированно существовать.

Увы, у такого представления строк есть нюанс: каждый раз вместе с указателем на массив необходимо передавать размер массива, иначе мы просто не узнаем, где заканчиваются границы массива! Было бы здорово, если бы мы знали, где заканчивается массив. И да, такая возможность уже реализована в мире С++ (вернее, в мире С, а из С была позаимствована в С++). Для конца строки используется специальный символ - ‘\0’, его еще называют терминирующим (завершающим) нулем - null terminated symbol. Наш массив с учетом терминирующего нуля должен выглядеть следующим образом

char arr[] = {'1', '2', '\0'};

В таком случае размер массива увеличится на 1 - это плата за то, чтобы гарантированно знать, где заканчивается массив символов.

Получается, что мы можем передать нашу строку в функцию, используя лишь указатель на массив:

#include <iostream>

void print(char* arr)
{
	std::cout << arr << '\n'; // Magic! Выведет: 12
}

int main()
{
	char arr[] = {'1', '2', '\0'};
	print(arr);
}

Почему std::cout вывел все значения из массива, а не адрес массива? Когда std::cout принимает указатель на char, он “понимает”, что это особый случай - ему передали для работы именно строку. Поэтому std::cout трактует arr как указатель на первый элемент массива символов, далее, используя арифметику указателей, он движется по массиву до тех пор, пока не встретит символ завершения массива \0.

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

Напомню, что массив - это коллекция элементов одного типа

char a[] = {'1', '2', '\0'};
char operation[] = {'+', '\0'};
char b[] = {'2', '\0'};

Вспомним, что a, b и operation - это указатели на первые элементы массивов. Т.е. мы их можем добавить в массив и получить массив указателей:

char a[] = {'1', '2', '\0'};
char operation[] = {'+', '\0'};
char b[] = {'2', '\0'};

char* argv[] = {a, operation, b};
int argc = 3;

argv - традиционное имя для массива аргументов командной строки (arguments vector), argc - традиционное имя для размера массива (arguments count).

Прочувствуй следующий момент, его понимание здорово поможет тебе при изучении ООП и в частности SOLID: мы собрали массив из указателей одного типа, но каждый указатель ссылается на свой массив. Размеры массивов разные, массивы хранят идеологически разную информацию, но это не мешает нам хранить все указатели вместе в одном массиве. Мы можем передавать массив указателей как единый объект, но работать с каждым указателем определенным способом. Например, значения по первому и третьему указателю мы должны будем преобразовывать к int, а в зависимости от значения по второму указателю будем совершать ту или иную математическую операцию.

Давай передадим массив указателей в функцию:

#include <iostream>

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

int main()
{
	char a[] = {'1', '2', '\0'};
	char operation[] = {'+', '\0'};
	char b[] = {'2', '\0'};

	char* argv[] = {a, operation, b};
	int argc = 3;
	
	print(argc, argv);
}

Обрати внимание на объявление функции void print(int argc, char** arr): char** arr - это указатель на массив указателей. Звучит намного проще, чем читается.

Лирическое отступление А как в незнакомом коде мы узнаем, что char** arr - это массив указателей, а не указатель на указатель? Указатель на указатель нужен, чтобы функция могла изменить значение указателя, т.е. адрес, который хранит указатель. Ответ: только через изучение уже существующего кода или документации. Увы, сырой указатель (raw pointer) не хранит никакой информации об исходном объекте.

argc и argv

Фух, наконец-то ты достаточно практически и теоретически подкован, чтобы начать работать с аргументами командной строки. У функции main есть версия с аргументами. В качестве аргументов main принимает параметры командной строки, которые передает пользователь. Заполнением этих аргументов занимается операционная система, нам же, разработчикам, остается только их использовать. Объявление main c аргументами тебе уже знакомо int main(int argc, char** argv) - именно такое объявление я использовал для объяснения работы с массивом указателей на строки. При работе с argc и argv нужно помнить о важном нюансе - первым элементом в массиве argv идет имя исполняемого модуля, т.е. argc всегда имеет значение как минимум 1. Продемонстрирую:

#include <iostream>

int main(int argc, char** argv)
{
	std::cout << "argc: " << argc << '\n'; // 1
	std::cout << "argv[0]: " << argv[0] << '\n'; // ./a.out
}

Тебя не смутило то, что в предыдущих уроках мы использовали объявление функции main вида int main(), а в этом уроке начали использовать int main(int argc, char** argv)? Согласно стандарту C++ существует два вида функции main https://en.cppreference.com/w/cpp/language/main_function.html одна с аргументами командной строки, другая - без. При этом в твоем коде может быть объявлена лишь одна версия функции main. По своей сути две версии функции main - это синтаксический сахар, компилятор всегда генерирует машинный код для версии main с аргументами, потому как это требование операционной системы.

Напишем минимальный код для обработки параметров командной строки и запустим наш бинарник с параметрами. Код:

#include <iostream>

int main(int argc, char** argv)
{
	std::cout << "argc: " << argc << '\n'; // 4
	std::cout << "argv[1]: " << argv[1] << '\n'; // 12
	std::cout << "argv[2]: " << argv[2] << '\n'; // +
	std::cout << "argv[3]: " << argv[3] << '\n'; // 2
}

Для запуска введи:

./a.out 12 + 2

В выводе ты должен получить:

argc: 4
argv[1]: 12
argv[2]: +
argv[3]: 2

Отлично! В этом уроке ты научился получать и обрабатывать параметры командной строки в нашем приложении. В следующем уроке мы напишем код для использования принятых аргументов для проведения математических расчетов.

На этом на сегодня всё, до встречи на следующем уроке!