6.4. Цикл for
Очень часто мы знаем количество итераций цикла: это либо константа, известная при написании программы, либо значение, вычисляемое перед началом цикла. Цикл со счетчиком можно запрограммировать следующим образом:
int i; /* Индекс цикла */
C |
i = low; /* Инициализация индекса */
while (i <= high) { /* Вычислить условие выхода */
statement;
i++: /* Увеличить индекс */
};
Поскольку это общая парадигма, постольку для упрощения программирования во всех (императивных) языках есть цикл for. Его синтаксис в языке С следующий:
int i; /* Индекс цикла */
int low, high; /* Границы цикла */
-
C
for (i = low; i <= high; i++) {
statement;
}
В Ada синтаксис аналогичный, за исключением того, что объявление и увеличение переменной цикла неявные:
Low, High: Integer;
Ada |
for I in Low .. High loop
statement;
end loop;
Ниже в этом разделе мы обсудим причины этих различий.
Известно, что в циклах for легко сделать ошибки в значениях границ. Цикл выполняется для каждого из значений от low до high; таким образом, общее число итераций равно high - low +1. Однако, если значение low строго больше значения high, цикл будет выполнен ноль раз. Если вы хотите выполнить цикл точно Л/ раз, цикл for будет иметь вид:
-
Ada
forlinl ..N loop...
и число итераций равно N -1 + 1 = N. В языке С из-за того, что для массивов индексы должны начинаться с нуля, цикл со счетчиком обычно записывается так:
C |
Так как запись i < п означает то же самое, что и i <= (п - 1), цикл выполняется (п -1)-0+1 =п раз, как и требуется.
Обобщения в циклах for
Несмотря на то, что все процедурные языки содержат циклы for, они значительно отличаются по предоставляемым дополнительным возможностям. Две крайности — это Ada и С.
В Ada исходным является положение, что цикл for должен использоваться только с фиксированным числом итераций и что это число можно вычислить перед началом цикла. Объясняется это следующим: 1) большинство реальных циклов простые, 2) другие конструкции легко запрограммировать в явном виде, и 3) циклы for и сами по себе достаточно трудны для тестирования и проверки. В языке Ada нет даже классического обобщения: увеличения переменной цикла на значения, отличные от 1 (или -1). Язык Algol позволяет написать итерации для последовательности нечетных чисел:
Algol |
for I := 1 to N step 2 do ...
в то время как в Ada мы должны явно запрограммировать их вычисление:
for l in 1 .. (N + 1)/2 loop
Ada |
|1 =2*|-1;
…
end loop;
В языке С все три элемента цикла for могут быть произвольными выражениями:
C |
В описании С определено, что оператор
C |
эквивалентен конструкции
C |
while (expression_2) {
statement;
expression_3;
}
В языке Ada также допускается использование выражений для задания границ цикла, но они вычисляются только один раз при входе в цикл. То есть
Ada |
statement;
end loop;
эквивалентно
I = expression_1;
Ada |
while (I < Temp) loop
statement;
I: = I + 1;
end loop;
Если тело цикла изменяет значение переменных, используемых при вычислении выражения expression_2, то верхняя граница цикла в Ada изменяться не будет. Сравните это с данным выше описанием цикла for в языке С, который заново вычисляет значение выражения expression_2 на каждой итерации.
Обобщения в языке С — нечто большее, чем просто «синтаксический сахар», поскольку операторы внутри цикла, изменяющие выражения expres-sion_2 и expression_3, могут вызывать побочные эффекты. Побочных эффектов следует избегать по следующим причинам.
• Побочные эффекты затрудняют полную проверку и тестирование цикла.
• Побочные эффекты неблагоприятно воздействуют на читаемость и поддержку программы.
•Побочные эффекты делают цикл гораздо менее эффективным, потому что выражения expression_2 и expression_3 нужно заново вычислять на каждой итерации. Если побочных эффектов нет, оптимизирующий компилятор может вынести эти вычисления за границу цикла.
Реализация
Циклы for — наиболее часто встречающиеся источники неэффективности в программах, потому что небольшие различия в языках или небольшие изменения в использовании оператора могут иметь серьезные последствия. Во многих случаях оптимизатор в состоянии решить эти проблемы, но лучше их понимать и избегать, чем доверяться оптимизатору. В этом разделе мы более подробно опишем реализацию на уровне регистров.
В языке Ada цикл
Ada |
statement;
end loop;
компилируется в
compute R1,expr_1
store R1,l Нижняя граница индексации
compute R2,expr_2
store R2,High Верхняя граница индексации
L1: load R1,l Загрузить индекс
load R2,High Загрузить верхнюю границу
jump_gt R1,R2,L2 Завершить цикл, если больше
statement Тело цикла
load R1,l Увеличить индекс
incr R1
store R1,l
jump L1
L2:
Очевидная оптимизация — это закрепление регистра за индексной переменной I и, если возможно, еще одного регистра за High:
compute R1 ,ехрг_1 Нижняя граница в регистре
compute R2,expr_2 Верхняя граница в регистре
L1: jump_gt R1,R2,L2 Завершить цикл, если больше
statement
incr R1 Увеличить индексный регистр
jump L1
L2:
Рассмотрим теперь простой цикл в языке С:
C |
statement;
Это компилируется в
compute R1,expr_1
store R1,i Нижняя граница индексации
L1: compute R2,expr_2 Верхняя граница внутри цикла!
jump_gt R1,R2,L2 Завершить цикл, если больше
statement Тело цикла
load R1,i Увеличить индекс
incr R1
store R1,i
jump L1
L2:
Обратите внимание, что выражение expression_2, которое может быть очень сложным, теперь вычисляется внутри цикла. Кроме того, выражение expres-sion_2 обязательно использует значение индексной переменной i, которая изменяется при каждой итерации. Таким образом, оптимизатор должен уметь выделить неизменяющуюся часть вычисления выражения expression_2, чтобы вынести ее из цикла.
Можно ли хранить индексную переменную только в регистре для увеличения эффективности? Ответ зависит от двух свойств цикла. В Ada индексная переменная считается константой и не может изменяться программистом. В языке С индексная переменная — это обычная переменная; она может храниться в регистре только в том случае, когда абсолютно исключено изменение ее текущего значения где-либо вне цикла. Никогда не используйте глобальную переменную в качестве индексной переменной, потому что другая процедура может прочитать или изменить ее значение:
-
C
int i;
void p2(void) {
i = i + 5;
}
void p1(void) {
for (i=0; i<100; i++) /* Глобальная индексная переменная */
p2(); /* Побочный эффект изменит индекс*/
}
Второе свойство, от которого зависит оптимизация цикла, — потенциальная возможность использования индексной переменной за пределами цикла. В Ada индексная переменная неявно объявляется for-оператором и недоступна за пределами цикла. Таким образом, независимо от того, как осуществляется выход из цикла, мы не должны сохранять значение регистра. Рассмотрим следующий цикл поиска значения key в массиве а:
C |
int i, key;
key = get_key();
for(i = 0;i< 100; i++)
if (a[i] == key) break;
process(i);
Переменная i должна содержать правильное значение независимо от способа, которым был сделан выход из цикла. Это может вызывать затруднения при попытке оптимизировать код. Обратите внимание, что в Ada требуется явное кодирование для достижения того же самого результата, потому что индексная переменная не существует вне области цикла:
Ada |
for I in 1 ..100 loop
if A(l) = Key then
Found = I;
exit;
end if;
end loop;
Определение области действия индексов цикла в языке C++ с годами менялось, но конечное определение такое же, как в Ada: индекс не существует вне области цикла:
for(int i=0;i<100;i++){
C++ |
}
На самом деле в любом управляемом условием операторе (включая, if- и switch-операторы) можно задать в условии несколько объявлений; область их действия будет ограничена управляющим оператором. Это свойство может способствовать читаемости и надежности программы, предотвращая непреднамеренное использование временного имени.
- Глава 1
- 1.2. Процедурные языки
- 1.3. Языки, ориентированные на данные
- 1.4. Объектно-ориентированные языки
- 1.5. Непроцедурные языки
- 1.6. Стандартизация
- 1.7. Архитектура компьютера
- 1.8. Вычислимость
- 1.9. Упражнения
- Глава 2
- 2.2. Семантика
- 2.3. Данные
- 2.4. Оператор присваивания
- 2.5. Контроль соответствия типов
- 2.7. Подпрограммы
- 2.8. Модули
- 2.9. Упражнения
- Глава 3
- 3.1. Редактор
- 3.2. Компилятор
- 3.3. Библиотекарь
- 3.4. Компоновщик
- 3.5. Загрузчик
- 3.6. Отладчик
- 3.7. Профилировщик
- 3.8. Средства тестирования
- 3.9. Средства конфигурирования
- 3.10. Интерпретаторы
- 3.11. Упражнения
- Глава 4
- 4.1. Целочисленные типы
- I: Integer; -- Целое со знаком в языке Ada
- 4.2. Типы перечисления
- 4.3. Символьный тип
- 4.4. Булев тип
- 4.5. Подтипы
- 4.6. Производные типы
- 4.7. Выражения
- 4.8. Операторы присваивания
- 4.9. Упражнения
- Глава 5
- 5.1. Записи
- 5.2. Массивы
- 5.3. Массивы и контроль соответствия типов
- Подтипы массивов в языке Ada
- 5.5. Строковый тип
- 5.6. Многомерные массивы
- 5.7. Реализация массивов
- 5.8. Спецификация представления
- 5.9. Упражнения
- Глава 6
- 6.1. Операторы switch и case
- 6.2. Условные операторы
- 6.3. Операторы цикла
- 6.4. Цикл for
- 6.5. «Часовые»
- 6.6. Инварианты
- 6.7. Операторы goto
- 6.8. Упражнения
- Глава 7
- 7.1. Подпрограммы: процедуры и функции
- 7.2. Параметры
- 7.3. Передача параметров подпрограмме
- 7.4. Блочная структура
- 7.5. Рекурсия
- 7.6. Стековая архитектура
- 7.7. Еще о стековой архитектуре
- 7.8. Реализация на процессоре Intel 8086
- 7.9. Упражнения
- Глава 8
- 8.1 . Указательные типы
- 8.2. Структуры данных
- 8.3. Распределение памяти
- 8.4. Алгоритмы распределения динамической памяти
- 8.5. Упражнения
- Глава 9
- 9.1. Представление вещественных чисел
- 9.2. Языковая поддержка вещественных чисел
- 9.3. Три смертных греха
- Вещественные типы в языке Ada
- 9.5. Упражнения
- Глава 10
- 10.1. Преобразование типов
- 10.2. Перегрузка
- 10.3. Родовые (настраиваемые) сегменты
- 10.4. Вариантные записи
- 10.5. Динамическая диспетчеризация
- 10.6. Упражнения
- Глава 11
- 11.1. Требования обработки исключительных ситуаций
- 11.2. Исключения в pl/I
- 11.3. Исключения в Ada
- 11.5. Обработка ошибок в языке Eiffei
- 11.6. Упражнения
- Глава 12
- 12.1. Что такое параллелизм?
- 12.2. Общая память
- 12.3. Проблема взаимных исключений
- 12.4. Мониторы и защищенные переменные
- 12.5. Передача сообщений
- 12.6. Язык параллельного программирования оссаm
- 12.7. Рандеву в языке Ada
- 12.9. Упражнения
- Глава 13
- 13.1. Раздельная компиляция
- 13.2. Почему необходимы модули?
- 13.3. Пакеты в языке Ada
- 13.4. Абстрактные типы данных в языке Ada
- 13.6. Упражнения
- Глава 14
- 14.1. Объектно-ориентированное проектирование
- В каждом объекте должно скрываться одно важное проектное решение.
- 14.3. Наследование
- 14.5. Объектно-ориентированное программирование на языке Ada 95
- Динамический полиморфизм в языке Ada 95 имеет место, когда фактический параметр относится к cw-типу, а формальный параметр относится к конкретному типу.
- 14.6. Упражнения
- Глава 15
- 1. Структурированные классы.
- 15.1. Структурированные классы
- 5.2. Доступ к приватным компонентам
- 15.3. Данные класса
- 15.4. Язык программирования Eiffel
- Если свойство унаследовано от класса предка более чем одним путем, оно используется совместно; в противном случае свойства реплицируются.
- 15.5. Проектные соображения
- 15.6. Методы динамического полиморфизма
- 15.7. Упражнения
- 5Непроцедурные
- Глава 16
- 16.1. Почему именно функциональное программирование?
- 16.2. Функции
- 16.3. Составные типы
- 16.4. Функции более высокого порядка
- 16.5. Ленивые и жадные вычисления
- 16.6. Исключения
- 16.7. Среда
- 16.8. Упражнения
- Глава 17
- 17.2. Унификация
- 17.4. Более сложные понятия логического программирования
- 17.5. Упражнения
- Глава 18
- 18.1. Модель Java
- 18.2. Язык Java
- 18.3. Семантика ссылки
- 18.4. Полиморфные структуры данных
- 18.5. Инкапсуляция
- 18.6. Параллелизм
- 18.7. Библиотеки Java
- 8.8. Упражнения