10.2– Массивы (часть 2)
Данный урок продолжает обсуждение массивов, начатое в уроке «10.1 – Массивы (часть 1)».
Инициализация фиксированных массивов
Элементы массива обрабатываются так же, как обычные переменные, и поэтому при создании они не инициализируются.
Один из способов «инициализировать» массив – делать это поэлементно:
int prime[5]; // храним первые 5 простых чисел
prime[0] = 2;
prime[1] = 3;
prime[2] = 5;
prime[3] = 7;
prime[4] = 11;
Однако это утомительно, особенно когда массив становится больше. Более того, это не инициализация, а присваивание. Присваивания не работают, если массив является константным.
К счастью, C++ предоставляет более удобный способ инициализации массивов целиком с помощью списка инициализаторов. В следующем примере массив инициализируется теми же значениями, что и в приведенном выше:
// используем список инициализаторов для инициализации фиксированного массива
int prime[5]{ 2, 3, 5, 7, 11 };
Если в списке инициализаторов больше, чем может вместить массив, компилятор выдаст ошибку.
Однако, если в списке инициализаторов меньше, чем может содержать массив, оставшиеся элементы инициализируются значением 0 (или любым другим значением, в которое 0 преобразуется для нецелочисленного базового типа – например, 0.0 для double
). Это называется нулевой инициализацией.
Следующий пример показывает это в действии:
#include <iostream>
int main()
{
int array[5]{ 7, 4, 5 }; // инициализируем только первые 3 элемента
std::cout << array[0] << '\n';
std::cout << array[1] << '\n';
std::cout << array[2] << '\n';
std::cout << array[3] << '\n';
std::cout << array[4] << '\n';
return 0;
}
Эта программа печатает:
7
4
5
0
0
Следовательно, чтобы инициализировать все элементы массива нулями, вы можете сделать так:
// Инициализируем все элементы значением 0
int array[5]{ };
// Инициализируем все элементы значением to 0.0
double array[5]{ };
// Инициализируем все элементы пустой строкой
std::string array[5]{ };
Если список инициализаторов опущен, элементы не инициализируются, если они не относятся к типу класса.
// неинициализированный
int array[5];
// неинициализированный
double array[5];
// инициализируем все элементы пустой строкой
std::string array[5];
Лучшая практика
Инициализируйте массивы явно, даже если они будут инициализированы без списка инициализаторов.
Опущенное значение длины
Если вы инициализируете фиксированный массив элементов с помощью списка инициализаторов, компилятор может определить длину массива за вас, и вы можете не указывать длину массива явно.
Следующие две строки эквивалентны:
int array[5]{ 0, 1, 2, 3, 4 }; // определяем длину массива явно
int array[]{ 0, 1, 2, 3, 4 }; // позволяем списку инициализаторов установить длину массива
Это не только экономит время на ввод текста, но и означает, что вам не нужно обновлять длину массива, если позже вы добавите или удалите элементы.
Массивы и перечисления
Одна из серьезных проблем с документированием массивов заключается в том, что целочисленные индексы не предоставляют программисту никакой информации о значении индекса. Рассмотрим класс из 5 учеников:
constexpr int numberOfStudents{5};
int testScores[numberOfStudents]{};
testScores[2] = 76;
Кого представляет testScores[2]
? Не ясно.
Это можно решить, создав перечисление, в котором перечислители сопоставляются с каждым из возможных индексов массива:
enum StudentNames
{
kenny, // 0
kyle, // 1
stan, // 2
butters, // 3
cartman, // 4
max_students // 5
};
int main()
{
int testScores[max_students]{}; // размещаем 5 значений int
testScores[stan] = 76;
return 0;
}
Таким образом, становится намного понятнее, что представляет каждый из элементов массива. Обратите внимание, что был добавлен дополнительный перечислитель с именем max_students
. Этот перечислитель используется во время объявления массива, чтобы гарантировать, что массив имеет правильную длину (так как длина массива должна быть на единицу больше, чем наибольший индекс). Это полезно как для документирования, так и потому, что при добавлении еще одного перечислителя размер массива будет изменен автоматически:
enum StudentNames
{
kenny, // 0
kyle, // 1
stan, // 2
butters, // 3
cartman, // 4
wendy, // 5
max_students // 6
};
int main()
{
int testScores[max_students]{}; // размещаем 6 значений int
testScores[stan] = 76; // всё еще работает
return 0;
}
Обратите внимание, что этот «трюк» работает только в том случае, если вы не изменяете значения перечислителей вручную!
Массивы и классы перечислений
Классы перечислений не имеют неявного преобразования в целочисленный тип, поэтому, если вы попробуете следующее:
enum class StudentNames
{
kenny, // 0
kyle, // 1
stan, // 2
butters, // 3
cartman, // 4
wendy, // 5
max_students // 6
};
int main()
{
int testScores[StudentNames::max_students]{}; // размещаем 6 значений int
testScores[StudentNames::stan] = 76;
return 0;
}
Вы получите ошибку компиляции. Это можно решить, используя static_cast
для преобразования перечислителя в целочисленный тип:
int main()
{
int testScores[static_cast<int>(StudentNames::max_students)]{}; // разместить 6 значений int
testScores[static_cast<int>(StudentNames::stan)] = 76;
return 0;
}
Однако делать так довольно утомительно, поэтому может быть лучше использовать обычное перечисление внутри пространства имен:
namespace StudentNames
{
enum StudentNames
{
kenny, // 0
kyle, // 1
stan, // 2
butters, // 3
cartman, // 4
wendy, // 5
max_students // 6
};
}
int main()
{
int testScores[StudentNames::max_students]{}; // разместить 6 значений int
testScores[StudentNames::stan] = 76;
return 0;
}
Передача массивов в функцию
Хотя передача массива в функцию на первый взгляд выглядит так же, как передача обычной переменной, но под капотом C++ обрабатывает массивы по-другому.
Когда обычная переменная передается по значению, C++ копирует значение аргумента в параметр функции. Поскольку параметр является копией, изменение значения параметра не приводит к изменению значения исходного аргумента.
Однако, поскольку копирование больших массивов может быть очень дорогостоящим, C++ не копирует массив при его передаче в функцию. Вместо этого передается исходный массив. Это имеет побочный эффект, поскольку функциям позволяется напрямую изменять значение элементов массива!
Следующий пример иллюстрирует эту концепцию:
#include <iostream>
void passValue(int value) // value является копией аргумента
{
value = 99; // поэтому его изменение здесь не изменит значение аргумента
}
void passArray(int prime[5]) // prime - это исходный массив
{
prime[0] = 11; // поэтому его изменение здесь изменит исходный аргумент!
prime[1] = 7;
prime[2] = 5;
prime[3] = 3;
prime[4] = 2;
}
int main()
{
int value{ 1 };
std::cout << "before passValue: " << value << '\n';
passValue(value);
std::cout << "after passValue: " << value << '\n';
int prime[5]{ 2, 3, 5, 7, 11 };
std::cout << "before passArray: " << prime[0] << " " << prime[1] << " " << prime[2] << " " << prime[3] << " " << prime[4] << '\n';
passArray(prime);
std::cout << "after passArray: " << prime[0] << " " << prime[1] << " " << prime[2] << " " << prime[3] << " " << prime[4] << '\n';
return 0;
}
before passValue: 1
after passValue: 1
before passArray: 2 3 5 7 11
after passArray: 11 7 5 3 2
В приведенном выше примере value
в main()
не изменяется, потому что значение параметра в функции passValue()
было копией значения переменной в функции main()
, а не исходной переменной. Однако, поскольку параметр массива в функции passArray()
является исходным массивом, passArray()
может напрямую изменять значение элементов!
Почему это происходит, связано с тем, как массивы реализованы в C++, и мы еще вернемся к этой теме после того, как рассмотрим указатели. На данный момент вы можете рассматривать это как причуду языка.
В качестве отступления от темы: если вы хотите убедиться, что функция не изменяет переданные в нее элементы массива, вы можете сделать массив константным:
// несмотря на то, что на самом деле массив является исходным,
// в этой функции он должен рассматриваться как константа
void passArray(const int prime[5])
{
// поэтому каждая из этих строк вызовет ошибку компиляции!
prime[0] = 11;
prime[1] = 7;
prime[2] = 5;
prime[3] = 3;
prime[4] = 2;
}
Определение длины массива
Для определения длины массивов может использоваться функция std::size()
из заголовка <iterator>
.
Например:
#include <iostream>
#include <iterator> // for std::size
int main()
{
int array[]{ 1, 1, 2, 3, 5, 8, 13, 21 };
std::cout << "The array has: " << std::size(array) << " elements\n";
return 0;
}
Эта программа напечатает:
The array has: 8 elements
Обратите внимание, что из-за того, как C++ передает массивы функциям, это не будет работать для массивов, которые были переданы функции!
#include <iostream>
#include <iterator>
void printSize(int array[])
{
std::cout << std::size(array) << '\n'; // ошибка
}
int main()
{
int array[]{ 1, 1, 2, 3, 5, 8, 13, 21 };
std::cout << std::size(array) << '\n'; // напечатает размер массива
printSize(array);
return 0;
}
std::size()
будет работать и с другими типами объектов (такими как std::array
и std::vector
), и она вызовет ошибку компиляции, если вы попытаетесь использовать ее с фиксированным массивом, который был передан в функцию! Обратите внимание, что std::size()
возвращает значение без знака. Если вам нужно значение со знаком, вы можете либо выполнить приведение типа результата, либо, начиная с C++20, использовать std::ssize()
(означает «signed size», размер со знаком).
std::size()
была добавлена в C++17. Если вы всё еще используете старый компилятор, вам придется использовать вместо нее оператор sizeof
. sizeof
не так прост в использовании, как std::size()
, и есть несколько вещей, на которые следует обратить внимание. Если вы используете компилятор с поддержкой C++17, можете перейти к разделу «Индексирование массива вне допустимого диапазона».
Оператор sizeof
может использоваться с массивами, в этом случае он вернет общий размер массива (длина массива, умноженная на размер элемента).
#include <iostream>
int main()
{
int array[]{ 1, 1, 2, 3, 5, 8, 13, 21 };
std::cout << sizeof(array) << '\n'; // напечатает размер массива, умноженный на размер int
std::cout << sizeof(int) << '\n';
return 0;
}
На машине с 4-байтовыми int
и 8-байтовыми указателями эта программа напечатала:
32
4
Если размеры типов у вас отличаются, вы можете получить другой результат.
Небольшой трюк: мы можем определить длину фиксированного массива, разделив размер всего массива на размер элемента массива:
#include <iostream>
int main()
{
int array[]{ 1, 1, 2, 3, 5, 8, 13, 21 };
std::cout << "The array has: " << sizeof(array) / sizeof(array[0]) << " elements\n";
return 0;
}
Это напечатает:
The array has: 8 elements
Как это работает? Во-первых, обратите внимание, что размер всего массива равен длине массива, умноженной на размер элемента. Проще говоря: размер массива = длина массива * размер элемента.
Используя математику, мы можем изменить это уравнение: длина массива = размер массива / размер элемента. sizeof(array)
– это размер массива, а sizeof(array[0])
– это размер элемента, поэтому наше уравнение принимает вид длина массива = sizeof(array)
/ sizeof(array[0])
. Обычно в качестве элемента массива мы используем нулевой элемент, поскольку это единственный гарантированно существующий элемент независимо от длины массива.
Обратите внимание, что это будет работать только в том случае, если массив является массивом фиксированной длины, и вы выполняете этот трюк в той же функции, в которой массив объявлен (о том, почему это ограничение существует, мы поговорим подробнее в следующем уроке этой главы).
Когда sizeof
используется с массивом, который был передан функции, он не вызывает ошибок, как std::size()
. Вместо этого он возвращает размер указателя.
#include <iostream>
void printSize(int array[])
{
std::cout << sizeof(array) / sizeof(array[0]) << '\n';
}
int main()
{
int array[]{ 1, 1, 2, 3, 5, 8, 13, 21 };
std::cout << sizeof(array) / sizeof(array[0]) << '\n';
printSize(array);
return 0;
}
Снова предполагая размер указателя 8 байт и размер int
4 байта, этот код напечатает:
8
2
Примечание автора
Правильно настроенный компилятор должен вывести предупреждение, если вы попытаетесь использовать sizeof()
для массива, переданного функции.
Расчет в main()
был правильным, но sizeof()
в printSize()
вернул 8 (это размер указателя), а 8, деленное на 4, равно 2.
По этой причине будьте осторожны при использовании sizeof()
для массивов!
Примечание. В общем случае термины «размер массива» и «длина массива» чаще всего используются для обозначения длины массива (размер массива в большинстве случаев бесполезен, за исключением показанного выше трюка).
Индексирование массива вне допустимого диапазона
Помните, что массив длины N имеет элементы от 0 до N-1. Итак, что произойдет, если вы попытаетесь получить доступ к элементу массива с индексом за пределами этого диапазона?
Рассмотрим следующую программу:
int main()
{
int prime[5]{}; // храним 5 простых чисел
prime[5] = 13;
return 0;
}
В этой программе наш массив имеет длину 5, но мы пытаемся записать простое число в шестой элемент (индекс 5).
C++ не выполняет никаких проверок, чтобы убедиться, что ваши индексы допустимы для длины вашего массива. Таким образом, в приведенном выше примере значение 13 будет вставлено в память там, где существовал бы шестой элемент. Когда это произойдет, вы получите неопределенное поведение – например, это может перезаписать значение другой переменной или вызвать сбой вашей программы.
Хотя это случается реже, но C++ также позволяет использовать отрицательный индекс с такими же нежелательными результатами.
Правило
При использовании массивов убедитесь, что ваши индексы допустимы для диапазона вашего массива!
Небольшой тест
Вопрос 1
Объявите массив для хранения максимальной температуры (с точностью до десятых долей градуса) для каждого дня в году (предположим, в году 365 дней). Инициализируйте массив значением 0.0 для каждого дня.
Ответ
double temperature[365] { };
Вопрос 2
Создайте перечисление с именами следующих животных: курица (chicken), собака (dog), кошка (cat), слон (elephant), утка (duck) и змея (snake). Поместите перечисление в пространство имен. Определите массив с элементом для каждого из этих животных и используйте список инициализаторов, чтобы инициализировать каждый элемент для хранения количества ног, которое есть у животного.
Напишите функцию main
, которая печатает количество ног слона, используя перечислитель.
Ответ
#include <iostream> namespace Animals { enum Animals // Имя этого перечисления можно не указывать, так как оно нигде не используется { chicken, dog, cat, elephant, duck, snake, max_animals }; } int main() { int legs[Animals::max_animals]{ 2, 4, 4, 4, 2, 0 }; std::cout << "An elephant has " << legs[Animals::elephant] << " legs.\n"; return 0; }