Циклы
Не будем описывать все возможные виды циклов, которые существуют в языках программирования, ограничимся основными.
- Универсальный: for (int i=0; i
- Универсальный в BASIC-стиле: FOR I=1 TO LENGHT STEP 1 // тело цикла NEXT
- Цикл «пока»: while (условие) { // тело цикла }
- Цикл с проверкой в конце: do { // тело цикла } while (условие)
- Цикл «for each»: for each (некое множество) { // тело цикла }
С одной стороны, программисты хотят иметь удобные циклы на все случаи жизни, что поощряет их разнообразие в языках программирования. С другой стороны, существует универсальный оператор цикла, который может заменить собой многие из этих циклов. Он же универсален! Интересный взляд на циклы можно обнаружить в статье Андрея Андреева.
Надо заметить, что мысли о большей роли универсального цикла имеет под собой прочные и обосноснованные аргументы. Именно поэтому не стоило перечислять все виды циклов в начале статьи. Мы должны придумать такой оператор цикла, который действительно позволит заменить собой все остальные. Можно заметить, что универсальный оператор цикла C/C++ имеет недостаток. С его помощью нельзя проверить условие в конце цикла. Это проблему надо решить.
Для начала рассмотрим подробнее устройство цикла в C/C++:

Действия, выполняемые в цикле, графически можно изобразить так:

«Допиливаем» этот оператор, за основу берём придуманный нами «симметричный скобочный» стиль. Си-шный стиль меняем на свой:
(for int i=0; i
А потом пристально его рассмотрим. В универсальном цикле после ключего слова идут выражения, инициализирующие цикл. Количество этих выражений может быть равным нулю, единице и больше. Далее — условие продолжения цикла. Количество условий — ноль или единица. Далее — выражения, выполняющиеся в конце цикла. Количество этих выражений — нуль, единица и больше.
Первым делом в этом цикле хочется заменить ключевое слово «for» («для»). Для программ, которые пишутся на русском языке, самое уместное ключевое слово — это слово «цикл». Просто называем вещи своими именами. Что наиболее подходит для английского языка? В языке Ada используют ключевое слово «loop» («петля»). Не знаю, почему там не прижилось «cycle».
Далее перемещаем это ключевое слово и размещаем между выражениями инициализации цикла и условием продолжения цикла. Ведь так гораздо логичнее. Ведь первым выражением, которое циклически выполняется, является проверка условия. Инициализация же выполняется лишь одиножды. Как ещё объявить и инициализировать переменые, область видимости которых ограничена рамками цикла? Помещая ключевое слово после этой инициализации, мы подчеркиваем роль кода перед этим словом и после его.
Ну и третий момент — заменяет «i++» на «++i» по причинам, которые объясняются в другой статье. В итоге получаем:
(int i=0
loop i
Или в более общем виде: (инициализирующие выражения
loop условие; выражения в конце цикла // тело цикла )
Изобразим это графически:

Суть этого цикла та же, что и в Си. Отличаются они лишь синтаксисом, внешним видом, семантика на 100% та же. Наша гипотетическая среда разработки могла бы отобразить это так:
( int i=0 loop i // ... ( int j=0 loop j // ... ( int k=0 loop k // ... ) ) )
А как должен выглядеть цикл «пока»? От универсального цикла его отличает отсутствие инициализирующих выражений и выражений, которые выполняются в конце цикла. В C/C++ такое «обрезание» универсального цикла выглядит так: for (;условие;) Этой кошке можно отрезать хвост по частям до полного его исчезновения: for (инициализирующ.выр-я; условие; выр-я в конце цикла) // исходный вид for (; условие; выражения в конце цикла) // нет инициализации for (инициализирующ.выр-я;; выр-я в конце цикла) // нет условия for (инициализирующ.выр-я; условие;) // нет выр-й в конце for (инициализирующ.выр-я;;) // только инициализация for (; условие;) // подобен while(условие) for (;; выр-я в конце цикла) // только выр-я в конце for (;;) // вообще пусто
А теперь то же, но в «симметричном скобочном» стиле:
( инициализирующ.выр-я loop условие; выр-я в конце цикла // ... ( loop условие; выр-я в конце цикла // ... ( инициализирующ.выр-я loop ; выр-я в конце цикла // ... ( инициализирующ.выр-я loop условие; // ... ) ) ) )
( инициализирующ.выр-я loop ; // ... ( loop условие; // ... ( loop ; выр-я в конце цикла // ... ( loop ; // ... ) ) ) )
Гибкость — не меньшая, чем в C/C++, но при большей лаконичности. Это гибкость — ещё и в том, что мы собираемся применять этот цикл вместо того, что называют «циклом с проверкой в конце». Чем отличается цикл «do {тело цикла} while (условие)» от цикла «while (условие) {тело цикла}»? Да тем, что во втором случае решение о продолжении цикла принимается в конце цикла, а впервом случае — в начале. Об этом нам недвусмысленно говорит фраза «while (условие)»: если она стоит вначале, то и проверка вначале. Если в конце, то и проверка в конце.
Универсальный же цикл «for (выражения инициализации; условие; выражения в конце цикла» не похож на циклы «while» и «do while» тем, что содержит выражения, нарушающие порядок «слева направо, сверху вниз». Если считать этот вид цикла некой функций с тремя операндами, то третий операнд (выражения в конце цикла) выбивается из общего ряда. Эта особенность привычна программистам и ни у кого не вызывает отторжения.
Наша идея состоит в том, чтобы переместив строку с «операндами» цикла в конец, заставить его работать как цикл «do {тело цикла} while (условие)»: ( // тело цикла инициализирующ.выр-я loop условие; выр-я в конце цикла)
Графически это можно изобразить так:

Ниже — действия, которые в этом цикле выполняются:

Непривычно? Только на первый взгляд. Это всё тот же универсальный цикл, вот только условие теперь проверяется не в начале, а в конце. А вот первый «операнд» (инициализирующие выражения) выполняется единожды перед тела выполнением цикла — как обычно. И третий операнд выполняется как обычно в конце. Так что такие циклы не вызовут аллергии у программистов. А среда разработки поможет своей графикой разобраться, где мухи, а где котлеты:
( // тело цикла ( // ... ( // ... ( // ... loop ; ) // ... loop ; выр-я в конце цикла ) // ... инициализирующ.выр-я loop условие; выр-я в конце цикла ) // ... инициализирующ.выр-я loop условие; выр-я в конце цикла )
Получился вполне себе симпатичный цикл с проверкой внизу. Если не ограничивать свою фантазию, можно сделать аналогичный
цикл с проверкой условия в средине! Вот такой:

А это — действия в нём:

Среда разработки покажет программисту примерно такое:
( // тело цикла ( // вложенный цикл с проверкой условия в средине инициализирующ.выр-я loop условие; выр-я в конце цикла // продолжение тела вложенного цикла ) // ... инициализирующ.выр-я loop условие; выр-я в конце цикла // ... // продолжение тела цикла )
Зачем это надо? Так ведь нередки циклы, где проверка условия выхода проверяется где-то внутри цикла. Допусти, так: for (;;) { // тело цикла if (условие) break; // продолжение тела цикла }
Мы же, перемещая строку с описанием цикла в средину, позволить сэкономить на операторе «if». Так что нами описан не только рабочий, но и интересный вариант. Выгоды от применения от такого цикла очевидна: его параметры записываются унифицированным способом. Меняется только место проверки условия, которое совпадает с положением управляющей строки цикла. А какие проблемы могут от этого возникнуть?
- Непривычная для программистов вид цикла, когда он употреблён в конце или средине.
- Усложнение компилятора. Ему надо будет научиться находить ключевое слово «loop» в самых разных местах цикла. Компилятор должен будет «заглядывать» далеко вперёд в поисках «loop»: ( // куча операторов // .. ... loop ... )
Всё ли описано? Нет, ещё предстоит рассмотреть цикл «для каждого» («for each»), а так же операторы «продолжение цикла» и «выход из цикла».
Цикл «для каждого»
Этот вид циклов описывает циклически выполняющиеся действия для каждого элемента некого множества. Почему этот цикл отсутствует в C/C++? На это есть несколько причин. Они касаются особенно C, потому что в C++ появились некоторые «костыли», которые позволяют в некоторых случаях заиметь в своём распоряжении «for each».
- Во-первых, в C нет такой абстракции, как «множество». В языке нет возможности передать некий объект, не заботясь о его сущности. Массив ли это, список, стек или дерево — как язык поможет определить конкретный вид множества, если, к примеру, указатель на него был передан в функцию?
- Во-вторых, как определить количество элементов в этом множестве?
- В-третьих, каким образом перебрать все элементы этого множества?
От этих вопросов нельзя отмахнуться, как от несущественных. Но они связаны м понятием множества, которые мы ещё не рассмотрели. Поэтому этот вид цикла рассмотрим позже, в статье о цикле «для каждого» («for each»).
P.S. Ответ на комментарий Михаила. Позвольте процитировать Википедию: «Цикл Дейкстры удобен при реализации некоторых специфических повторяющихся вычислений». Конструкции, которыми можно применять во всех ситуациях или их большинстве предпочтительнее тех, которые решают специфичные задачи. Особенно если первые просты и наглядны. Цикл
for (инициализация; проверка условия в начале; действия в конце цикла) { тело цикла} легко меняется на цикл (инициализация
loop проверка условия в начале; действия в конце цикла тело цикла) Цикл
while do (проверка условия в начале) { тело цикла} соответственно меняется на цикл (
loop проверка условия в начале тело цикла) Цикл
repeat until (проверка условия в конце) { тело цикла} соответственно меняется на цикл ( тело цикла
loop проверка условия в конце) В итоге имеем цикл, который, будучи универсальным, не становится громоздким. Всё просто и элегантно. При этом циклы могут досрочно прерываться, для этого имеются конструкции, которые перекрывают все потребности.
«Цикл-паук» всё же очень громоздок. Некрасиво. Кажется неслучайным, что он не воплощён в каком-либо языке программирования.
Что ещё почитать на эту тему
Последняя правка: 2015-01-23 07:31
Отзывы
2013/04/14 10:25, автор: Михаил
Неудачная идея, сделать один универсальный цикл. Лучше несколько разных циклов с разной семантической нагрузкой. Например так:
циклы с одной точкой выхода
for (фиксированное число повторений)
while do (цикл с предусловием)
repeat until (цикл с постусловием)
циклы с несколькими точками выхода
loop ... exit ... exit ... end; (бесконечный цикл + оператор его прерывания)
цикл паук, цикл дейкстры и пр.
2013/04/14 13:29, автор: Автор сайта
Ответ читайте выше.
2013/04/18 20:13, автор: Михаил
Синтаксический сахар это то, без чего можно обойтись, выразив через другие конструкции языка. От составного оператора отказаться нельзя в языках паскаль или С, кроме как перепроектировав его, но это будет уже другой язык. Собственно Модула и есть сильно перепроектированный Паскаль.
2013/04/19 13:45, автор: Автор сайта
Вопрос и ответ перенес в ветку по теме: Синтаксический сахар
2013/04/18 20:22, автор: Михаил
Вы, фактически, повторили решение для языка С.
Цикла с фиксированным числом итераций у Вас нет.
Досрочно прерывать циклы с фиксированным числом повторений, пред- и постусловием считаю не правильным. Для прерываемого цикла должна быть отдельная конструкция, например бесконечный цикл.
2013/04/19 14:07, автор: Автор сайта
Повторили решение Си авторы C++, C#, Java, Perl, PHP, D, Cyclone, Rust и многих других. Я же творчески переработал. Переместил ключевое слово внутри «параметров» цикла, предложил использовать его не только в начале цикла, но и в конце и даже в средине. Т.е. это ещё большая универсализация.
Цикл с фиксированным числом повторений есть:
( i=0
loop i тело цикла)
Этот цикл выполнится фиксированное число раз, а именно 100.
Отдельная конструкция с прерываемым циклом не нужна. Если универсальный цикл записать так:
(
loop (if a>b
exit)
тело цикла)
То это выглядит так, как Вы этого хотите. Желаемая цель достигается.
А зачем бороться с «break» и «continue»? Ведь это не «goto»! Более того, отказ от «break» как провоцирует употребление «goto».
2013/04/23 11:45, автор: Михаил
Пусть так, но зачем повторять не очень удачное решение примененное в Си? Вы ведь не хотите повторять составной оператор, которой применен в Си, С++, С# и т.д.
Это цикл while с помощью которого вы сделали цикл с фиксированным повторением. Представьте что на месте выражения i<100 стоит некоторое сложное выражение, причем функции, входящие в него, могут иметь побочный эффект. Выражение у Вас будет вычисляться на каждой итерации, а у цикла с фиксированным числом повторений это не так, число повторений вычисляется только один раз.
Желаемой цели (реализации алгоритма) можно достичь с помощью всего двух операторов: if и goto. Я же о семантике. Ваш вариант не обеспечивает фиксированной семантики для конструкции, что затрудняет её понимание.
continue очень вредный и ненужный оператор, лучше использовать if для обхода.
break вполне допустим, но не везде. Можно разрешить его использование внутри только прерываемого цикла, тоже самое касается и goto. Это не нарушит семантики.
Если по алгоритму Вам нужен цикл наиболее общего вида, то его и используйте, а не вводите в заблуждение других людей используя для этого специализированные циклы вроде while do или do while.
2013/04/25 11:58, автор: Автор сайта
«break» и «continue» помогают исключить из языка «goto», вред которого теоретически доказан. Не могли бы Вы дать ссылку на авторитетные источники, где обоснован вред «continue»?
С помощью «if» можно обойтись без «continue». Но заменить «continue N» сложнее, появятся лишние операторы.
«break» не предлагается к использованию в других местах, кроме циклов. В операторе «switch» его предлагается исключить, т.к. он провоцирует появление «спагетти-кода».
В Паскале цикл с фиксированным числом повторений выглядит так:
for V:= E1 to E2 do S, где E1 и E2 – выражения, которые могут иметь побочные эффекты.
Чтобы избежать побочного эффекта при каждой итерации, нужно просто E2 вычислить однократно до начала цикла. В C++ это можно сделать так:
for (int i=E1, int N=E2; i.
Если E2 – константа или переменная, то побочных эффектов не будет. Но это частный случай. Но стоит ли делать 2 цикла,
for V:= CV1 to CV2 do S, где CV1 и CV2 – константа или переменная, и
for V:= E1 to E2 do S, где E1 и E2 – выражения?
Вам известны языки программирования, грамматика которых запрещала бы использование выражений для задания числа повторенний?
2014/01/16 11:20, автор: Pensulo
Теоретически, инициализатор можно вынести за скобки самого цикла (выполнить инициализацию переменных задействованных в самом цикле перед начальной конструкцией цикла).
Сам цикл действительно напоминает замкнутую петлю (loop) бесконечно выполняемого блока действий из которой (петли) есть возможность выйти принудительно, приняв решение об этом где-то внутри самого цикла. Анализ необходимости выхода можно при этом проводить в любом месте и сколько угодно раз, т.е. и в начале и в середине и в конце блока вычислений цикла.
Тогда конструкция циклического исполнения программного блока могла бы выглядеть, например, следующим образом:
<инициализация_цикла>
(loop
if пред_условие leave
<часть_тела_цикла_1>
if сред_условие levae
<часть_тела_цикла_2>
if пост_условие leave
)
Где leave по-сути тот же самый exit, что и в ваших примерах, но тогда сохраняется возможность осуществлять досрочный выход из функции с помощью служебного слова exit.
Размещать анализаторы выхода if .. leave можно в любом месте внутри циклических скобок (loop .. ) и в любом количестве.
А вот с предоставлением возможности выхода из объемлющего цикла, морально ОЧЕНЬ трудно согласиться.
2014/01/16 12:15, автор: Автор сайта
Цикл for в C++ - Основы программирования на C / С++ |
25 авг 2012 ... Цикл — многократное прохождение по одному и тому же коду программы.
Циклы необходимы программисту для многократного ... http://cppstudio.com/post/348/ |
А зачем инициализатор выносить за скобки цикла? В том то вся выгода, что вся конструкция цикла (впрочем – любая конструкция) находится внутри скобок. Мы что инициализируем, переменные цикла? Значит это надо сделать внутри конструкции цикла. Но это не значит, что до открывающей скобки ничего нельзя делать. Просто находящийся внутри скобок инициализатор подчеркивает, что он относится именно к циклу.
А какая альтернатива выходу из объемлющего цикла? Только «goto». Поэтому «exit 2» можно читать как «goto label2». Только первый способ короче.
2014/04/23 02:40, автор: 85.192.188.252
Тогда конструкция циклического исполнения программного блока могла бы выглядеть, например, следующим образом:
(loop
if пред_условие leave
if сред_условие leave
if пост_условие leave
) Такой цикл уже придуман в Scheme (диалект Lisp'а). Аналогично там есть сложный составной условный оператор.
2015/05/27 08:25, автор: Алексей Яковлев
А не будет loop теряться в коде, если он в середине или в конце?
И еще, как отделить инициализацию от тела цикла, если loop опять же в середине или конце?
2015/05/27 15:15, автор: Автор сайта
IDE должна выделять ключевые слова: его символы должны быть жирными, иметь другой цвет. Можно поставить перед «loop» пиктрограмму. Блок, из которого состоит цикл, можно обрамлять специальной рамкой. Много способов обратить внимание на то, что это цикл.
Инициализирующие выражения располагаются на той же строке, что и ключевое слово и стоят перед ним – что в начале, что в средине, что в конце цикла. В дополнение к этому IDE должна его как-то выделять.
Написать автору можно на электронную почту mail(аt)compiler.su