§13 Линейный C-массив и контейнер array

Инициализация. Ввод / вывод элементов массива

Массив – это структура данных, содержащая совокупность элементов, принадлежащих одному и тому же типу с общим для всех элементов именем. Для хранения массива используется область памяти, в которой элементы хранятся последовательно. Элементы массива пронумерованы индексами, следовательно доступ к элементу массива осуществляется по индексу. Индексирование элементов начинается с 0:

a[0], a[1], a[2] ... 

Описание C-массива производится следующим образом

Тип Имя [Количество элементов]; 

Например:

int mass[20];
float a[5];

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

int mass[10] {5, 4, 11, 9, 0, 10, 3, 8, 2, 13};
auto ar[] = {2, 4, 6, 8, 10, 12, 14};
float omega[] {1.02, 4.2, 3.7, 2.1};
int alpha[100] {};
Количество элементов инициализируемого массива (omega) можно опустить, компилятор сам подсчитает их количество. Элементы массива alpha будут иметь значение 0. Обратите внимание, что если используется спецификатор auto, то операция = обязательна! (В остальных случаях использовать можно, но не обязательно).

Объем памяти, занятой элементами массива, можно определить с помощью операции sizeof().

sizeof(имя_массива)

Следовательно, размер массива можно определить так:

размер_массива = sizeof(имя_массива) / sizeof(тип)

Но лучше применят функцию std::size(). Функция возвращает, упомянутый ранее, тип size_t.
С-массивы являются статическими, то есть – это массивы фиксированного размера. Нельзя присваивать значение элементов одного C-массива элементам другого массива обращаясь по имени, например: mass1 = mass2. Подобным образом нельзя сравнивать C-массивы и производить с ними арифметические операции. Изменять значения элементов массива можно только поэлементно, обращаясь к элементу по имени массива и индексу:

mass[0] = 15;
omega[2] = 0.3;

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

Программа cpp-13.1

Инициализация и вывод элементов массива

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

int main() {
	int mass[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
	for (int i = 0; i < 10; i++)
		cout << mass[i] << setw(3);
	return 0;
}
Заполнять массив прогрессией с шагом 1 можно с помощью функции iota() из числовой библиотеки numeric:

void iota(first, last, value);

где: first и last – итераторы (см. ниже), а value – начальное значение последовательности (тип должен поддерживать операцию ++).

Программа cpp-13.2

Клавиатурный ввод элементов массива.

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

int main() {
	int mass[10];
	// Ввод элементов
	for (int i = 0; i < 10; i++) {
		cout << "mass[" << i <<"] = ";
		cin >> mass[i];
	}
	// Вывод элементов
	for (int i = 0; i < 10; i++)
		cout << mass[i] << setw(3);
	return 0;
}

Цикл for основанный на диапазоне

Помимо обычной инструкции for, стандарт C++11 ввел в использование специальную форму цикла for (range-based for), так называемый цикл for основанный на диапазоне. Такая форма выглядит более лаконичной. Так, например, вывод элементов массива в программе cpp-13.2 можно осуществить следующим образом:

for (auto &ar : mass)
	cout << ar << setw(3);

В переменной ar перебираются все элементы массива mass. Эта переменная может быть как ссылочного типа (это показано в примере), так и обычной. Если используется переменная не ссылочного типа, то элементы массива будут копироваться.

Заполнение массива случайными числами

Для решения задач с большими массивами часто возникает необходимость быстро заполнить массив данными. Для этого можно воспользоваться библиотекой random и заполнить массив случайными числами.

Программа cpp-13.3

Заполнить массив случайными числами из отрезка [10, 99].

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

int main() {
	int mass[20];
	int seed = system_clock::now().time_since_epoch().count();
	default_random_engine rnd(seed);
	uniform_int_distribution<int> d(10, 99);
	for (int i = 0; i < 20; i++)
		mass[i] = d(rnd);
	cout << "Элементы массива:" << endl;
	for (int i = 0; i < 20; i++)
		cout << mass[i] << setw(3);
	return 0;
}
Элементы массива:
48 97 59 18 36 27 75 79 98 77 70 80 13 59 17 81 56 72 92 98

Указатели и С-массивы

Имя массива является указателем-константой, значением которой служит адрес первого элемента массива (&arr[0]). Следовательно, имя массива может являться инициализатором указателя к которому будут применимы все правила адресной арифметики, связанной с указателями (подробнее см. в §4).

int arr[10];
int *p = arr; // что равносильно int *p = &arr[0]

Что это дает? Обход массива с помощью указателей осуществляется с большей производительностью, нежели с применением операции обращения по индексу. Это связано с тем, что на каждом шаге цикла осуществляется пересчет позиции элемента, так как операция arr[i] равносильна операции *(arr + i). Если для обхода массива используются указатели, то для перемещения указателя к следующему элементу применяется операция инкремента, а для доступа к значению элемента операция разыменования. Например:

int arr[10] {2, 4, 6, 8, 10, 12, 14, 16, 18, 20};
int *p = arr;
for (int i = 0; i < 10; i++) {
    cout << *p << " ";
    p++;
    // или так, объединив две эти инструкции:
    // cout << *p++ << " ";
}

Операция инкремента, применяемая к указателю, означает, что величина адреса памяти, хранящегося в указателе, увеличится на размер типа. Иными словами, будет осуществлен переход к следующему элементу.
pointer
Мы можем убедиться в том, что элементы массива следуют в памяти один за другим, если выведем их адреса:

for (int i = 0; i < 10; i++){
	cout << "arr[" << i << "] => " << p++ << endl;
}

Возможный вывод

arr[0] => 0xbffc8f00
arr[1] => 0xbffc8f04
arr[2] => 0xbffc8f08
arr[3] => 0xbffc8f0c
arr[4] => 0xbffc8f10
arr[5] => 0xbffc8f14
arr[6] => 0xbffc8f18
arr[7] => 0xbffc8f1c
arr[8] => 0xbffc8f20
arr[9] => 0xbffc8f24

Массивы, как и переменные, можно временно создавать, в процессе работы программы, с помощью указателей. Именно так создаются динамические массивы. Если размер массива для хранения элементов не будет удовлетворять, то создается новый - большего размера, затем осуществляется перенос данных из исходного массива в новый, а исходный удаляется. Для выделения памяти под массив используется операция new. Синтаксис выделения памяти для массива имеет вид:
Тип указатель = new Тип[размер_массива].
Например:

int n = 10;
int *arr = new int[n];

Освобождение памяти производится с помощью операции delete:

delete [] arr;

При этом, размер массива не указывается.
Решим задачу. Заполнить целочисленный массив arr1, созданный динамическим выделением памяти, случайными числами. Показать исходный массив. Переписать в новый целочисленный массив arr2, созданный подобным же образом, все элементы с нечетными порядковыми номерами (1, 3, ...). Вывести содержимое массива arr2.

Программа cpp-13.4
#include <iostream>
#include <ctime>
#include <random>
using namespace std;

int main() {
	int n;
	cout << "n = "; cin >> n;
	int *arr1 = new int[n];
	default_random_engine rnd(time(0));
	uniform_int_distribution<int> d(10, 99);
	for (int i = 0; i < n; i++) {
        	arr1[i] = d(rnd);
        	cout << arr1[i] << " ";
	}
	cout << endl;
	int *arr2 = new int[n / 2];
	for (int i = 0; i < n / 2; i++) {
		arr2[i] = arr1[2 * i + 1];
		cout << arr2[i] << " ";
	}
	delete [] arr1;
	delete [] arr2;
	return 0;
}

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

Итераторы

Итератор (iterator, англ. перечислитель) - это такой объект, который позволяет перебирать элементы массива, переходя от одного элемента - к другому. Итераторы функционально схожи с указателями. Разница между обычным указателем и итератором заключается в том, что последний предназначен для обхода сложных структур данных, которые управляются с помощью функционала std, прежде всего, это контейнеры (или классы контейнеров). Контейнеры поддерживают определенный тип итератора, который наделен своим собственным набором доступных операций. Эти наборы довольно сильно отличаются.
Контейнер array, который мы будем рассматривать ниже, использует тип итераторов, который обладает самым большим функционалом — RandomAccessIterator (итератор произвольного доступа), который позволяет обходить контейнер в любом порядке, а также изменять содержимое контейнера (array, vector) в процессе обхода.
Для этого типа итераторов определены следующие операции:

  • it -> mem
  • ++it, it++
  • --it, it--
  • it[i] (конвертирование в ссылку)
  • *it
  • +, -
  • <, <=, >, >=, ==, !=
  • +=, -=

Две операции *it, it++ можно объединить в одну инструкцию, возвращающую ссылку: *it++.

Перед использованием итераторов в контейнере array их необходимо определить:

array<int, 10>::iterator first;
array<int, 10>::iterator last;

Для получения итератора не нужно использовать операцию взятия адреса (&). Для этих целей, каждый класс контейнеров имеет специальные функции-члены. Наиболее важные функции-члены возвращающие итераторы - это begin() и end(). Функция begin() возвращает итератор, указывающий на начало контейнера (первый элемент). Функция end() возвращает итератор, указывающий на позицию, следующую за последним элементом. Таким образом, функции begin() и end() определяют полуоткрытый интервал [first, last).
iter1
Чтобы не использовать громоздкий синтаксис типа (например, array<int, 10>::iterator) удобнее использовать спецификатор auto, позволяющий вывести тип итератора автоматически:

auto first = mas.begin();
auto last = mas.end();

Аналогичные функции std::begin() и std::end() введены и для использования с С-массивами. В этом случае определение будет выглядеть следующим образом:

auto first = begin(ar);
auto last  = end(ar);

где first и last - имена итераторов, а ar - имя массива. Начиная с С++17 эти функции определены в простарнстве имен std, поэтому подключение заголовка <iterator> не требуется.
Для перемещения итератора по элементам массива используется операция ++, применяемая к итератору first:

first++

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

Программа cpp-13.5

Заполнить массив членами арифметической прогрессии, первый член которой равен a, а знаменатель q.

#include <iostream>
using namespace std;

int main() {
    int a, q;
    cout << "a = "; cin >> a;
    cout << "q = "; cin >> q;
    int ar[20];
    auto first = begin(ar);
    auto last  = end(ar);
    while (first != last) {
        *first = a;
        a += q;
        first++;
    }
    for (int &mas : ar)
        cout << mas << " ";
    cout << endl;
    return 0;
}
Реверсивные итераторы

Реверсивные итераторы предназначены для обхода массива с конца. Реверсивные итераторы возвращают функции rbegin() и rend(). В этой форме итераторов последний элемент массива представляется как если бы он стоял на первом месте в развернутой в обратную сторону последовательности. Соответственно последний элемент (гипотетический) соответствует позиции элемента предшествующего первому элементу в неразвернутой последовательности. Эта форма итераторов ничем не отличается от использования обычной формы итераторов (begin() и end()), функционально они равнозначны. Удобство применения этих итераторов в полной мере раскроется при использовании обобщенных алгоритмов.
В следующей задаче необходимо получить новый массив сформированный из элементов исходного массива взятых в обратном порядке.

Программа cpp-13.6
#include <iostream>
using namespace std;

int main() {
    int mas1[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
    int mas2 [size(mas1)] {};
    auto first = rbegin(mas1);
    auto start = begin(mas2);
    for (auto &r : mas1) {
        cout << r << " ";
        *start++ = *first++;
    }
    cout << endl;
    for (auto &r : mas2)
        cout << r << " ";
    return 0;
}

Вывод

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

Контейнер array

Как вы могли заметить, стандартный С-массив имеет ряд недостатков. Для более удобной и безопасной работы с последовательностями фиксированного размера в C++ используется шаблонный класс контейнера array, позволяющий создавать объекты подобные статическим С-массивам с равной эффективностью. Преимущество использования класса array (по сравнению с C-массивами) в том, что он предоставляет ряд удобных функций, общих для всех встроенных контейнеров, таких как сравнение, присваивание, итераторы, агрегатная инициализация и другие.
Для создания объекта array необходимо включить одноименный заголовочный файл:

#include <array>
Конструктор

В классах и шаблонных классах для создания объекта (экземпляра класса) используется конструктор. Конструктор перегружается (о перегрузках мы будем говорить позднее) для того чтобы предоставить несколько возможных вариантов создания объекта (с начальной инициализации или без таковой). Конструктор объекта класса array по умолчанию имеет вид:

array<тип, размер> имя;

Если при определении объекта типа array начальные значения (элементов) не передаются, то создается массив элементов, значения которых не определены. Объекты класса array можно создавать копированием другого объекта array. В этом случае, типы объектов должны совпадать:

array<int, 20> mas1(mas2); // или
array<int, 20> mas1 = mas2;

Поскольку array является агрегатным типом, то это позволяет использовать для инициализации агрегатную инициализацию с помощью фигурных скобок. Однако, обработать существующий объект std::initializer_list array не может, так как в нем нет соответствующего конструктора.

Агрегатом называется массив, структура или объединение которые не имеют конструкторов и защищённых членов (с этими понятиями мы познакомимся позднее).

Инициализировать массив списком инициализации можно следующим образом:

array<int, 5> mass1 {};
array<int, 5> mass2 {8};
array<int, 5> mass3 {1, 2, 3, 4, 5};
// Начиная с С++17, шаблонный параметр можно опустить:
array mass3 {1, 2, 3, 4, 5};

В первой строке мы получим массив из 5 нулевых элементов. Во второй - значением 8 будет инициализирован первый элемент, остальные - значением по умолчанию.

Деструктор

Уничтожается объект класса array специальной функцией - деструктор. Все элементы массива (mas) удаляются, а память освобождается:

mas.~array();
Функции класса
  • at() – доступ к элементу по индексу с проверкой границ массива
  • operator[] – доступ к элементу по индексу
  • front() – доступ к первому элементу
  • back() – доступ к последнему элементу
  • data() – прямой доступ к C-массиву
  • empty() – проверяет пустой (true) контейнер или нет
  • size() – размер массива
  • fill() – заполнение контейнера определенным значением
  • swap() – обмен элементами между двумя массивами
  • operator<, operator<=, operator>, operator>=, operator==, operator!= - операции сравнения
Пример работы с методами класса array и функциями библиотеки обобщенных алгоритмов представлен в программе ниже.

Программа cpp-13.7
#include <iostream>
#include <array>
#include <initializer_list>
#include <algorithm> // sort и reverse
using namespace std;

int main() {
    initializer_list<int> iList {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
    array<int, 10> ar1 {};
    array<int, 10> ar2 {1, 7, 5, 4, 6, 2, 1, 0, 9, 8};
    array<int, 10> ar3 {};
    // Хотя инициализировать array iList нельзя,
    // но можно скопировать из iList в массив
    copy(begin(iList), end(iList), ar1.begin());
    ar1.swap(ar2); // Обмен элементами между массивами
    if (!ar3.empty()) // Массив не пустой!
        for (size_t i = 0; i < 10; i++)
            ar3.at(i) = ar1.at(i) * ar2.at(i);
    if (ar1 > ar2) // Производится поэлементное сравнение
        reverse(ar2.begin(), ar2.end()); // развернуть массив
    sort(ar1.begin(), ar1.end()); // сортировка массива
    cout << "ar1\tar2\tar3" << endl;
    for (size_t i = 0; i < 10; i++) {
        cout << ar1.at(i) << '\t'
             << ar2.at(i) << '\t'
             << ar3.at(i) << endl;
    }
    return 0;
}

Вывод

ar1     ar2     ar3
0       0       1
1       9       14
1       8       15
2       7       16
4       6       30
5       5       12
6       4       7
7       3       0
8       2       81
9       1       0

Постановка следующей задачи. Дан массив. Определить:

  1. среднее арифметическое элементов массива;
  2. количество элементов меньших k.
  3. Поменять местами максимальный и минимальный элементы массива
Программа cpp-13.8
#include <iostream>
#include <array>
#include <random>
#include <chrono>
using namespace std;
using namespace chrono;

int main() {
//============================= 1 =============================//
	int seed = system_clock::now().time_since_epoch().count();
    default_random_engine rnd(seed);
    uniform_int_distribution<int> d(10, 99);
    int s = 0, k;
    array<int, 20> ar {};
    cout << "Исходный массив:" << endl;
    for (auto &i : ar) {
        i = d(rnd);
        cout << i << " ";
        s += i;
    }
    cout << endl;
    cout << "Среднее арифметическое => "
         << float(s) / 20 << endl;
//=========================== 2, 3 ===========================//
    cout << "k = "; cin >> k;
    s = 0;
    int max = ar[0], m = 0;
    int min = ar[0], n = 0;
    for (size_t i = 1; i < ar.size(); i++) {
        if (ar[i] < k) s++;
        if (ar[i] > max) {max = ar[i]; m = i;}
        if (ar[i] < min) {min = ar[i]; n = i;}
    }
    cout << "Элементов < " << k
         << " => " << s << endl;
    s = ar[m];
    ar[m] = ar[n];
    ar[n] = s;
    cout << "Измененный массив:" << endl;
    for (auto &i : ar) {
        cout << i << " ";
    }
    cout << endl;
    return 0;
}
Исходный массив:
62 21 79 88 51 79 13 86 56 61 72 97 18 79 83 51 71 38 88 32 
Среднее арифметическое => 61.25
k = 20
Элементов < 20 => 2
Измененный массив:
62 21 79 88 51 79 97 86 56 61 72 13 18 79 83 51 71 38 88 32 
Заметим, что в библиотеке обобщенных алгоритмов (заголовок <algorithm>) имеется специальная функция minmax_element(), которая возвращает пару итераторов: min элемент в последовательности - это первый элемент пары, а max - второй элемент пары. Аргументы функции - итераторы диапазона, для поиска.

Решим еще одну задачу на использование абстрактных типов в массивах. Дан массив сведений о 20 выстрелах в виде пары координат (x, y) - места попадания снаряда в области ограниченной окружностью с радиусом R и центром в начале координат. Выяснить, наблюдается ли кучность стрельбы и отклонение от центра. Кучность стрельбы наблюдается, если не менее 70% попаданий находится в области окружности с радиусом r, а отклонение не наблюдается, если количество попаданий в опредленной четверти не превысит 30%.

Программа cpp-13.9
#include <iostream>
#include <array>
#include <cmath> // hypot
using namespace std;

int main() {
    double R, r;
    cin >> R >> r;
    array<pair<double, double>, 20> ar;
    array<int, 4> crd {}; // Массив счетчиков
    for (auto &i : ar) { // Ввод массива пар
        cin >> i.first >> i.second;
    }
    int j {};
    for (auto &i : ar) { // Анализ
        double x = i.first; // Распаковываем пару
        double y = i.second;
        double g = hypot(x, y); // Находим расстояние от центра
        if (g > R) continue; // Если промах, пропускаем
        if (g < r) j++; // Точные попадания
        if (x > 0 && y > 0) ++crd[0]; // Корреляция
        if (x < 0 && y > 0) ++crd[1];
        if (x < 0 && y < 0) ++crd[2];
        if (x > 0 && y < 0) ++crd[3];
    }
    if (j / 20. > 0.7)
        cout << "Наблюдается кучность стрельбы" << endl;
    else
        cout << "Нет кучности стрельбы!" << endl;
    if (crd[0] / 20. > 0.3 ||
        crd[1] / 20. > 0.3 ||
        crd[2] / 20. > 0.3 ||
        crd[3] / 20. > 0.3)
        cout << "Отклонение попаданий от центра!" << endl;
    else
        cout << "Отклонений нет!" << endl;
    return 0;
}

Вывод

10 3
-1 4
2 1
1 1
0 0
1 0
0 1
-7 -3
-1 -1
-1 0
-2 2
2 -2
1 -2
-2 -2
7 20
3 1
3 3
-3 -1
-1 0
-2 4
-3 3
Нет кучности стрельбы!
Отклонений нет!
Примеры решения задач
  • Ввод и вывод массива. Дано целое число N (> 0). Сформировать и вывести целочисленный массив размера N, содержащий N первых положительных нечетных чисел: 1, 3, 5, ....
  • #include <iostream>
    using namespace std;
    
    int main() {
    	int n;
    	cout << "n = "; cin >> n;
    	int mas[n];
    	for (int i = 0; i < n; i++) {
    		mas[i] = 2 * i + 1;
    		cout << mas[i] << ' ';
    	}
    
    	return 0;
    }
    
    k = 10
    1 3 5 7 9 11 13 15 17 19 
    
  • Анализ элементов массива. Дан целочисленный массив размера N, не содержащий одинаковых чисел. Проверить, образуют ли его элементы арифметическую прогрессию. Если образуют, то вывести разность прогрессии, если нет — вывести 0.
  • #include <iostream>
    using namespace std;
    
    int main() {
        int mas[] {1, 3, 5, 7, 9, 11, 13, 15};
        // Проверяем арифметическую прогрессию
        int m = mas[1] - mas[0];
        bool flag = true;
        for (int i = 0; i < 7; i++) {
            if (mas[i + 1] - mas[i] != m) {
                flag = false;
                break;
            }
        }
        cout <<  (flag ? m : 0) << endl;
        return 0;
    }
    
  • Изменение массива. Дан массив размера N. Обнулить все его локальные максимумы (то есть числа, большие своих соседей).
  • #include <iostream>
    #include <iomanip>
    #include <ctime>
    #include <cstdlib>
    #include <array>
    using namespace std;
    
    int main() {
    	array<int, 50> mas;
    	array<size_t, 25> pos;
    	srand(time(0));
    	size_t j = 0;
    	// Заполняем массив и показываем
    	for (auto &ar : mas) {
    		ar = 10 + rand() % 90;
    		cout << ar << setw(3);
    	}
    	// Реализация
    	// Запоминаем где находятся эти локальные максимумы
    	for (size_t i = 1; i < mas.size() - 1; i++)
    		if (mas[i] > mas[i - 1] && mas[i] > mas[i + 1]) {
                pos[j] = i;
                j++;
            }
        // Заменяем их нулями
        j = 0;
        for (size_t i = 1; i < mas.size() - 1; i++)
    		if (i == pos[j]) {
                mas[i] = 0;
                j++;
            }
    	// Показываем измененный массив
    	cout << "\nЭлементы измененного массива:" << endl;
    	for (auto &ar : mas)
    		cout << ar << setw(3);
    	return 0;
    }
    
    95 72 55 43 64 71 18 22 41 26 27 55 45 30 48 32 34 11 39 95 77 14 89 54 42 77 58 65 72 92 17 68 64 62 63 28 86 72 93 27 50 20 34 48 40 72 70 26 35 10
    Элементы измененного массива:
    95 72 55 43 64  0 18 22  0 26 27  0 45 30  0 32  0 11 39  0 77 14  0 54 42  0 58 65 72  0 17  0 64 62  0 28  0 72  0 27  0 20 34  0 40  0 70 26  0 10
    
Вопросы
1. Возможны ли такие выражения для C-массива?

mass1[1] = mass2[1]
int mass1[2] = int mass2[2]
mass1[1] == mass2[1]
int mass1[2] = 10
int mass1[10] = mass2[1]
int mass1[2] > int mass2[2]
Темы сообщений
Задания А
1. Дано целое число N (> 0). Сформировать и вывести целочисленный массив размера N, содержащий степени двойки от первой до N-й: 2, 4, 8, 16, … .
2. Даны целые числа N (> 2), A и B. Сформировать и вывести целочисленный массив размера N, первый элемент которого равен A, второй равен B, а каждый последующий элемент равен сумме всех предыдущих.
3. Дан целочисленный массив размера N, не содержащий одинаковых чисел. Проверить, образуют ли его элементы арифметическую прогрессию. Если образуют, то вывести разность прогрессии, если нет — вывести false.
Задания Б
Задания С
Ссылки
1 Звезда2 Звезды3 Звезды4 Звезды5 Звезд (1 оценок, среднее: 5,00 из 5)
Загрузка...

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

Print Friendly, PDF & Email

Обсуждение закрыто.