§9 Инструкции циклов. Цикл for (цикл с параметром). Процессор случайных чисел

Цикл с параметром. Инструкция for

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

Синтаксис:

for (инициализация; условие; модификация) {
    Тело цикла;
}

Цикл работает следующим образом:

  1. В начале происходит инициализация переменной-счетчика. Инициализация производится единожды, до выполнения цикла.
  2. Затем проверяется выражение-условие: если выражение имеет значение true, будет выполняться тело цикла.
  3. После выполнения инструкций тела цикла производится модификация (изменение) величины счетчика; обычно для этого используются операции инкремента или декремента.
Примечание: в C++ является правилом создавать определение переменной-счетчика в заголовке цикла. Но это не обязательно, тем более, если планируется инициализировать несколько переменных в разделе инициализации так, как это реализовано в программе 9.4. Однако использование описания переменной-счетчика в заголовке цикла приводит к описанию локальной переменной (см. ниже область видимости данных), уничтожаемой автоматически при завершении работы цикла. Поэтому, без крайней необходимости, описание переменной-счетчика вне цикла for производить не следует.

Программа 9.1 Вывести натуральные числа от 1 до n.

#include <iostream>
#include <iomanip>
using namespace std;

int main() {
	int n;
	cout << "n = "; cin >> n;
	for (int i = 1; i <= n; i++)
		cout << i << setw(3); 
	return 0;
}

Если в теле цикла более чем одна инструкция, то инструкции тела цикла необходимо заключать в фигурные скобки (блок). Например. Составить программу вывода n чисел Фибоначчи. Числа Фибоначчи: f1 = 1, f2 = 1, fn = fn-1 + fn-2
Программа 9.2

#include <iostream>
#include <iomanip>
using namespace std;

int main() {
	int n;
	unsigned long long f, f1 = 1, f2 = 1;
	cout << "n = "; cin >> n;
	cout << "1 => " << f1 << endl;
	cout << "2 => " << f2 << endl;
	for (int i = 3; i <= n; i++) {
		f  = f1 + f2;
		f1 = f2;
		f2 = f;
		cout << i << " => " << f << endl;
	}
	return 0;
}

Вывод программы

n = 10
1 => 1
2 => 1
3 => 2
4 => 3
5 => 5
6 => 8
7 => 13
8 => 21
9 => 34
10 => 55

В процессе работы цикла for не рекомендуется изменять значение счетчика в теле цикла. Такую программу будет сложно читать, но сами изменяемые значения (счетчика), использовать можно. Рассмотрим пример.
Программа 9.3 Дано натуральное число N. Вывести все делители этого числа.

#include <iostream>
using namespace std;

int main() {
	int N;
	cout << "N = "; cin >> N;
	for (int i = 2; i < N / 2; i++) {
		if (N % i == 0)
			cout << i << " ";
	}
	return 0;
}
N = 16000
2 4 5 8 10 16 20 25 32 40 50 64 80 100 125 128 160 200 250 320 400 500 640 800 1000 1600 2000 3200 4000 

Область видимости данных

Мы уже знакомы с понятием составной инструкции, заключенной в фигурные скобки. Фигурные скобки определяют не только блок (составную инструкцию), но также и область видимости (англ. scope) переменных объявляемых внутри этого блока (включая заголовок цикла). Если попытаться получить доступ к счетчику вне тела цикла, то программа выдаст ошибку. Переменная i (например, в программе 9.3) является локальной переменной цикла – её видимость определяют { }. После завершения работы цикла переменная i, объявленная в заголовке, уничтожается и получить значение i (вне цикла) станет невозможным.

Примечание. Как мы уже сказали, переменная-счетчик не обязательно должна быть локальной. Если есть необходимость, то переменная-счетчик может быть объявлена вне цикла. В этом случае спецификатор типа (например, int) не используется. Тип данных счетчика может быть любой из арифметических типов, но чаще всего – это int.

Если переменная определена как локальная, но не была произведена инициализация этой переменной, то её значение неопределено и обращение к ее значению будет считаться ошибкой. Попытка изменить значения локальных переменных вне блока приведет к ошибке, но внутри блока можно получать и изменять значения всех объектов определенных вне блока (за исключением других локальных объектов). Более подробно локальную и глобальную видимость переменных мы обсудим позднее.

Использование инструкции continue в цикле for

При использовании инструкции continue в цикле for необходимо учитывать особенности работы этого цикла:

  • Инструкции, следующие после continue, будут пропущены
  • Затем происходит модификация счетчика
  • Переход к выполнению следующей итерации (иначе, проверки условия)
Покажем это на примере:

int main() {
	for (int i = 1; i < 20; i++) {
		if (i % 2 == 0) continue;
		cout << i << " ";
	}
1 3 5 7 9 11 13 15 17 19 

Обратите внимание: хотя вывод чисел по условию пропущен, но инкрементация счетчика выполняется. Этот пример приведен всего-лишь для иллюстрации, программировать цикл так не следует! Эту задачу лучше решить следующим образом:

int main() {
	for (int i = 1; i < 20; i += 2) 
		cout << i << " ";

Несколько выражений в разделе инициализации и модификации

Как мы уже отметили ранее в заголовке инструкции for должно быть три раздела. Выражения, находящееся в этих разделах, можно опускать, но нельзя опускать ";". В конце концов, можно оставить только ;. Заголовок в виде:

for (;;) { ... }

является заголовком “бесконечного” цикла. (Выход из цикла должен программироваться внутри тела цикла).
C++ поддерживает несколько выражений в разделах инициализации и модификации в заголовке инструкции for. При этом условие продолжения цикла должно быть одно!
Например. Вычислить факториал числа, не превосходящий 20.
Программа 9.4

#include <iostream>
using namespace std;

int main() {
	unsigned long long n;
	int i, k;
	cout << "k = "; cin >> k; // 0 <= k <= 20
	for(n = 1, i = 1; i <= k; n *= i, ++i);
	cout << k << "! = " << n << endl;
	return 0;
}
k = 20
20! = 2432902008176640000
Примечание. обратите внимание, что поток вывода в стр. 9 не относится к телу цикла! (В конце заголовка – ;). Таким образом, данный цикл в теле имеет пустую инструкцию, а все выражения вычисляются в заголовке. Программа 9.4 правильно вычисляет факториал числа от 0 до 20.

Цикл for основанный на диапазоне (range-based for)

Для перебора последовательности элементов (массива или контейнера) приходится выполнять однотипные действия и, при этом, использовать громоздкий код. Для упрощения работы с массивами в C++ существует специальная форма цикла forrange-based for (цикл for основанный на диапазоне или диапазонный for). Она была введена в C++11.
Синтаксис:

for (range_declaration : range_expression) 
    loop_statement

Нам пока не известно понятие массива. Рассмотрим пример работы цикла range-based for на примере списка инициализации:
Программа 9.5

#include <iostream>
using namespace std;

int main() {
	for (auto i : {'a', 'b', 'c', 'd'})
		cout << i << " => " << static_cast<int>(i) << endl;
	return 0;
}

Для автоматического выведения типа в этом цикле используется спецификатор auto (очевидно, что это тип char). Параметр цикла (i) поочередно, на каждом шаге, принимает значения элементов последовательности. Данная программа выводит символы списка и их коды. Более подробно работу этого типа цикла мы рассмотрим позднее, при изучении массивов.

Процессор случайных чисел

В целом ряде задач, в которых требуется моделировать различные явления, возникает необходимость работы с такими величинами, которые получены случайным образом. (Например, игра в подбрасывание кубиков). Для создания наборов случайных чисел в C++ существует библиотека random. В этой библиотеке определены два типа взаимосвязанных классов:

  • random-number engine – процессоры случайных чисел
  • random-number distribution – распределения случайных чисел
В библиотеке random несколько процессоров случайных чисел. По умолчанию используется процессор default_random_engine. (Полный список см. здесь).
Для определения диапазона генерации случайных чисел необходимо использовать распределение случайных чисел.

Примечание. Распределение вероятностей — это закон, описывающий область значений случайной величины и вероятности их исхода. Подробнее см. здесь.

Для распределения определены классы как для целых, так и для действительных типов:

  • uniform_int_distribution<type>
  • uniform_real_distribution<type>
Процесс создания последовательности случайных чисел будет состоять из следующих этапов:

  1. Создание объекта класса default_random_engine. Конструктор может иметь необязательный параметр – начальное значение;
  2. Создание объекта класса распределения с указанием, в качестве аргумента конструктора, промежутка на котором должна построиться последовательность случайных чисел;
  3. Получение случайного числа путем обращения к объекту распределения, которому, в качестве параметра, передается функциональный объект процессора.

На самом деле, случайные числа, генерируемые процессором, не являются случайными, а являются псевдослучайными, поскольку вычисляются по математическим формулам.
Процессор генерирует машинно-зависимую последовательность элементов. Это означает, что при каждом запуске программы будет генерироваться одна и та же последовательность. В этом можно убедиться несколько раз запустив программу 9.6
Программа 9.6

#include <iostream>
#include <random>
using namespace std;

int main() {
	// Подключаем процессор случайных чисел
	default_random_engine rnd;
	// Определяем распределение и указываем диапазон
	uniform_int_distribution<int> d(10, 100);
	for (int i = 0; i < 20; i++)
        cout << d(rnd) << " ";
	return 0;
}

Возможный вывод программы:

10 21 78 51 58 29 14 71 71 95 44 57 85 13 14 58 71 10 44 16 

Для того, чтобы процессор выдавал всегда различные наборы случайных чисел необходимо установить начальное значение последовательности как параметр процессора. Однако такое значение требуется изменять прикаждом запуске программы. Чтобы автоматизировать этот процесс для инициализации начального значения, используется момент системного времени, получаемый с помощью метода now() класса системных часов (system_clock) библиотеки chrono.

Примечание. Подробнее об этой библиотеке вы можете прочитать здесь.

Рассмотрим пример. Разработать программу игры “Угадай число”. Компьютер “загадывает” число, которое пользователь должен угадать за известное количество шагов.
Программа 9.7

#include <iostream>
#include <random>
#include <chrono>
using namespace std;
using namespace chrono;

int main() {
	int n = 10, k;
	// Получаем системный момент времени
	long seed = system_clock::now().time_since_epoch().count();
	// Инициализируем этим значением процессор
	default_random_engine rnd(seed);
	uniform_int_distribution<int> d(1, 100);
	// Компьютер загадал число:
	k = d(rnd);
	cout << "У вас " << n << " попыток" << endl;
	for (int i = 1; i <= n; i++) {
		cout << "Попытка " << i
			 << ".\nКакое число загадал компьютер? -> ";
		int a;
		cin >> a;
		if (a > k)
			cout << " много" << endl;
		else
			if (a < k)
				cout << " мало" << endl;
			else {
				cout << "Вы выиграли!" << endl;
				break;
			}
		if (!(n - i))
			cout << "Вы проиграли!";
	}
    return 0;
}

Вывод программы 9.7

У вас 10 попыток
Попытка 1.
Какое число загадал компьютер? -> 50
 много
Попытка 2.
Какое число загадал компьютер? -> 30
 много
Попытка 3.
Какое число загадал компьютер? -> 15
 мало
...
Попытка 7.
Какое число загадал компьютер? -> 17
Вы выиграли!

Вложенные циклы for

Так же как и другие инструкции циклов, for поддерживает структуру вложенных циклов. Кроме того, цикл одного типа может находиться внутри цикла другого типа. Если цикл работает от счетчика, то в случае с for запись будет осуществляться более компактно. Например. Вывести квадратную таблицу (матрицу) сложения от 1 до 20, представленной в 16-ой системе счисления.
Программа 9.8

#include <iostream>
#include <iomanip>
using namespace std;

int main() {
	for (int i = 0; i < 21; i++) {
		for (int j = 0; j < 21; j++) {
			cout << hex 	  // выводить в 16-ой системе
				 << uppercase // символы выводить в верхнем регистре
				 << setw(3)
				 << i + j;
		}
		cout << '\n';
	}
	return 0;
}

Вывод программы 9.8

  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F 10 11 12 13 14
  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F 10 11 12 13 14 15
  2  3  4  5  6  7  8  9  A  B  C  D  E  F 10 11 12 13 14 15 16
  3  4  5  6  7  8  9  A  B  C  D  E  F 10 11 12 13 14 15 16 17
  4  5  6  7  8  9  A  B  C  D  E  F 10 11 12 13 14 15 16 17 18
  5  6  7  8  9  A  B  C  D  E  F 10 11 12 13 14 15 16 17 18 19
  6  7  8  9  A  B  C  D  E  F 10 11 12 13 14 15 16 17 18 19 1A
  7  8  9  A  B  C  D  E  F 10 11 12 13 14 15 16 17 18 19 1A 1B
  8  9  A  B  C  D  E  F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C
  9  A  B  C  D  E  F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D
  A  B  C  D  E  F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E
  B  C  D  E  F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F
  C  D  E  F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20
  D  E  F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21
  E  F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22
  F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23
 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24
 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25
 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26
 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27
 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28

В некоторых задачах параметр вложенного цикла ставится в зависимость от изменения параметра внешнего цикла. Типичная задача – вывод по образцу. Например. Вывести числовой треугольник вида:

0 0 0 0 0 0 0 0 0 0 
1 1 1 1 1 1 1 1 1 
2 2 2 2 2 2 2 2 
3 3 3 3 3 3 3 
4 4 4 4 4 4 
5 5 5 5 5 
6 6 6 6 
7 7 7 
8 8 
9 

Программа 9.9

#include <iostream>
using namespace std;

int main() {
	for (int i = 0; i < 10; i++) {
		for (int j = 0; j < 10 - i; j++)
			cout << i << " ";
		cout << "\n";
	}
	return 0;
}

Мы вернемся к этому типу задач при заполнении матриц.

Вопросы

1. Может ли быть заменена в программе инструкция цикла for на инструкцию цикла while? Всегда ли это можно сделать?
2. Когда удобнее применять для организации циклов инструкцию for? while?
3. Возможны ли в заголовке инструкции for следующие записи:

    a) for (;a > b && !(a % 2);)
    b) for (a > b;;)
    c) for (;;i = 0)
    d) for (;i = 0;)
    e) for (;;i++, --b)
    f) for (--i;;)
    g) for (b = 0; b != a;) ?

4. Необходимо получать случайные числа из отрезка [10, 99]. В каком виде будет использоваться функция rand()?
5. Переменная i – параметр внешнего цикла, а j – вложенного. Доступна ли будет переменная j во внешнем цикле? i во вложенном цикле?

Практические задания

1. Перепишите программу 7.9 (из предыдущего параграфа) с помощью инструкции for.
2. Найти сумму всех n-значных чисел (1 <= n <= 4), кратных k.
3. Найти сумму всех двузначных чисел, не превосходящих двузначного числа k. Условную инструкцию не использовать! (Обратите внимание на то, что в разделе модификации не обязательно использовать инкремент).
4. Составить программу-игру, которая имитирует подбрасывание двух игральных кубиков. Условия выигрыша придумайте самостоятельно.

Print Friendly, PDF & Email

Comments are closed.