В предыдущих статьях в общих чертах были рассмотрены базовые понятия линейного программирования. То есть такого, в котором выполнение кода происходит сверху вниз(без исключений) и слева направо. В этой же статье мы рассмотрим возможные отклонения от этих правил. Зачем это нужно? Дело в том, что линейные программы не покрывают весь спектр существующих задач. К примеру, они не могут варьировать свое поведение в зависимости от действий пользователя. Вы можете сказать, что это не так, рассмотренный в предыдущей статье оператор ?: позволяет вводить некоторую вариацию. Это, конечно же, верно, однако далее мы покажем, что оператор ?: это упрошенная запись частного случая условного оператора if.







Условный оператор if



Рассмотрим следующую задачу. Необходимо написать программу которая бы могла преобразовывать время(для простоты, часы без минут) введенное в формате 24 часа в формат 12 часов + AM/PM. Мы пока не рассматриваем вопросы ввода и вывода, потому будем считать, что введенные пользователем данные находятся в переменной input, типа int, а вывод будет производится из переменной output, типа char[4](детально ввод и вывод будут рассмотрены в следующей статье, но общая идея будет изложена здесь в разделе функции). В таком случаи Код может иметь такой вид.
...
char output [4];
if(input>12) {
output[0]= '0'+(input-12)/10;
output[1]='0'+(input-12)%10;
output[2]='P';
output[3]='M';
} else {
output[0]= '0'+input/10;
output[1]='0'+input%10;
output[2]='A';
output[3]='M';
}
...

Выглядит довольно ужасающе, но мы сейчас разберем каждую строку по отдельности и станет более понятно. Основной причиной такого "ужасного" вида, является то, что C++ не поддерживает строки на уровне языка. Эта проблема исправляется с помощью дополнительного модуля, который будет рассмотрен в следующей статье. Так что, уже со следующей статьи работа со строками будет попроще. Этот пример поможет закрепить описанное в первой статье про массивы. Итак, первая строка нашего фрагмента просто объявляет массив символов(на данном этапе наша замена для строк). Во второй строке мы видим тот самый "условный оператор if". В данном случаи его следует читать так "если значение переменной input больше 12, то ..., иначе..." Вместо первого многоточия(в тексте, не в коде) следует подставить код из первого блока(до else), вместо второго, соответственно код из второго блока(после else). Как было сказано в предыдущей статье, отношение > это операция, с булевыми результатом. Условный оператор применяется как раз к таким значениям. Однако, как уже упоминалось, в C++ нет отдельного булевого типа, потому можно использовать любое значение(хотя это и не очень хорошая практика), соответственно нулевые и пустые(о которых речь пойдет дальше) значения будут трактоваться как "ложь", а все прочие, как "истина".
Итак, рассмотрим теперь два варианта:

1. Число введенное пользователем больше 12. Для наглядности возьмем, например, 22. Поскольку, 22>12 выполняется первый блок(блок обозначается фигурными скобками и служит для группировки операторов, подробности - в последующих статьях). Строка 3: первым делом мы вычитаем 12, чтоб получить часы в формате "12 часов". В нашем случаи результат 10. Далее берем от результата первую цифру(путем деления на 10), это 1. Далее производится работа с символами. Нам необходимо перевести цифру 1 в символ '1'. Поскольку в C++, символ это просто числ(номер символа в таблице ASCII), то мы к числу, соответствующему символу '0', прибавляем наше значение, то есть 1. Поскольку цифры в таблице символов идут подряд, то '0'+1='1', а '0'+8='8'. Но будьте внимательны, это работает только для цифр, то есть от 0 до 9. Теперь мы полностью разобрали правую часть строки 3. Ее результат '1', и операцией =, присваивается левой части выражения, то есть первому элементу массива символов output(массивы с C++, в отличии от таких языков, как Pascal, нумеруються с нуля). Строка 4: полностью аналогична строке 3, за тем лишь исключением, что вместо первой цифры мы берем вторую(вместо деления используем остаток от деления), а результат присваиваем второму элементу output(индекс 1). Строки 5 и 6 присваивают соответственно символы 'P' и 'M' третьему и четвёртому элементам массива. В нашем примере массив output будет содержать следующие значения
{'1', '0', 'P', 'M'}

При выводе на экран это будет выглядеть, как 10PM. Не очень презентабельно, но для учебного примера вполне годится(есть возможность улучшить самостоятельно).

2. Пусть теперь число введенное пользователем меньше 12, например 3. В таком случаи, будет выполнен второй блок кода. По сути он отличается от первого только двумя вещами: не вчитается 12 и вместо 'P' используется 'A'. Посему комментировать каждую строку этого блока смысла особо нет(полная аналогия с блоком 1). Но все же есть пара моментов, которые следует прояснить. В примере из пункта 1 число часов получалось двух-циферным, поскольку входящий параметр соответствовал 10 часам вечера. В данном же примере должно получится одноциферное число(3). Но полученный вывод в этом смысле слегка подкачал: 03AM. Лидирующий ноль получается в связи с тем, что в первый элемент массива записываются десятки числа, даже если их нет(то же верно и для блока 1). Такой вывод может показаться несколько неаккуратным, однако это всего лишь еще одна возможность для улучшения. Кстати оба эти улучшения(пробел между числом и AM/PM и "убирание" лидирующего нуля) могут быть выполнены в рамках тех возможностей языка, которые были описаны до этого момента в данном цикле статей.

Касательно оператора "?:", как и было указано ранее он является сокращенной записью условного оператора if. Пример:
Запись
if(a>3)
b=4;
else
b=5;

можно сократить до
b=(a>3)?4:5;

Оператор switch case



Бывают ситуации, когда одной проверки недостаточно. К примеру, необходимо различное поведение, если переменная равна 1, 2, 3 и 4. С помощью оператора if эта запись получится громоздкой и будет плохо читаться:
if(a==1) {
...
} else {
if(a==2){
...
} else {
if(a==3){
....
} else {
if(a==4){
...
} else {
...
}
}
}
}

Согласитесь, не очень удобно, и это всего 5 вариантов, а их бывает и больше(хотя обычно это свидетельствует о том, что есть проблемы в архитектуре). В таких ситуациях лучше использовать оператор switch case. Перепишем этот фрагмент, используя switch case:
switch(a){
case 1:
...
break;
case 2:
...
break;
case 3:
...
break;
case 4:
...
break;
default:
...
break;
}

В целом запись, достаточно прозрачна, но все же есть пара вещей, на которые следует обратить внимание. Во-первых, default. Это аналог последнего else, из предыдущего примера, то есть он выполняется тогда, когда значение a не соответствует ни одному из case'ов. Во-вторых, break. С этим немного сложнее. Для того, чтобы понять его назначение, case следует рассматривать как метки(коими они на самом деле и являются). Соответственно, когда выполнение кода доходит до оператора switch начинает выполняться код, который находится по соответствующей метке. При этом предыдущие блоки пропускаются. Но не последующие. Таким образом в отсутствии break, в случаи если значение a равно 3, выполнились бы блоки case 3, case 4 и default. Это иногда бывает полезно, но зачастую следует использовать break, в этом случаи последующие блоки кода(до фигурной скобки, закрывающей switch будут пропущены).

Циклы




В пердыдущих разделах мы рассмотрели один из видов нелинейности - условие. Здесь же мы рассмотрим другой - цикличность. Очень многие(да что там, практически все) задачи программирования требуют повторения одних и тех же действий. Это может быть цикл считывания команд пользователя или цикл подсчета некоторой итеративной величины. Циклы в C++ можно условно поделить на два вида, циклы со счетчиком и соответственно без счетчика. В разных ситуациях нужны различные циклы, но немного чаще, все же, используются циклы со счетчиком, потому о них поговорим в первую очередь.

Цикл со счетчиком - оператор for



Одна из распространенных задач на циклы - подсчет факториала числа(n!=1*2*3*4*...*n). Ее часто задают на всяческих собеседованиях, для того, чтобы выяснить знает ли интервьюируемый азы того или иного языка(и программирования в целом). Опять же будем предполагать, что значение n, типа int, введено пользователем, а значение result будет выведено ему по завершении фрагмента кода:


...
int result=1;
for(int i=2;i<=n;i++) {
result=result*i;
}
...

Приведенный код, для простоты, предполагает, что введенное пользователем число больше либо равно нулю(потому что для отрицательных чисел понятие факториала не существует), хотя в общем случаи это нужно проверять с помощью описанного ранее условного оператора. Итак, в первой строке мы просто присваиваем переменной result значение 1. Во второй строке мы видим оператор for. Внутри него, разделенные точками с запятой, находятся три выражения. Условно назовем их "инициализация", "проверка условия" и "инкремент". Блок цикла в фигурных скобках будем называть "телом" цикла(кстати, если блок состоит из одной строки, как в данном случаи, фигурные скобки не обязательны). Теперь рассмотрим то, как именно будет выполняются цикл.
1. Выполняется инициализация(в нашем случаи int i=1).
2. Выполняется проверка условия(i<=n). Если условие "ложно" осуществляется выход из цикла, в противном случаи переходит к следующему шагу.
3. Выполняется тело цикла.
4. Выполняется инкремент цикла и возврат к шагу 2.


Рассмотрим последовательность, для n=3. int i=2. i<=3 -"истинно" - продолжаем. result=result*i. i++. i<=3 -"истинно" - продолжаем. result=result*i. i++. i<=3 -"ложно" - выходим. Результат: переменная result содержит значение 6=3!.

Счетчик цикла начинается с двух, с целью пропустить избыточный шаг умножения на единицу.
Понимание порядка выполнения операций в цикле for, является основным для решения подобных задач.

Циклы без счетчика - операторы while и do-while



Отличие этого типа циклов состоит в отсутствии счетчика(кэп?:)). Соответственно, поскольку счетчика нет, его не нужно ни инициализировать, ни инкрементировать, то есть из трех выражений для for, остается только одно - условие продолжения цикла. Следующая задача, может быть решена с использованием цикла while. Пользователь вводит число, а программа отвечает ему тем же числом(программы, которые возвращают пользователю его ввод называются echo(эхо)), до тех пор, пока не будет введено число 0.
...
while(input!=0){
...
}
...

Мы снова опустили все подробности связанные с вводом и выводом, но думаю суть все равно ясна. Цикл while вначале проверяет условие, затем выполняет тело, потом снова проверяет условие и так далее.

Цикл do-while, отличается лишь тем, что вначале выполняется тело, а затем проверяется условие. Таким образом гарантируется, что цикл выполнится хотя бы один раз. Если использовать do-while, то код будет выглядеть вот так:
...
do {
...
} while(input!=0)
...

В целом это все о циклах. Однако, хочу еще раз заметить, что это один из наиболее часто используемых инструментов в программировании.

Функции



Что вообще такое функции? Какие мы знаем функции? К примеру, тригонометрические функции, такие как sin и tg. Можем вспомнить, еще log и exp. Все это математические функции, но все они имеют какие-то параметры(в данном случаи это одно число) и какой-то результат. Понятие функции в C++ значительно более общее, но так же связано с параметрами и результатом. Скажем так, функция - некоторый фрагмент кода, в общем случаи, имеющий входящие параметры и возвращающие некоторый результат. Пожалуй, самым важным свойством функции является то, что ее можно вызвать. Чуть позже, мы увидим, что функция может не иметь параметров(то есть, их число будет равно нулю) или же не возвращать результат(то есть, результат может иметь тип void), последние, в некоторых других языках еще называются процедурами. Рассмотрим простой пример: функция "сумма", которая возвращает сумму двух своих аргументов(к примеру целых чисел):





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

Рассмотрим подробно структуру этой функции. Первая строчка - так называемый заголовок функции, он состоит из типа возвращаемого результата(int в начале строки), имени функции(sum) и аргументов с их типами(соответственно, а типа int и b типа int). Далее идет "тело функции", то есть непосредственно операции, выполняемые при ее вызове. В данном случаи - это две операции, записанные в одной строке - сложение двух чисел и возврат результата функции(return). Теперь эту функцию, можно вызвать таким образом:
int c = sum(2,6);

Данная команда вызывает функцию с параметрами 2 и 6(соответственно, а будет равно 2, b - 6, а результат c - 8).

Зачем вообще нужны функции? На самом деле, причин много, но основных две. Во-первых, функции позволяют структурировать код, то есть выделять законченный фрагменты и давать им имена. Это невероятно полезно, поскольку значительно повышает читаемость кода(если, конечно, имена правильно подобраны), особенно когда объем написанного кода переваливает за 100 строк(что происходит почти всегда). Во-вторых, функции позволяют повторно использовать код, то есть если один и тот же фрагмент кода встречается больше чем один раз, его можно(и нужно) вынести в функцию, а затем заменить вхождения этого фрагмента на вызов функции. Это в свою очередь имеет следующие достоинства: во-первых, обычно меньше кода - меньше ошибок, если это не в ущерб читаемости, конечно, во-вторых, проще менять - в последствии изменения нужно будет сделать в одном месте, а не в нескольких(необходимость производить изменения одного и того же кода в многих местах - источник значительного количества ошибок в программировании).

Давайте теперь напишем первую законченную программу. Во-первых, любая программа на C++ содержит функцию main, которая выполняется при запуске программы. На данный момент мы будем объявлять функцию main без параметров. В последствии покажем, что задав параметры, можно использовать атрибуты командой строки переданные при запуске(иногда это бывает полезно). Итак, перепишем фрагмент кода, приведенный в разделе об условных операторах, в виде законченной программы.
int main(){
int input=15;
char output [4];
if(input>12) {
output[0]= '0'+(input-12)/10;
output[1]='0'+(input-12)%10;
output[2]='P';
output[3]='M';
} else {
output[0]= '0'+input/10;
output[1]='0'+input%10;
output[2]='A';
output[3]='M';
}
printf(output);
}

Эта программа не считывает пользовательское значение, а использует заранее заданное. Это сделано по поскольку, я бы не хотел углубляться в описание команд ввода вывода.
Как бы то ни было, мы видим, что в приведенном коде, объявлена функция main. Тип ее результата int. Правда, как мы видим она не возвращает результат. Это исключение из правил, функция main не обязана(но может) возвращать результат(подробнее об этом в следующей статье). В первой строке тела функции объявляется переменная input и ей приваривается значение. Далее, мы видим уже знакомый нам фрагмент. И завершается все вызовом функции printf. Функция printf выводит значение входящего параметра на экран. Данная функция примечательна тем, что ее возвращаемый результат, обычно, не имеет значения, главное в ней - вывод на экран.

Как мы видим в этой программе присутствуют два подобных фрагмента кода(фрагменты внутри условного оператора). Перепишем нашу программу, вынеся дублирующийся код в функцию.

void printFormatedTime(int hours, char meridiem) {
char output [4];
output[0]= '0'+hours/10;
output[1]='0'+hours%10;
output[2]=meridiem;
output[3]='M';
printf(output);
}

int main(){
int input=15;
if(input>12) {
printFormatedTime(input-12, 'P');
} else {
printFormatedTime(input, 'A');
}
}


Теперь программа короче(пустые строки и строки состоящие из одной фигурной скобки учитывать не следует) и выглядит проще. Обратим внимание на заголовок функции printFormatedTime, а конкретно на тип возвращаемого результата - он указан, как void. Это означает, что данная функция не возвращает результат, соответственно в ней отсутствует команда return, а вызывающий ее код, не должен использовать ее результат.

На сегодня это все, в следующей статье я подробнее расскажу о вводе-выводе и подключении библиотек. Успехов!
0

Комментарии

Для того, чтоб оставлять комментарии или зарегистрируйтесь.