Цей урок є продовженням попереднього уроку про масиви в мові С++.
Ініціалізація фіксованих масивів
Елементи масиву обробляються так само, як і звичайні змінні, тому вони не ініціалізуються при створенні. Одним із способів ініціалізації масиву є присвоєння значень кожному елементу по черзі:
|
1 2 3 4 5 6 |
int array[5]; // масив містить 5 простих чисел array[0] = 4; array[1] = 5; array[2] = 8; array[3] = 9; array[4] = 12; |
Однак це не зовсім зручно, особливо коли масив великий.
На щастя, мова C++ підтримує більш зручний спосіб ініціалізації масивів за допомогою списку ініціалізаторів. Наступний приклад еквівалентний вищевказаному прикладу:
|
1 |
int array[5] = { 4, 5, 8, 9, 12 }; // використовується список ініціалізаторів для ініціалізації фіксованого масиву |
Якщо в цьому списку ініціалізаторів більше, ніж може містити масив, то компілятор видасть помилку.
Проте, якщо в списку ініціалізаторів менше, ніж може містити масив, інші елементи будуть проініціалізовані значенням 0. Наприклад:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> int main() { int array[5] = { 5, 7, 9 }; // ініціалізуємо тільки перших 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; } |
Результат виконання програми:
5
7
9
0
0
Отже, щоб проініціалізувати всі елементи масиву значенням 0, потрібно:
|
1 2 |
// Ініціалізуємо всі елементи масиву значенням 0 int array[5] = { }; |
У C++11 замість цього може використовуватися синтаксис uniform-ініціалізації:
|
1 |
int array[5] { 4, 5, 8, 9, 12 }; // використовуємо uniform-ініціалізацію для ініціалізації фіксованого масиву |
Довжина масиву
Якщо ви ініціалізуєте фіксований масив за допомогою списку ініціалізаторів, то компілятор може визначити довжину масиву замість вас, і вам вже не потрібно буде її оголошувати.
Наступних два рядки коду виконують одне і те ж:
|
1 2 |
int array[5] = { 0, 1, 2, 3, 4 }; // явно вказуємо довжину масиву int array[] = { 0, 1, 2, 3, 4 }; // список ініціалізаторів автоматично визначить довжину масиву |
Це не тільки заощадить час, але також вам не доведеться оновлювати довжину масиву, якщо ви захочете додати або видалити елементи пізніше.
Масиви і перерахування
Одна з основних проблем з документацією в масивах полягає в тому, що цілочисельні індекси не надають ніякої інформації програмісту про їх значення. Розглянемо клас з 5 учнів:
|
1 2 3 |
const int numberOfStudents(5); int testScores[numberOfStudents]; testScores[3] = 65; |
Що представлено елементом testScores[3]? Незрозуміло!
Це можна вирішити, використовуючи перерахування, в якому енумератори зіставляються з кожним можливим індексом масиву:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
enum StudentNames { SMITH, // 0 ANDREW, // 1 IVAN, // 2 JOHN, // 3 ANTON, // 4 MAX_STUDENTS // 5 }; int main() { int testScores[MAX_STUDENTS]; // всього 5 студентів testScores[JOHN] = 65; return 0; } |
Тепер вже зрозуміло, що являє собою кожний з елементів масиву. Зверніть увагу, що був доданий додатковий енумератор з ім’ям MAX_STUDENTS. Він використовується під час оголошення масиву для гарантії того, що масив має коректну довжину (розмір повинен бути на одиницю більше найбільшого індексу). Це корисно як в цілях документації, так і в тому, що масив автоматично зміниться, якщо додати ще один енумератор:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
enum StudentNames { SMITH, // 0 ANDREW, // 1 IVAN, // 2 JOHN, // 3 ANTON, // 4 MISHA, // 5 MAX_STUDENTS // 6 }; int main() { int testScores[MAX_STUDENTS]; // всього 6 студентів testScores[JOHN] = 65; // все працює return 0; } |
Зверніть увагу, цей трюк працює тільки в тому випадку, якщо ви не змінюєте значення енумераторів вручну!
Масиви і класи enum
Класи enum не мають неявної конвертації в цілочисельний тип, тому, якщо ви спробуєте зробити наступне:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
enum class StudentNames { SMITH, // 0 ANDREW, // 1 IVAN, // 2 JOHN, // 3 ANTON, // 4 MISHA, // 5 MAX_STUDENTS // 6 }; int main() { int testScores[StudentNames::MAX_STUDENTS]; // всього 6 студентів testScores[StudentNames::JOHN] = 65; } |
То отримаєте помилку від компілятора. Це можна вирішити, використовуючи оператор static_cast для конвертації енумератора в ціле число:
|
1 2 3 4 5 |
int main() { int testScores[static_cast<int>(StudentNames::MAX_STUDENTS)]; // всього 6 студентів testScores[static_cast<int>(StudentNames::JOHN)] = 65; } |
Проте це також не дуже зручно, тому краще використовувати стандартне перерахування всередині простору імен:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
namespace StudentNames { enum StudentNames { SMITH, // 0 ANDREW, // 1 IVAN, // 2 JOHN, // 3 ANTON, // 4 MISHA, // 5 MAX_STUDENTS // 6 }; } int main() { int testScores[StudentNames::MAX_STUDENTS]; // всього 6 студентів testScores[StudentNames::JOHN] = 65; } |
Передача масивів у функції
Хоча передача масиву в функцію на перший погляд виглядає так само, як передача звичайної змінної, але “під капотом” мова C++ обробляє масиви трохи інакше.
Коли звичайна змінна передається по значенню, то мова C++ копіює значення аргументу в параметр функції. Оскільки параметр є копією, то зміна значення параметру не змінює значення вихідного аргументу.
Але оскільки копіювання великих масивів — справа громіздка, то C++ не копіює масив при його передачі в функцію. Замість цього передається фактичний масив. І тут ми отримуємо побічний ефект, що дозволяє функціям безпосередньо змінювати значення елементів масиву!
Наступний приклад добре ілюструє цю концепцію:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#include <iostream> void passValue(int value) // тут value - це копія аргументу { value = 87; // зміна value тут не вплине на фактичну змінну value } void passArray(int array[5]) // тут array - це фактичний масив { array[0] = 10; // зміна array тут змінить вихідний масив array array[1] = 8; array[2] = 6; array[3] = 4; array[4] = 1; } int main() { int value = 1; std::cout << "before passValue: " << value << "\n"; passValue(value); std::cout << "after passValue: " << value << "\n"; int array[5] = { 1, 4, 6, 8, 10 }; std::cout << "before passArray: " << array[0] << " " << array[1] << " " << array[2] << " " << array[3] << " " << array[4] << "\n"; passArray(array); std::cout << "after passArray: " << array[0] << " " << array[1] << " " << array[2] << " " << array[3] << " " << array[4] << "\n"; return 0; } |
Результат виконання програми:
before passValue: 1
after passValue: 1
before passArray: 1 4 6 8 10
after passArray: 10 8 6 4 1
У вищенаведеному прикладі значення змінної value не змінюється в функції main(), тому що параметр value в функції passValue() був лише копією фактичної змінної value. Однак, оскільки масив в параметрі функції passArray() є фактичним масивом, то passArray() безпосередньо змінює значення елементів!
Примітка: Якщо ви не хочете, щоб функція змінювала значення елементів масиву, переданого в неї в якості параметра, то потрібно зробити масив константним:
|
1 2 3 4 5 6 7 8 9 10 |
// Навіть якщо array є фактичним масивом, всередині цієї функції він повинен розглядатися як константний void passArray(const int array[5]) { // Тому кожен з наступних рядків викличе помилку компіляції! array[0] = 11; array[1] = 7; array[2] = 5; array[3] = 3; array[4] = 2; } |
Оператор sizeof і масиви
Оператор sizeof можна використовувати і з масивами: він повертає загальний розмір масиву (довжина масиву помножена на розмір одного елемента) в байтах. Зверніть увагу, що через те, як С++ передає масиви у функції, ця операція не буде коректно виконана з масивами, переданими в функції! Наприклад:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <iostream> void printSize(int array[]) { std::cout << sizeof(array) << '\n'; // виводиться розмір вказівника, а не розмір масиву } int main() { int array[] = { 1, 3, 3, 4, 5, 9, 14, 17 }; std::cout << sizeof(array) << '\n'; // виводиться розмір масиву printSize(array); return 0; } |
Результат виконання програми:
32
4
З цієї причини будьте обережні з використанням оператора sizeof з масивами!
Визначення довжини фіксованого масиву
Щоб визначити довжину фіксованого масиву, поділіть розмір всього масиву на розмір одного елемента масиву:
|
1 2 3 4 5 6 7 8 9 |
#include <iostream> int main() { int array[] = { 1, 3, 3, 4, 5, 9, 14, 17 }; 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]). Зазвичай використовується нульовий елемент в якості елемента масиву в рівнянні, оскільки тільки він є єдиним елементом, який гарантовано існує в масиві, незалежно від його довжини.
Це працює тільки якщо масив фіксованої довжини, і ви виконуєте цю операцію в тій же функції, в якій оголошено масив.
Примітка: На наступних уроках ми будемо використовувати термін «довжина» для позначення загальної кількості елементів в масиві, і термін «розмір», коли мова йтиме про байти.
Індексування поза діапазоном масиву
Пам’ятайте, що масив довжиною N містить елементи від 0 до N-1. Отже, що станеться, якщо ми спробуємо отримати доступ до індексу масиву поза межами цього діапазону? Розглянемо наступну програму:
|
1 2 3 4 5 6 7 |
int main() { int array[5]; // масив містить 5 простих чисел array[5] = 14; return 0; } |
Тут наш масив має довжину 5, але ми намагаємося записати значення в 6-й елемент (індекс 5).
Мова C++ не виконуватиме тут ніяких перевірок коректності вашого індексу. Таким чином, у вищенаведеному прикладі значення 14 буде поміщено в комірку пам’яті, де знаходився б 6-й елемент (якби він взагалі існував би). Але як ви вже здогадалися, це матиме свої наслідки. Наприклад, відбудеться перезапис значення іншої змінної або взагалі збій програми.
Хоча це відбувається рідше, але мова C++ також дозволяє використовувати від’ємний індекс, що теж може призвести до небажаних результатів.
Правило: При використанні масивів переконайтеся, що ваші індекси коректні і відповідають діапазону вашого масиву.
Тест
Завдання №1
Оголосіть масив для зберігання температури (дробове число) кожного дня в році (всього 365 днів). Ініціалізуйте масив значенням 0.0 для кожного дня.
Відповідь №1
Примітка: Якщо розмір не є обмеженням, то замість типу float краще використовувати тип double.
|
1 |
double temperature[365] = { 0.0 }; |
Завдання №2
Створіть перерахування з наступними енумераторами: chicken, lion, giraffe, elephant, duck і snake. Помістіть перерахування в простір імен. Оголосіть масив, де елементами будуть ці енумератори і, використовуючи список ініціалізаторів, ініціалізуйте кожен елемент відповідною кількістю лап певної тварини. У функції main() виведіть кількість ніг у слона, використовуючи енумератор.
Відповідь №2
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream> namespace Animals { enum Animals { CHICKEN, LION, GIRAFFE, 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; } |

Так можна ініціалізувати лише масив цілих чисел?
А так можна?:
або
Чи лише як у завданні 1:
Коментар до завдання №2
Масив можна оголосити і проініціалізувати за межами функції main(). Як я і зробив. Все працює.
Чи матиме в таких випадках таке розміщення оголошення і ініціалізація елементів масиву якийсь негативний вплив на код ? Дякую.
Ти оголосив глобальну змінну, і краще її не використовувати так. Якщо забув що таке глобальна змінна зайди на урок 52 та 53
Поки масив виглядає як річ, яка може спростити щось, але при цьому може натворити купа проблем, якщо не бути обережним