basic-c/sections/06-cycles.tex

355 lines
35 KiB
TeX
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

\section{Циклы}
Теперь, когда мы узнали, что можно делать в программе с использованием условий, пришло время познакомиться с таким базовым понятием программирования, как цикл.
\frm{Цикл - это одно или несколько действий повторяющихся до тех пор пока не наступит условие, прекращающее эти действия.}
С помощью циклов в программировании выполняются все рутинные задачи, такие как поиск значений в больших наборах данных, создание разного рода прогрессий, построение графиков, сортировки, ожидание ответов на запросы, чтение потоков данных и многие другие.
\subsection{Операторы циклов}
\subsubsection{\code{while()}}
Базовый цикл в языке С записывается при помощи ключевого слова \code{while()} после которого в круглых скобках пишется условие. При истинности данного условия будет выполняться тело цикла, которое в свою очередь пишется в фигурных скобках. Общий внешний вид очень похож на условный оператор, с той лишь разницей, что по окончании выполнения операторов внутри фигурных скобок мы переходим не на следующую строку, а обратно в проверку условия.
Для примера, выведем на экран все числа в заданном промежутке, границы которого мы обозначим как \code{а} и \code{b}. Для этого нам необходимо их инициализировать, то есть объявить и задать начальные значения, и пока \code{а} меньше \code{b} заходить в тело цикла где мы будем выводить на экран и инкрементировать меньшее число, пока оно не станет равным второму. Как только числа сравняются условие входа в тело цикла перестанет быть истинным, и мы не зайдём в него, оказавшись на следующей после тела цикла строке.
\begin{lstlisting}[language=C,style=CCodeStyle]
int a = 10;
int b = 20;
while (a < b) {
printf("%d ", a++);
}
\end{lstlisting}
Запустим программу и убедимся что все работает:
\begin{verbatim}
$ ./program
10 11 12 13 14 15 16 17 18 19
$
\end{verbatim}
В данном коде мы использовали оператор инкремента в постфиксной записи, которая означает, что значение переменной \code{а} сначала будет передано функции вывода на экран, а уже потом увеличено на единицу.
\frm{Один проход тела цикла и возврат в управляющую конструкцию называется \textbf{итерацией}, этот термин можно очень часто услышать в беседах программистов, а зачастую и не только в беседах о программировании.}
Также допустимо использовать префиксную запись оператора инкремента, при которой, как не сложно догадаться, значение сначала будет увеличено, а уже потом передано функции вывода на экран.
\begin{lstlisting}[language=C,style=CCodeStyle]
int a = 10;
int b = 20;
while (a < b) {
printf("%d ", ++a);
}
\end{lstlisting}
Запустим снова, чтобы увидеть разницу: после запуска первого варианта мы увидели в терминале значения от 10 до 19, а после запуска второго варианта значения от 11 до 20.
\begin{verbatim}
$ ./program
11 12 13 14 15 16 17 18 19 20
$
\end{verbatim}
Давайте напишем ещё один пример, в котором подсчитаем сколько чётных чисел на промежутке от \code{а} до \code{b}. для этого нам понадобится циклически пройтись по всем числам от \code{а} до \code{b} и в каждой итерации цикла, то есть для каждого числа, сделать проверку, является ли число чётным. Если является - увеличить счётчик чётных чисел для заданного промежутка. После того, как перестанет выполняться условие в скобках и наш цикл будет завершён, выведем в консоль количество чётных чисел.
\begin{lstlisting}[language=C,style=CCodeStyle]
int a = 10;
int b = 20;
int evens = 0;
while (a < b) {
if (a % 2 == 0)
evens++;
a++;
}
printf("There are %d even numbers in sequence", evens);
\end{lstlisting}
Запустим нашу программу и убедимся в правильности ее работы.
\begin{verbatim}
$ ./program
There are 5 even numbers in sequence
$
\end{verbatim}
Программа выдала результат: пять чётных чисел, давайте пересчитаем вручную: 10, 12, 14, 16, 18, значение 20 не войдёт в результат работы программы, поскольку на последней итерации, когда \code{а = 20} условие в круглых скобках окажется ложным: двадцать не меньше двадцати. А раз условие не выполнено мы не попадаем в тело цикла.
Цикл \code{while()} используется когда мы не можем достоверно сказать сколько итераций нам понадобится выполнить. На этом примере мы видим что тело цикла может быть любой сложности, оно может содержать как условные операторы так и другие циклы.
\subsubsection{\code{do \{\} while();}}
Простой цикл \code{while()}, по очевидным причинам, попадает в категорию циклов с предусловием. Сначала программа проверяет условие, затем, по результатам этой проверки, либо выполняет либо не выполняет тело цикла. А раз есть циклы с предусловием, то логично предположить наличие циклов с постусловием. Как ни удивительно, в языке С есть и такой. Это разновидность цикла \code{while()}, который записывается как ключевое слово \code{do \{тело цикла\} while (условие);}, в этой конструкции сначала гарантированно один раз выполнится тело цикла, и только потом будет принято решение, нужно ли выполнять его снова. Такие циклы широко применяются для проверки пользовательского ввода до получения приемлемого результата и для ожидания ответов на запросы, что логично, поскольку глупо было бы ожидать ответ, не послав запрос.
Например, мы пишем калькулятор, и знаем, что в арифметике числа нельзя делить на ноль. Предположим, что наш пользователь этого не знает. Что ж, будем предлагать пользователю ввести делитель, пока он не введёт что-то отличное от нуля. Если пользовательский ввод равен нулю, значит нам нужно спросить его снова. Это и будет условием для очередной итерации цикла. Когда пользователь введёт удовлетворяющее нашему условию число произведём необходимые подсчёты и выведем их результаты в консоль.
\begin{lstlisting}[language=C,style=CCodeStyle]
int input;
do {
printf("Enter a divider for 100, ");
printf("remember, you can't divide by zero: ");
scanf("%d", &input);
} while (input == 0);
printf("100 / %d = %d", input, 100 / input);
\end{lstlisting}
Запустим программу. Каждый раз когда мы вводим ноль, программа будет повторно задавать нам вопрос.
\begin{verbatim}
$ ./program
Enter a divider for 100, remember, you can't divide by zero: 0
Enter a divider for 100, remember, you can't divide by zero: 0
Enter a divider for 100, remember, you can't divide by zero: 5
100 / 5 = 20
$
\end{verbatim}
Когда введем любое другое число получим результат нашего деления.
\subsubsection{\code{for(;;)}}
Зачастую, складываются ситуации, когда мы точно знаем, сколько итераций цикла нам понадобится, например, когда мы последовательно проверяем содержимое созданного нами числового ряда, или заполняем значениями таблицы, границы которых заранее известны. В конце концов, для подсчёта среднего арифметического некоторого конечного числового ряда. В этих ситуациях принято использовать. Это цикл с предусловием, где заранее отведено место для инициализации переменной-счётчика, условия захода в следующую итерацию цикла и изменения переменной счетчика. В более поздних версиях С (C99) появилась возможность объявлять переменную счетчик прямо в управляющей конструкции.
\frm{Для применения более позднего стандарта в проекте, необходимо установить специальный ключ компиляции \code{-std=c99}, то есть полная команда компиляции исходного кода для транслятора GCC будет выглядеть так:
\centering\code{gcc program.c -std=c99 -o program}}
В классическом С необходимо объявить переменную счетчик заранее, а в управляющей конструкции можно только задать ей начальное значение.
Условие захода в следующую итерацию цикла это логическое выражение, которое может принимать два значения: истина и ложь. Если условие истинно - идём на следующую итерацию (исполняем тело цикла), если ложно выходим из цикла и перемещаемся на следующий после цикла оператор.
\begin{lstlisting}[language=C,style=CCodeStyle]
// classic style
int i;
for (i = 0; /*condition*/; /*increment*/) {
// body
}
// C99 and later
for(int i = 0; /*condition*/; /*increment*/) {
// body
}
// Example:
int i;
for (i = 0; i < 5; i++) {
printf("%d ", i);
}
\end{lstlisting}
Цикл из примера выше выведет на экран числа от нуля до четырёх. На каждой итерации мы будем инкрементировать значение \code{i}, соответственно, пока логическое условие \code{i < 5} верно, мы будем заходить в тело цикла, а как только \code{i} станет равным пяти, логическое условие вернет ложь (\code{0}) и мы из него выйдем.
\begin{verbatim}
$ ./program
0 1 2 3 4
$
\end{verbatim}
\subsection{Управление циклами}
Операторы, которые осуществляют управление циклами называют операторами безусловного перехода, поскольку они просто перемещают выполнение программы на другую строку, невзирая ни на что. Программист должен сам описать логику такого перемещения, используя условные операторы.
\paragraph{Оператор \code{continue;}}
нужен для того, чтобы программа проигнорировала оставшиеся действия на текущей итерации цикла, часто используется для отбрасывания неподходящих значений при переборе больших объёмов данных. Оператор \code{continue} просто напросто передаёт управление логической конструкции цикла.
\paragraph{Оператор \code{break;}}
используется для того, чтобы выйти за пределы цикла, мы сразу попадаем к следующему после цикла оператору, без передачи управления логической конструкции.
\subsection{Практические задачи}
\paragraph{Возведение в степень}
Решим немного более сложную, чем выведение в консоль числовых рядов задачу возведения числа в степень. Язык С не предоставляет оператора возведения в степень по умолчанию, как это делают некоторые другие языки высокого уровня, такие как Python, поэтому для этой математической операции нам нужно подключать специальную математическую библиотеку. Но иногда это может оказаться излишним, ведь не так сложно написать собственную функцию, которая бы делала это.
\frm{Как известно, возведение в степень - это последовательное умножение основания на само себя указанное количество раз.}
А раз заранее известно, сколько раз мы будем умножать основание само на себя, это работа для цикла \code{for(;;)}. Объявим переменную-итератор \code{i}, переменную-основание, переменную-показатель и переменную, в которую будем сохранять промежуточные и конечный результаты.
\begin{lstlisting}[language=C,style=CCodeStyle]
int i;
int base;
int significative;
int result = 1;
\end{lstlisting}
Логика работы следующая: результатом работы будет совокупность результатов предыдущих итераций умноженных на основание.
\begin{lstlisting}[language=C,style=CCodeStyle]
for (i = 0; i < significative; i++) {
result = result * base;
}
\end{lstlisting}
Запись вида \code{result = result * base;} можно сократить до \code{result *= base;}, и выведем результаты работы цикла в консоль.
\begin{lstlisting}[language=C,style=CCodeStyle]
for (i = 0; i < significative; i++) {
result *= base;
}
printf("%d powered by %d is %d \n", base, significative, result);
\end{lstlisting}
Конечно, мы можем спросить у пользователя какое число, и в какую степень он хочет возвести, для этого применим уже привычные нам конструкции. Так, весь код программы будет иметь следующий вид:
\begin{lstlisting}[language=C,style=CCodeStyle]
#include<stdio.h>
int main(int argc, char *argv[]) {
int i;
int base;
int significative;
int result = 1;
printf("Enter base: ");
scanf("%d", &base);
printf("Enter significative: ");
scanf("%d", &significative);
for (i = 0; i < significative; i++) {
result *= base;
}
printf("%d powered by %d is %d \n", base, significative, result);
}
\end{lstlisting}
Запустим нашу программу, введем для базы значение два, для показателя десять. Убедимся, что наша программа работает корректно, $2^{10}=1024$.
\begin{verbatim}
$ ./program
Enter base: 2
Enter significative: 10
2 powered by 10 is 1024
$
\end{verbatim}
Вообще, степени двойки, пожалуй, самые популярные в программировании числа.
\paragraph{Простое число}
Решим ещё одну несложную задачу. Напишем программу, которая будет определять, является ли введённое пользователем число простым. Мы напишем не самый быстрый и оптимальный алгоритм, но постараемся использовать все доступные нам конструкции. То есть, эта задача призвана продемонстрировать возможности языка, а не улучшить или заменить существующие, более оптимальные алгоритмы проверки.
\frm{Что такое простое число? Это такое число, которое имеет ровно два делителя с целочисленным результатом - единицу и само себя.}
Наша программа будет запрашивать у пользователя число и определять, простое оно или нет. Для этого заведём переменную, и привычными нам функциями, попросим пользователя ввести число, которое положим в неё. Для подсчетов нам понадобятся дополнительные переменные, например, переменная которая будет хранить количество делителей, назовем ее \code{dividers} и переменная итератор \code{i} значение которой будет увеличиваться от единицы до введенного пользователем числа.
\begin{lstlisting}[language=C,style=CCodeStyle]
int number;
int dividers = 0, i = 1;
printf("Enter number: ");
scanf("%d", &number);
\end{lstlisting}
Поскольку мы не знаем, сколько итераций понадобится, напишем цикл \code{while}, и пройдемся от единицы, которую мы записали в переменную \code{i} до введённого пользователем числа. После того как мы переберем все возможные варианты от единицы до введённого пользователем числа выведем в консоль получившийся результат. Напишем введённое число, а дальше предоставим программе выбор ставить ли частицу <<не>> при помощи заполнителя \code{\%s} и тернарного оператора. В случае истинности условия \code{dividers == 2} тернарный оператор вернет пустую строку, в случае ложности, вернет частицу <<не>>. Обратите внимание на то, как оператор вывода на экран написан в несколько срок. Такие разделения на несколько строк можно часто увидеть, если оператор длинный и может, например, выйти за пределы экрана.
\begin{lstlisting}[language=C,style=CCodeStyle]
while (i <= number) {
// here will be the check algorithm
}
printf("Number %d is%s prime",
number,
(dividers == 2) ? "" : " not"
);
\end{lstlisting}
Итак, что будет происходить на каждой итерации цикла? Мы будем проверять делится ли введённое пользователем число на текущее значение итератора. Если остаток от деления исходного числа на текущий делитель равен нулю, то мы увеличим счетчик количества целочисленных делителей для данного числа. Тело цикла примет следующий вид:
\begin{lstlisting}[language=C,style=CCodeStyle]
if (number++ % i == 0) {
dividers++;
} else {
continue;
}
if (dividers == 3)
break;
\end{lstlisting}
Если количество целочисленных делителей не изменилось, то мы прекратим текущую итерацию цикла при помощи ключевого слова \code{continue}. Как мы знаем, оператор \code{continue} передаст управление в логическую конструкцию цикла, заставив программу проигнорировать все дальнейшие инструкции в рамках текущей итерации. Если количество целочисленных делителей достигнет трёх, что будет означать нецелесообразность дальнейших вычислений, мы разорвем цикл при помощи ключевого слова \code{break}. И полный получившийся код приложения будет такой:
\begin{lstlisting}[language=C,style=CCodeStyle]
#include<stdio.h>
int main(int argc, char *argv[]) {
int number;
int dividers = 0, i = 1;
printf("Enter number: ");
scanf("%d", &number);
while (i <= number) {
if (number++ % i == 0) {
dividers++;
} else {
continue;
}
if (dividers == 3)
break;
}
printf("Number %d is%s prime",
number,
(dividers == 2) ? "" : " not"
);
}
\end{lstlisting}
Естественно, повторимся, этот код можно оптимизировать по множеству направлений, как минимум, сократив как количество проверок, так и границы проверок (нет смысла проверять числа больше, чем $\sqrt{number}$). Дополнительно можно не проверять чётные числа, например.
\begin{verbatim}
$ ./program
Enter number: 2
Number 2 is not prime
$ ./program
Enter number: 7
Number 7 is prime
$ ./program
Enter number: 457
Number 457 is prime
$ ./program
Enter number: 1457
Number 1457 is not prime
$
\end{verbatim}
\subsection{Множественный выбор \code{switch()\{\}}}
Пришло время поговорить об операторе множественного выбора \code{switch()\{\}}. Тем более, что теперь мы обладаем всеми необходимыми для этого знаниями. Оператор множественного выбора используется когда мы хотим описать действия для какого-то ограниченного количества условий. В отличие от оператора \code{if()}, который может использоваться также и для проверки диапазонов значений.
\frm{Это не совсем точно, потому что \code{switch()\{\}} в языке C тоже может использоваться для проверки диапазонов значений, но это довольно редко применяется, а в С++ и других языках может вовсе не работать. Не стоит пугаться, увидев \code{case 5 ... 50:} это как раз проверка диапазона целочисленных значений от 5 до 50 включительно.}
Удобство применения того или иного оператора, естественно, зависит от задачи. Довольно важным ограничением оператора \code{switch(){}} в языке С является то, что он умеет работать только с целыми числами. Для примера, напишем свой собственный маленький калькулятор. Сразу предположим, что калькулятором будет пользоваться внимательный человек, который понимает, что арифметические действия можно совершать только с числами, и умножать строки или символы - не получится.
Заведем в нашей программе переменные типа \code{float}, которые будут хранить операнды числа над которыми будут производиться действия. И переменную типа \code{char} для хранения операции. Спросим у пользователя, какие числа он хочет посчитать, для этого используем уже привычную нам связку \code{printf();}/\code{scanf();}. Далее, таким же образом предложим пользователю ввести действие, действие мы закодируем в виде чисел:
\begin{itemize}
\item 1 сложение;
\item 2 вычитание;
\item 3 умножение
\item 4 деление.
\end{itemize}
Здесь вступает в силу тот факт, что на ноль делить нельзя, поэтому нам нужно не дать пользователю ввести в качестве второго операнда \code{"0"}, если в качестве оператора он выбрал деление. Конечно, это можно оставить на усмотрение пользователя, но мы, как сознательные программисты, не дадим пользователю в арифметическом порыве сломать нашу программу. Совершенно очевидно, что просто скопировать ввод будет неправильным действием. Для того чтобы не дать пользователю сделать неправильный ввод введем в прорамму условие: если пользователь выбрал деление, используя цикл \code{do \{\} while();} будем просить его ввести второй операнд отличный от нуля, а если выбранный оператор не является делением, то просто попросим пользователя ввести второе число, без проверок и повторений.
\begin{lstlisting}[language=C,style=CCodeStyle]
if (operator == 4) {
do {
printf("/nEnter second operand: ");
scanf("%f", &second);
} while (second == 0);
} else {
printf("/nEnter second operand: ");
scanf("%f", &second);
}
\end{lstlisting}
Если мы воспользуемся нашими знаниями на текущий момент, то мы напишем примерно следующее: если оператор равен единице, то делать это, в противном случае, если оператор равен двум, то делать вот это, и так далее, описывали бы все возможные действия условными операторами. Получился бы весьма громоздкий код.
\begin{lstlisting}[language=C,style=CCodeStyle]
if (operator == 1) {
//...
} else if (operator == 2) {
//...
}
//...
\end{lstlisting}
Но хорошая новость в том, что существует гораздо более удобный оператор \code{switch()\{\}}. Воспользуемся им относительно переменной \code{operator}. Разделим действия оператора \code{switch()\{\}} на несколько так называемых \textit{кейсов}.
\begin{lstlisting}[language=C,style=CCodeStyle]
switch (operator) {
case 1:
result = first + second;
case 2:
result = first - second;
case 3:
result = first * second;
case 4:
result = first / second;
default:
printf("Unknown operator\n");
}
\end{lstlisting}
Оператор \code{switch()\{\}} последовательно проверит входящую переменную на соответствие описанным в кейсах значениях. В случае, если значение совпадёт, будет выполнен блок кода до оператора \code{break;}, если же значение переменной не совпадёт ни с одним из описанный в кейсах, выполнится блок по умолчанию \code{default}.
\frm{Важно помнить, что в случае отсутствия внутри \code{case} оператора \code{break;}, программа будет выполнять последующие кейсы, пока не найдёт \code{break;} или пока не закончится конструкция \code{switch()\{\}}, то есть пока не встретится её закрывающая фигурная скобка.}
В кейсах мы опишем присваивание результата в переменную \code{result}, а после выхода из \code{switch()\{\}} - вывод результата в консоль. Кейсом по умолчанию будет вывод пользователю сообщения о невозможности распознать оператор. Так получается, что даже если мы ввели неизвестный оператор, программа попытается вывести в консоль результат, что неприемлемо. Поэтому кейс по умолчанию должен содержать также и оператор \code{return 1;} вынуждающий программу экстренно завершиться с кодом ошибки \code{1}. Получится такой код:
\begin{lstlisting}[language=C,style=CCodeStyle]
#include <stdio.h>
int main (int argc, char *argv[]) {
float first;
float second;
float result;
int operator;
printf("Enter first operand: ");
scanf("%f", &first);
printf("/nEnter 1 for (+), 2 for (-), 3 for (*), 4 for (/): ");
scanf("%d", &operator);
if (operator == 4) {
do {
printf("/nEnter second operand: ");
scanf("%f", &second);
} while (second == 0);
} else {
printf("/nEnter second operand: ");
scanf("%f", &second);
}
switch (operator) {
case 1:
result = first + second;
break;
case 2:
result = first - second;
break;
case 3:
result = first * second;
break;
case 4:
result = first / second;
break;
default:
printf("Unknown operator\n");
return 1;
}
printf("Result is: %f \n", result);
return 0;
}
\end{lstlisting}
Запустив описанный нами калькулятор, убедимся что все работает. Сымитируем нерадивого пользователя и несколько раз попробуем ввести при использовании четвёртого оператора цифру ноль, программа естественно не даст нам этого сделать.
\begin{verbatim}
$ ./program
Enter first operand: 10
Enter 1 for (+), 2 for (-), 3 for (*), 4 for (/): 4
Enter second operand: 0
Enter second operand: 0
Enter second operand: 0
Enter second operand: 3
Result is: 3.333333
$ ./program
Enter first operand: 10
Enter 1 for (+), 2 for (-), 3 for (*), 4 for (/): 5
Unknown operator
$
\end{verbatim}
На основе этого кода можно описывать любые виды меню, описать поведение программ, которые должны опираться на получаемые извне команды или описывать конечные автоматы.