[Язык C] Управляющие операторы: циклы (глава 6)


Шестая глава в книжке Стивена Прата “C Primer Plus”

Цикл с предусловием:
for (инициализация; проверка; обновление)
            оператор

Инициализирующее выражение → проверяемое выражение → корректирующее выражение

Цикл с постусловием:
do
     оператор
while (выражение)

Оперативная часть повторяется, пока выражение не станет ложным (false), т.е. равным нулю.

Греческий философ Зенон и цикл for

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

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

На примере этой программы я впервые понял, насколько важно давать хорошие, годные имена переменным. Иначе программа превращается в настоящую кашу — со всеми этими n, x, y и проч. Мой вариант воплощения апории Зенона отличается от книжного, в нем рассматривается ситуация полета стрелы на сервере за некий бесконечно делимый тик на сервере:

#include <stdio.h>
int main (void)
{
int tick, limit;
double arrow, distance;
printf("Enter number of server ticks\n");
scanf("%d", &limit);
for (tick=1, arrow=1; tick <= limit; tick++, arrow=arrow/2)
{
distance = 1 - arrow;
printf("For %d tick arror would move %.6f distance\n",tick,distance);
}
getchar();getchar();
return 0;
}

Тогда как в книге представлено скорее упражнение с дробями.

…предположим, что стреле требуется одна секунда, чтобы пролететь первую половину пути. Затем ей понадобится 1/2 секунды, чтобы пролететь половину оставшегося пути, еще 1/4 секунды, чтобы преодолеть половину пути, который остался после этого, и т.д. Полное время пролета стрелы можно представить в виде следующей бесконечной последовательности:
1 + 1/2 + 1/4 + 1/8 + 1/16 + …

Таким образом в программе предлагается использовать ‘инкремент’ умножения знаменателя (англ. denominator) дроби на 2. Вот так (секунды я заменил тиками, это как-то более правильно):

#include <stdio.h>
int main (void)
{
int tick, limit;
double denominator, distance;
printf("Enter number of server ticks\n");
scanf("%d", &limit);
for (distance=0, tick=1, denominator=1; tick <= limit; tick++, denominator*=2)
{
distance+=1/denominator;
printf("For %d tick arror would make %.6f distance\n",tick,distance);
}
getchar();getchar();
return 0;
}

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

Программа «угадай число» — цикл с постусловием (do while)

#include <stdio.h>
int main (void)
{
int n;
do
{
printf("Guess my favorite number!\n");
scanf("%d", &n);
} while (n != 13);
printf("Yay! It's 13!\n");
getchar();getchar();
return 0;
}

Пример цикла. использующего возвращаемое значение функции

Довольно долго корпел над этой программой из-за косяков с типами данных, потом не сходились результаты из-за < вместо <= 🙂

// power.c - program to power up number

#include <stdio.h>
double power (double a, int b);

int main (void)
{
double a; // number which we want to power
int b; // power value
double result; // result of calculation

printf("Enter number and power or 'q' to quit:\n");

while (scanf("%lf%d", &a, &b) == 2)
{
     result = power (a, b);
     printf("%.3f powered by %d is %.3f\n", a, b, result);
     printf("Enter another number and power or 'q' to quit):\n");
}

getchar();getchar();
return 0;
}

double power (double a, int b)
{
 int count; // counter to get to power value
 double pow = 1.0; // power calculation (sum of multiplications)
 for (count = 1; count <= b; count++)
     pow *= a;
 return pow;
}

Начинаются программы с более сложной структурой, поэтому буду их публиковать с отступами (придется из-за этого переносить комментарии, но что делать…).

Программа из контрольных вопросов (#5), где надо было найти ошибки; вот уже исправленный вариант:

#include <stdio.h>
int main(void)
{

int i, j, list[10];

for (i = 0; i <= 9; i++)
        {
        list[i] = 2*i + 3;

        for (j = 0; j <= i; j++)
                printf(" %d", list[j]);
                printf("\n");
        }

getchar(); getchar();
return 0;
}

Вывод:

 3
 3 5
 3 5 7
 3 5 7 9
 3 5 7 9 11
 3 5 7 9 11 13
 3 5 7 9 11 13 15
 3 5 7 9 11 13 15 17
 3 5 7 9 11 13 15 17 19
 3 5 7 9 11 13 15 17 19 21

В этой программе столкнулся тут с тем, что нужно «реверсивно» понять, что программа должна делать. Разобраться помог «пошаговый» режим компирятора (в Borland это называется Step Over, хоткей F8), в нем все встало на свои места 🙂

Шестая программа из контрольных вопросов — с вложенными циклами. Пока читаешь про вложенные циклы, кажется, что это довольно сложно сложно. На деле же, не так страшна малютка;по крайне мере, если задача относительно простая, как здесь, вывести золотой квадрат:

$$$$$$$$
$$$$$$$$
$$$$$$$$
$$$$$$$$

Выводим так:

#include <stdio.h>
int main(void)
{

int col, row;

for (row = 1; row <= 4; row++)
        {
        for (col = 1; col <= 8; col++)
                printf("$");
        printf("\n");
        }
getchar();
return 0;
}

При выполнении контрольного вопроса #5, я понял, что запутался в цикле do while, т.к. мне казалось, что в примере, который там указан, нужно напечатать лишнюю «Bye!»:

#include <stdio.h>
int main(void)
{
int i = 0;
while (++i < 4)
printf("Hi! ");
do
printf("Bye! ");
while (i++ < 8);
return 0;
} 

Вывод:

Hi! Hi! Hi! Bye! Bye! Bye! Bye! Bye! 

Чтобы разобраться, я изменил цикл и в «пошаговом» режиме компилятора увидел, как оно работает, рекомендую:

#include <stdio.h>
int main(void)
{
int i = 1;

 do
   printf("%d ", i);
 while (i++ < 4);

getchar();
return 0;
}

Вообще, do while опасная штука.. Для аутентификации она отлично подходит, конечно, но запутаться довольно просто. Возможно, дело в привычке, надо набить руку 🙂

Про восьмую задачку я написал отдельную статью — уж больно задачка была получительная (не шути со scanf!).

Далее, меня накрыл ступор касательно очередности инкрементов.. Забавно — я только запомню что-то одно про С — так то, что ранее запоминал — выветривается из головы напрочь. Вот кейс, решил поэкспериментировать:

#include <stdio.h>
 int main(void)
 {
 char letter;
 letter = 'a';

 printf("%c is %d\n\n", letter, letter);

 printf("%c is %d... now a++: %c is %d\n",
 letter, letter, (letter)++, (letter)++);

 getchar(); getchar();

 return 0;
 }

Вывод:

a is 97

c is 99... now a++: b is 97

Чивоо? Начал курить и вспомнил, что сначала инкременты эти все срабатывают, а потом уже все печатается (а не по одному, как мне казалось) 🙂 Ну ёлы-палы. Вот что значит «мало практиковаться» (МИФ). Но черт побери, почему выводится c is 99... now a++: b is 97) ? Видимо это какая-то рекурсия, которая берется хрен знает откуда.. Боюсь представить, что можно натворить такими кривыми выражениями 😀 Тьфу-тьфу-тьфу, чур меня!

Упражнения по программированию для этой главы я также вынесу в отдельный пост, т.к. тут уже порядочно теста, а в упражнениях аж целых 18 заданий и многие из них непростые.


Запись опубликована в рубрике С (Си). Добавьте в закладки постоянную ссылку.

6 комментариев на «[Язык C] Управляющие операторы: циклы (глава 6)»

  1. ILIUS говорит:

    Чего-то в начале куски кода не форматированные.

    • Компьютерщик говорит:

      сначала я форматировал через <code>.. с ним легче постить, не надо париться за переносы. было вполне ок, т.к. не было циклов сложных. но теперь уже приходится через <pre>, чтобы отступы показывать

  2. ILIUS говорит:

    Решил полюбопытствовать то же. Позапускать примерчики. Оказывается и для Си есть онлайн компиляторы, очень удобно для целей изучения.
    Вот например: https://www.onlinegdb.com/online_c_compiler

    Еще зацепился глаз за повторяющиеся вызовы
    getchar();getchar();

    Понятно что это все поиграться, потыкаться. Но думаю будет интересно. «Проблема» в scanf, который оставляет, нажатый при вводе числа, Enter в подвисшем состоянии (ожидающим буфера) и первый getchar его глатает сразу. Погуглил — знающие люди рекомендуют сразу приучаться использовать заданный, ограниченный буфер. Это будет и безопаснее и не будет этих проблем. Соответственно использовать fgets и sscanf (или сразу strtol, если получаем число).

    • Компьютерщик говорит:

      getchar(); — эт вполне каноничный способ «держать» консоль открытой. Тут не только в scanf дело, любая программа закрывается, когда ее запускаешь из борланда без getchar(). Это выходит гораздо быстрее, чем билдить, открывать виндовую консоль и там запускать экзешник.

  3. ILIUS говорит:

    Ты не понял. Я говорю про то, почему тебе два раза приходится вызывать getchar(). Это не совсем правильно и это приходится делать именно из-за scanf. Я не говорю что не надо getchar() в конце вызывать, если ты хочешь сделать что-то типа «Press any key to exit…». Однозначно это удобно.

Добавить комментарий для ILIUS Отменить ответ

🇬🇧 Attention! Comments with URLs/email are not allowed.
🇷🇺 Комментарии со ссылками/email удаляются автоматически.