- •Часть 1
- •1. Основы программирования на языке Си
- •1.1. Структура программы на языке Си.
- •1.2. Структура простейшей одномодульной программы
- •1.2.1. Комментарии в программе на языке Си
- •1.2.2. Начальные сведения о препроцессоре
- •1.2.3. Определение функции main().
- •1.2.4. Пример простейшей программы
- •1.3. Алфавит, синтаксис и семантика
- •1.4. Алфавит языка Си
- •1.5. Понятие о типе
- •1.6. Система типов языка Си
- •1.7. Понятие об объекте
- •1.8. Лексемы
- •1.9. Зарезервированные слова
- •1.10. Идентификаторы
- •1.11. Литералы
- •1.11.1. Целочисленные литералы
- •1.11.2. Литерал вещественного типа
- •1.11.3. Символьные литералы
- •1.11.4. Строковый литерал
- •1.12. Переменные
- •1.13. Символические константы в языке Си
- •1.14. Операторы, выражения и инструкции. Общие сведения.
- •1.14.1. Классификация операторов
- •1.14.2. Приоритет и ассоциативность операторов.
- •1.14.3. Побочные эффекты при вычислении выражений
- •1.14.4. Порядок вычисления выражений
- •1.15. Арифметические операторы и выражения
- •1.15.1. Унарные операторы
- •1.15.2. Бинарные операторы
- •1.15.3. Преобразования типа при выполнении бинарных операторов
- •1.15.3.1. Автоматическое преобразование типов
- •1.15.3.2. Явное преобразование типа
- •1.15.4. Математические функции
- •1.16. Оператор присваивания и инструкция присваивания
- •1.16.1. Простой оператор присваивания
- •1.16.2.Множественное присваивание
- •1.16.3. Составной оператор присваивания
- •1.16.4. Преобразование типа при присваивании
- •1.17. Начальные сведения об указателях. Выходные параметры функции
- •1.18. Принятие решений и логические величины. Операторы отношения и сравнения на равенство
- •1.18.1. Логические операторы
- •1.18.2. Поразрядные операторы
- •1.19. Условные выражения (оператор ?:)
- •1.20. Оператор запятая
- •1.21. Оператор sizeof
- •1.22.4. Инструкция return
- •1.23. Составная инструкция
- •1.24. Инструкция if else
- •1.24.1. Полная и сокращенная формы инструкции if
- •1.24.2. Вложенные инструкции if
- •1.25. Инструкция switch
- •1.25.1. Синтаксис инструкции switch
- •1.25.2. Использование инструкции switch
- •1.26. Функциональные компоненты цикла
- •1.27. Арифметические и итерационные циклы
- •1.27.1. Задачи, приводящие к арифметическим циклам
- •1.27.2. Задачи, приводящие к итерационным циклам
- •1.28. Циклические управляющие инструкции
- •1.29. Цикл, управляемый инструкцией for
- •2. Примеры решенных задач
- •2.1. Линейные вычислительные процессы
- •2.2. Организация разветвлений
- •2.2.1. Простейшие разветвления
- •2.2.2. Многовариантные разветвления
- •2.2.2.1. Использование переключателя switch
- •2.2.2.2. Использование вложенных инструкций if else if
- •2.2.2.3. Использование вложенных инструкций if ... If
- •2.2.2.4. Использование сокращённых форм инструкции if
- •2.2.3. Рекомендации по программированию
- •2.3. Организация циклов
- •2.3.1. Простые циклы
- •2.3.2. Циклы и разветвления
- •2.3.2.1. Разветвление до цикла
- •2.3.2.2. Разветвление внутри цикла
- •2.3.2.3. Разветвление после цикла
- •2.3.2.4. Циклы в ветвях разветвлений.
- •2.3.3. Рекомендации по программированию
- •Содержание
- •1. Основы программирования на языке Си
- •Литература
2.3. Организация циклов
В настоящем разделе рассматриваются только арифметические циклы. Раздел содержит три подраздела:
● Простые циклы.
● Циклы и разветвления.
● Рекомендации по программированию.
В первом из подразделов (простые циклы) рассматриваются задачи, при программировании которых требуется организовать только один цикл, причем необходимость в использовании разветвлений отсутствует.
Во втором подразделе (циклы и разветвления) рассматриваются все возможные комбинации арифметических циклов и разветвлений.
Раздел заканчивается рекомендациями по решению задач, требующих программирования циклических алгоритмов.
Во второй части пособия будут приведены решения задач, требующие организации итерационных и вложенных циклов.
2.3.1. Простые циклы
Цикл будем называть простым в том случае, когда он не входит в состав других управляющих структур, а его тело не содержит управляющие структуры. Будет рассмотрен ряд технических приемов, облегчающих программирование и способствующих разработке более качественных программ.
Начинающие программисты часто при организации циклов испытывают трудности в разделении однократно и многократно выполняемых операций. В первом примере, который приводится ниже, предлагается технический прием, предназначенный для преодоления указанных трудностей.
Пример 1. Отделение многократно и однократно выполняемых вычислений.
Постановка задачи. Вычислить значение величины y, заданной следующим образом:
Решение.
При решении рассматриваемой задачи у некоторых студентов, возможно, возникнут затруднения, связанные с определением в какой части алгоритма извлекать корень – в теле цикла или после его завершения. Для преодоления этих трудностей целесообразно выполнить преобразование исходной формулы, обозначив через s входящую в нее сумму. В результате выполненных преобразований вместо одной исходной формулы получим следующие две расчетные формулы:

Вычисления по первой из формул требуют организации циклического алгоритма для накопления суммы, а вычисления по второй – могут быть выполнены в линейном алгоритме (после цикла). Таким образом, становится очевидным, что корень квадратный следует извлекать только один раз.
Для вычисления по первой из расчетных формул следует организовать арифметический цикл. Задача о накоплении суммы уже рассматривалась в пункте 1.29. Используя методику, изложенную в указанном пункте, можно написать следующую программу для решения рассматриваемой задачи.
#include<stdio.h> #include<conio.h> #include<math.h> int main(void) { int i, n; double x, y, s;
printf(“n=”) scanf(“%d”, &n); printf(“x=”); scanf(“%lf”, &x); s = 0; for(i = 2 i <= n; i++) { t = i / (i + x); y += t * t; } y = sqrt(s); printf(“y=%0.4g\n”, y); getch(); return 0; }
Заметим в заключение, что с целью упрощения возведения в квадрат в теле цикла введена вспомогательная переменная “t”, в которой запоминается результат вычисления значения выражения i / (i +x).
Пример 2. Чистка цикла
“Чистка” цикла состоит в вынесении из тела цикла действий, результаты вычисления которых в цикле не изменяются. Подобная операция может привести к существенному уменьшению объема вычислений. Особенно полезным это может быть в тех случаях, когда из тела цикла выносятся обращения к библиотечным функциям. Следует учитывать, что чистка цикла может осуществляться компилятором в процессе выполнения оптимизации.
Постановка задачи.
Вычислить значение величины “y”, заданной следующим образом
y =
Решение.
Непосредственное использование исходного выражения, определяющего значение “y”, приводит к организации цикла, имеющего следующий вид
y = 0; for(i=3; i<= n; i++) y += i / (i * i + sqrt(fabs(x)));
Вычисление выражения sqrt(fabs(x)) на каждом шаге цикла будет давать один и тот же результат. С целью уменьшения времени выполнения программы значение этого выражения можно вычислить один раз в линейной части алгоритма, предшествующей циклу. В связи с тем, что для вычисления корня квадратного следует использовать цикл, можно ожидать существенного уменьшения объема вычислений. Этот приём и составляет существо чистки цикла.
#include<stdio.h> #include<conio.h> #include<math.h> int main(void) { int i, n; double y, x, z;
printf(“n=”) scanf(“%d”, &n); printf(“x=”) scanf(“%lf”, &x); z= sqrt(fabs(x)); y=0; for(i = 3; i <= n; i++) y += i /(i * i + z); printf(“y=%0.4g\n”,y); getch(); return 0; }
Пример 3. Вычисление суммы знакопеременного ряда
Рассмотрим в качестве примера задачу, в которой необходимо вычислить сумму следующего вида:
![]()
Наличие сомножителя (-1)i делает ряд знакопеременным. Слежение за знаком очередного слагаемого можно реализовать путем введения вспомогательной переменной (назовем ее znak). Изменение знака можно получить с помощью инструкции следующего вида: znak = - znak;. При рассмотрении вопроса об инициализации переменной znak следует обращать внимание на знак первого слагаемого в искомой сумме. В рассматриваемой задаче переменная znak должна быть инициализирована значением 1. Это обусловлено тем обстоятельством, что первое слагаемое суммы является положительным. Ниже приводится программа, выполняющая необходимые вычисления.
#include<stdio.h> #include<conio.h> int main(void) { int i, n; int s, znak;
printf(“n=”) scanf(“%d”, &n); s = 0; znak = 1; for(i = 2; i <= n; i++) { s += znak * i * i; znak = -znak; } printf(“s=%d\n”, s); getch(); return 0; }
Пример 4. Вычисление очередного слагаемого по рекуррентной формуле.
Пусть необходимо вычислить сумму следующего вида:
.
На первый взгляд, для вычисления рассматриваемой суммы необходимо организовать вложенные циклы. При этом внешний цикл должен накапливать сумму s, а внутренний − вычислять факториал i!. Такой подход имеет ряд недостатков. Первый из них связан с быстрым возрастанием факториала i!, что может привести к переполнению разрядной сетки. B то же время значение очередного слагаемого, определяемого величиной
,
может
еще поместиться в разрядной сетке
компьютера. Второй недостаток состоит
в необходимости организации вложенных
циклов.
Другой подход к решению задачи состоит в использовании функциональной связи между двумя последовательными значениями очередных слагаемых суммы. Предположим, что значение очередного слагаемого уже известно и содержится в переменной “a”. В этом случае накопление суммы может быть выполнено с помощью следующей инструкции присваивания:
s += a;
После этого следует выполнить продвижение цикла. С этой целью необходимо найти очередное значение слагаемого, которое должно быть добавлено к сумме s на следующем шаге цикла. Для этого попытаемся найти связь между двумя последовательными значениями слагаемых. Для этого перейдем к математическому представлению очередного и последующего слагаемых. Обозначим через ai – последнее слагаемое, добавленное в сумму s. Индекс i этого слагаемого соответствует текущему значению управляющей переменной. Тогда через ai + 1 следует обозначить последующее слагаемое. Отношение этих величин примет следующий вид:
.
Отсюда имеем:
![]()
Выполним теперь переход к программированию на языке Си. Логика работы оператора присваивания позволяет отбросить индексы и записать следующую инструкцию, выполняющую вычисление последующего слагаемого:
a *= x / (i + 1);
Определившись с телом цикла, перейдем к рассмотрению его вспомогательных элементов. Наибольшие сложности возникают с инициализацией переменной a. До начала работы цикла ее значение должно быть равно первому слагаемому. Это значение следует вычислить вручную. В рассматриваемом примере для этого необходимо написать следующую инструкцию присваивания:
a = pow(x, 4) / 24;
Приведем теперь программу, выполняющую необходимые вычисления:
#include<stdio.h> #include<conio.h> #include<math.h> int main(void) { int i, n; double x, s, a; printf(“n=”); scanf(“%d”, &n); printf(“x=”); scanf(“%lf”, &x); s = 0; a = pow(x, 4) / 24; for(i = 4; i <= n; i++) { s += a; a *= x / (i + 1); } printf(“s = %0.4g\n”, s); getch(); return 0; }
Пример 5. Объединение циклов.
В некоторых случаях циклы могут быть объединены. Приведем пример.
Постановка задачи.
Пусть необходимо вычислить значение величины y, заданной следующим образом:
.
Обозначим через s и p соответственно сумму и произведение, входящие в исходную расчетную формулу. Это позволяет вместо одной расчетной формулы написать следующие три формулы:

Выполненное преобразование позволяет в вычислении величины y выделить три этапа. Первые два из них требуют организации циклических вычислительных процессов. Один из них необходим для накопления суммы s, а другой – для накопления произведения p. Завершать вычисления должен линейный вычислительный процесс, предназначенный для вычисления величины y по последней формуле. Решение, соответствующее такому подходу, имеет следующий вид:
/* Первый вариант решения */ #include<stdio.h> #include<conio.h> #include<math.h> int main(void) { int i, n; double x, s, p, y; printf(“n=”); scanf(“%d”, &n); printf(“x=”); scanf(“%lf”, &x); s = 0; for(i = 2; i <= n; i++) s += i / (i + fabs(x)); p = 1; for(i = 2; i <= n; i++) p *= i / (i + fabs(x)); y = (2 + s) / (3 + p); printf(“y=%g\n”, y); getch(); return 0; }
Написанная программа корректно решает поставленную задачу. Считается хорошим стилем в программировании не останавливаться на первом полученном решении. Следует выполнить анализ полученного решения с целью его улучшения. В первую очередь при таком анализе целесообразно обращать внимание на возможность улучшения структуры программы. Процесс, приводящий к улучшению структуры программы, в литературе называют рефакторингом
В исходном коде полученной программы обращает на себя внимание похожесть циклов, предназначенных для вычисления суммы и произведения. Действительно, сравниваемые циклы имеют одинаковые заголовки. Кроме того, в теле каждого из циклов выполняется вычисление одного и того же выражения i / (i + fabs(x)). Ниже приводится второй вариант решения поставленной задачи, который предусматривает:
● Объединение циклов накопления суммы и произведения в одном цикле,
● вычленение вычисления выражения i / (i + fabs(x)).
/* Второй вариант решения */ #include<stdio.h> #include<conio.h> #include<math.h> int main(void) { int i, n; double x, s, p, y, a,z; clrscr(); printf(“n=”); scanf(“%d”, &n); printf(“x=”); scanf(“%lf”, &x);
z = fabs(x);
s = 0; p = 1; for(i = 2; i <= n; i++) { a = i / (i + z); s += a; p *= a; } y = (2 + s) / (3 + p); printf(“y=%0.4g\n”, y); getch(); return 0; }
Пример 6. Задача табулирования.
Постановка задачи
Табулировать в “n” равноотстоящих точках функцию y = x2, начиная от начального значения x = xn вплоть до конечного значения x = xk. Будем считать, что аргументом табулируемой функции является переменная вещественного типа.
Решение
Существо рассматриваемой задачи состоит в том, что требуется обеспечить n – кратное вычисление заданной функции, последовательно изменяя значение аргумента “x” от значения, равного x = xn, до x = xk с постоянным шагом dx, для вычисления которого в программе следует предусмотреть инструкцию присваивания следующего вида:
dx = (xk - xn) / (n - 1);
Для решения рассматриваемой задачи следует организовать арифметический цикл, который должен обеспечить n – кратное выполнение однотипных действий.
Напомним, что в соответствии с концепцией арифметического цикла для управления его работой следует использовать переменную целого типа. Использование в рассматриваемом примере для этих целей действительного аргумента “x” является недопустимым. Применение для этих целей циклической инструкции следующего вида:
for(x = xn; x <= xk; x += dx)
является нежелательным. Дело в том, что погрешности, присущие представлению данных действительного типа (double или float), не позволяют гарантировать выполнение заданного количества повторений цикла.
Вывод таблицы должен состоять из двух частей:
● вывод заголовка таблицы.
● Вывода строк таблицы, содержащих данные.
Вывод заголовка таблицы следует выполнить в линейной части программы, предшествующей циклу. Вывод очередной (текущей строки) должен находиться в теле цикла. Спецификации преобразования вызовов функции printf() вывода заголовка таблицы и текущей строки должны быть согласованы. Необходимо согласовывать модификаторы минимальной ширины соответствующих спецификации этих вызовов.
Программа, реализующая требуемые вычисления, приведена ниже.
#include<stdio.h> #include<conio.h> int main(void) { int i, n; double x, xn, xk, dx, y; printf(“Количество расчетных точек=”); scanf(“%d”, &n); printf(“Начальное значение аргумента=”); scanf(“%lf”, &xn); printf(“Конечное значение аргумента=”); scanf(“%lf”, &xk) printf(“%5s%15s%15s\n”, “НОМЕР”, “АРГУМЕНТ”, “ФУНКЦИЯ”); x = xn; dx = (xk - xn) / (n - 1); for(i = 1; i <= n; i++) { y = x * x; printf(“%5d%15.4g%15.4g\n”, i, x, y); x += dx; } getch(); return 0; }
