Денис Злобин

Урок 11. Собираем всё вместе

Привет! В этом уроке мы применим все изученные ранее конструкции и подходы к нашему проекту и завершим работу над кодом консольного калькулятора. Итак, приступим.

Для начала проведем ревью архитектуры нашего проекта. Наш проект можно описать следующим образом:

main(argc, argv)
{
	run(argc, argv);
}

run(argc, argv)
{
	parse(argc, argv, data);
	calculate(data);
	output(data);
}

data
{
	value1;
	value2;
	operation;
	status;
	result;
};

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

Важное уточнение. Чтобы не раздувать текст урока, я осознанно пропускаю обработку ошибок.

Функция run Я рекомендую придерживаться следующей рекомендации: старайся делать тело функции main как можно короче. Во-первых, так её будет легче сопровождать: из функции main мы вызываем нашу пользовательскую функцию run и больше ничего не делаем - причины для внесения изменений в main сведены к минимуму. Во-вторых, так будет проще тестировать работу нашего приложения. Тема тестирования будет рассмотрена во второй части курса, посвященной ООП. Пока поверь мне на слово:)

Напишем код функции main:

int main(int argc, char** argv)
{
	run(argc, argv);
}

Просто и компактно.

Напишем код функции run:

void run(int argc, char** argv)
{
	Task task;
	parse(argc, argv, task);
	calculate(task);
	output(task);
}

Что такое Task? Task - это структура Структура - это коллекция данных, которые могут иметь разный тип. Если в массив мы могли добавить данные строго одного типа, то в структуру мы можем добавить как данные одного типа, так и разных типов. Ещё одно определение структуры - пользовательский тип данных, т.е. тип данных, который создает пользователь (программист). Для того, чтобы создать свой тип данных, его нужно объявить:

struct Task
{
	// данные
};

Объявление структуры начинается с ключевого слова struct, далее идет имя структуры, после чего идут фигурные скобки, после которых обязательно ставится точка с запятой. Традиционно пользовательский тип данных в С++ пишется с большой буквы, так его будет легко отличить в коде программы.

В фигурных скобках находится наша коллекция данных. Для нашей задачи в структуру имеет смысл добавить первое число, операцию, второе число, статус выполнения операции (успех или номер ошибки) и результат выполнения операции. Заполним фигурные скобки:

struct Task
{
	int value1;
	char operation;
	int value2;
	int status;
	int result;
};

Почему я решил использовать структуру? Благодаря использованию структуры я добавил немного абстракции в нашу программу: теперь все функции работают с конкретной сущностью - заданием на расчет, которое описывается входным набором данных и результатом выполнения.

Функция parse Функция parse парсит аргументы командной строки и записывает результат в структуру.

void parse(int argc, char** argv, Task& task)
{
	task.value1 = std::atoi(argv[1]);
	task.operation = *(argv[2]);
	task.value2 = std::atoi(argv[3]);
}

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

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

Для того, чтобы строку символов представить в целое число, необходимо использовать функцию std::atoi (alphabet to integer). На вход функция принимает указатель на строку, завершающуюся терминирующим нулем. В случае успеха функция возвращает результат преобразования, иначе - ноль.

Функция calculate Функция calculate производит математические вычисления, используя написанные нами ранее функции.

void calculate(Task& task)
{
	task.status = 0;
	switch(task.operation)
	{
		case '+':
			task.result = addition(task.value1, task.value2);
			break;
		case '-':
			task.result = subtraction(task.value1, task.value2);
			break;
		case '*':
			task.result = multiplication(task.value1, task.value2);
			break;
		case '/':
			task.status = division(task.value1, task.value2, task.result);
			break;
		case '^':
			task.result = power(task.value1, task.value2);
			break;
		default:
			task.status = 1;
	}
}

Для написания этой функции я использовал конструкцию switch-case. Во-первых, она удобнее конструкции if-else в случае нескольких условий. Во-вторых, она работает быстрее, чем if-else. Общий синтаксис конструкции switch-case

switch(переменная_для_сравнения)
{
	case значение1:
		действие1;
		break;
	case значениеN:
		действиеN;
		break;
	default:
		действие_по_умолчанию;
}

В скобках после ключевого слова switch записывается переменная (или выражение) для сравнения со значениями, которые записываются после ключевого слова case. Обрати внимание, что переменная для сравнения и значения могут быть только целочисленными переменными, т.е. для нашего случая можно использовать либо int, либо char. Переменная для сравнения последовательно сравнивается со значением каждого из case. Если значения совпадают, выполняется действие в case. Для того, чтобы выйти из switch-case необходимо использовать ключевое слово break. Если в case не написать break, то будет выполнено действие из следующего case и так будет продолжаться до первого break или окончания switch. Если ни один из case не будет совпадать со значением переменной для сравнения, то будет выполнен блок default - т.е. действие по умолчанию. Наличие default не обязательно, но является признаком хорошего стиля кода.

Функция output Функция output выводит результат вычислений в консоль. Функция принимает объект структуры по копии: структура содержит только тривиальные типы, копирование таких типов - дешевая операция. В зависимости от результата выполнения операции в консоль выводится либо результат вычисления, либо ошибка. В текущей реализации обрабатывается только ошибка деления на ноль, но при желании можно увеличить количество обрабатываемых ошибок.

void output(Task task)
{
	if (task.status == 0)
	{
		std::cout << task.value1 << ' ' << task.operation << ' ' << task.value2 << " = " << task.result << '\n';	
	}
	else if (task.status == -1)
	{
		std::cout << "Error! Division by zero!\n";
	}
	else if (task.status == 1)
	{
		std::cout << "Error! Unknown operation!\n";
	}
	else
	{
		std::cout << "Unknown error\n";
	}
}

Соберем всё вместе Добавим код написанных нами ранее математических функций и получим завершенный код нашего приложения.

#include <iostream>

int addition(int a, int b)
{
	return a + b;
}

int subtraction(int a, int b)
{
	return a - b;
}

int multiplication(int a, int b)
{
	return a * b;
}

int division(int a, int b, int& c)
{
	if (b == 0)
	{
		return -1;
	}
	c = a / b;
	return 0;
}

int power(int a, int b)
{
	int c = 1;
	for (int i = 0; i < b; ++i)
	{
		c = c * a;
	}
	return c;
}

struct Task
{
	int value1;
	char operation;
	int value2;
	int status;
	int result;
};

void parse(int argc, char** argv, Task& task)
{
	// TODO Добавить проверку, что argc равно 4
	task.value1 = std::atoi(argv[1]);
	task.operation = *(argv[2]);
	task.value2 = std::atoi(argv[3]);
}

void calculate(Task& task)
{
	task.status = 0;
	switch(task.operation)
	{
		case '+':
			task.result = addition(task.value1, task.value2);
			break;
		case '-':
			task.result = subtraction(task.value1, task.value2);
			break;
		case '*':
			task.result = multiplication(task.value1, task.value2);
			break;
		case '/':
			task.status = division(task.value1, task.value2, task.result);
			break;
		case '^':
			task.result = power(task.value1, task.value2);
			break;
		default:
			task.status = 1;
	}
}

void output(Task task)
{
	if (task.status == 0)
	{
		std::cout << task.value1 << ' ' << task.operation << ' ' << task.value2 << " = " << task.result << '\n';
	}
	else if (task.status == -1)
	{
		std::cout << "Error! Division by zero!\n";
	}
	else if (task.status == 1)
	{
		std::cout << "Error! Unknown operation!\n";
	}
	else
	{
		std::cout << "Unknown error\n";
	}
}

void run(int argc, char** argv)
{
	Task task;
	parse(argc, argv, task);
	calculate(task);
	output(task);
}

int main(int argc, char** argv)
{
	run(argc, argv);
}

Теперь можно собрать наш проект:

g++ main.cpp

И запустить:

denis@debian:~$ ./a.out 3 + 2
3 + 2 = 5
denis@debian:~$ ./a.out 3 - 2
3 - 2 = 1
denis@debian:~$ ./a.out 3 \* 2 
3 * 2 = 6
denis@debian:~$ 
denis@debian:~$ ./a.out 3 / 2
3 / 2 = 1
denis@debian:~$ ./a.out 3 ^ 2
3 ^ 2 = 9

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

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