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

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

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

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

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

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

Например:

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

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

int mass[10] {5, 4, 11, 9, 0, 10, 3, 8, 2, 13};
float omega[] {1.02, 4.2, 3.7, 2.1};
int alpha[100] {};

Примечание: Количество элементов инициализируемого массива (omega) можно опустить, компилятор сам посчитает их количество. Элементы массива alpha будут иметь значение 0.
Объем памяти, занятой элементами массива, можно определить с помощью оператора sizeof().

Количество_байтов = sizeof(тип) * размер_массива

Примечание. Собственно, размер массива в байтах можно определить и так: sizeof(имя_массива), т. о.

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

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

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

Поэтому, на практике, элементы обрабатываются всей совокупностью с помощью инструкции циклов. Поскольку количество элементов C-массива известно, то удобно использовать инструкцию цикла for.
Программа 10.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;
}

Программа 10.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 основанный на диапазоне. Такая форма выглядит более лаконичной. Так, например, вывод элементов массива в программе 10.2 можно осуществить следующим образом:

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

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

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

Для решения задач с большими массивами часто возникает необходимость быстро заполнить массив данными. Для этого можно воспользоваться библиотекой random и заполнить массив случайными числами.
Программа 10.3 Заполнить массив случайными числами из отрезка [10, 99].

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

int main() {
	int mass[20];
	default_random_engine rnd(time(0));
	uniform_int_distribution<unsigned> 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;
}

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

Указатели тесно связаны с C-массивами. Дело в том, что имя массива является константным указателем на первый элемент массива. Следовательно, именем массива можно инициализировать указатель. Например:

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

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

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

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

Итераторы

Итератор (iterator) - это такой объект, который позволяет перебирать элементы массива, переходя от одного элемента - к другому. Итераторы функционально схожи с указателями. Разница между обычным указателем и итератором заключается в том, что последний является интеллектуальным указателем, т. е. указателем, который может обходить сложные структуры данных.
iter1
Для получения итератора не нужно использовать операцию взятия адреса (&). Для этих целей существуют специальные функции begin() и end(). Чтобы эти функции стали доступны, необходимо включить следующую директиву:

#include <iterator>

Функция begin() возвращает итератор, указывающий на начало массива (первый элемент), а end() возвращает итератор, указывающий на позицию, следующую за последним элементом. Таким образом, функции begin() и end() определяют полуоткрытый интервал [first, last).
Перед использованием итераторов их необходимо объявить:

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

где first и last - имена итераторов, а ar - имя массива.
Для перемещения итератора по элементам массива используется операция ++, применяемая к итератору first:

first++

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

#include <iostream>
#include <iterator>
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;
}

Контейнер array

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

#include <array>

Объект класса array создается следующим образом:

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

Приведем пример работы с объектом класса array.
Постановка задачи. Дан массив. Определить:

  1. среднее арифметическое элементов массива;
  2. количество элементов меньших k.
  3. Поменять местами максимальный и минимальный элементы массива

Программа 10.5

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

int main() {
//============== 1 ================//
    default_random_engine rnd(time(0));
    uniform_int_distribution<unsigned> 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;
}

Примечание. В строке 26 используется специальный тип size_t. Это машиннозависимый беззнаковый целый тип, возвращаемый методом size() (используется и в других методах, в которых необходимо возвращать или принимать размер или позицию, если это не итераторы).

Вопросы

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.


Добавить комментарий