Масиви структур і вкладені структури
Масиви структур і вкладені структури
Повернення до початкової проблеми
У першій статті цієї серії ми сформулювали проблему: три паралельних масиви для одного логічного поняття — хиткий, небезпечний дизайн:
// Антипатерн: три незалежних масиви для одного поняття
const int SIZE = 30;
std::string names[SIZE];
int ages[SIZE];
double gpas[SIZE];
Перша стаття навчила нас створювати структуру Student. Але ми ще не повністю замінили паралельні масиви — ми лише могли оголошувати окремі змінні Student s1, Student s2. Тепер час завершити трансформацію:
// Правильний дизайн: один масив, один концепт
Student students[SIZE];
Один масив — і всі три атрибути кожного студента завжди рухаються разом. Відсортувати масив — name, age і gpa залишаться відповідати одному студенту. Передати у функцію — один параметр замість трьох.
Масив структур: синтаксис і ініціалізація
Оголошення
Масив структур оголошується так само, як масив будь-якого іншого типу:
Student students[30]; // масив із 30 елементів типу Student
Student group[3] = { ... }; // масив із 3 елементів з ініціалізатором
Якщо Student має default member initializers — всі 30 елементів одразу ініціалізовані коректно. Якщо ні — числові поля містять невизначені значення.
Ініціалізація списком
Найчистіший спосіб — агрегатний ініціалізатор для масиву, де кожен елемент сам ініціалізується агрегатно:
#include <iostream>
#include <string>
#include <iomanip>
struct Student
{
std::string name;
int age;
double gpa;
};
void printStudent(const Student& s)
{
std::cout << std::left << std::setw(20) << s.name
<< std::right << std::setw(4) << s.age
<< std::setw(8) << std::fixed << std::setprecision(2) << s.gpa
<< "\n";
}
int main()
{
const int CLASS_SIZE = 5;
Student students[CLASS_SIZE] =
{
{ "Олена Коваль", 20, 3.90 },
{ "Максим Дяченко", 22, 3.40 },
{ "Тетяна Бондар", 21, 3.75 },
{ "Дмитро Мельник", 19, 2.80 },
{ "Аліна Петренко", 20, 3.60 },
};
std::cout << std::left << std::setw(20) << "Ім'я"
<< std::right << std::setw(4) << "Вік"
<< std::setw(8) << "GPA" << "\n";
std::cout << std::string(32, '-') << "\n";
for (int i = 0; i < CLASS_SIZE; ++i)
printStudent(students[i]);
return 0;
}
Ітерація: for і for-each
Зі звичайним for ми маємо доступ до індексу, що корисно при сортуванні та попарному порівнянні:
for (int i = 0; i < CLASS_SIZE; ++i)
printStudent(students[i]);
Range-based for (C++11) — коли індекс не потрібний:
for (const Student& s : students)
printStudent(s);
Зверніть: const Student& у range-for — це той самий принцип, що й при передачі у функцію. Без const& кожна ітерація скопіювала б студента; з const& — ітеруємося без копій.
Пошук у масиві структур
Лінійний пошук за полем
#include <iostream>
#include <string>
struct Student
{
std::string name;
int age;
double gpa;
};
// Повертає вказівник на знайдений елемент або nullptr
const Student* findByName(const Student* arr, int size, const std::string& name)
{
for (int i = 0; i < size; ++i)
{
if (arr[i].name == name)
return &arr[i];
}
return nullptr;
}
// Повертає індекс студента з максимальним GPA
int indexOfBest(const Student* arr, int size)
{
if (size <= 0) return -1;
int bestIdx = 0;
for (int i = 1; i < size; ++i)
{
if (arr[i].gpa > arr[bestIdx].gpa)
bestIdx = i;
}
return bestIdx;
}
int main()
{
const int SIZE = 5;
Student students[SIZE] =
{
{ "Олена Коваль", 20, 3.90 },
{ "Максим Дяченко", 22, 3.40 },
{ "Тетяна Бондар", 21, 3.75 },
{ "Дмитро Мельник", 19, 2.80 },
{ "Аліна Петренко", 20, 3.60 },
};
// Пошук за ім'ям
const Student* found = findByName(students, SIZE, "Тетяна Бондар");
if (found)
std::cout << "Знайдено: " << found->name << ", GPA: " << found->gpa << "\n";
// Пошук кращого студента
int bestIdx = indexOfBest(students, SIZE);
std::cout << "Кращий: " << students[bestIdx].name
<< " (GPA: " << students[bestIdx].gpa << ")\n";
return 0;
}
findByName повертає const Student* — вказівник на оригінальний елемент масиву, а не копію. Перед використанням результату — обов'язкова перевірка на nullptr.
Сортування масиву структур
Сортування масиву структур демонструє ключову перевагу агрегування: при заміщенні елементів усі поля рухаються разом — жодна «координата» не загубиться:
#include <iostream>
#include <string>
struct Student
{
std::string name;
int age;
double gpa;
};
// Сортування за GPA за спаданням (selection sort)
void sortByGpaDesc(Student* arr, int size)
{
for (int i = 0; i < size - 1; ++i)
{
int maxIdx = i;
for (int j = i + 1; j < size; ++j)
{
if (arr[j].gpa > arr[maxIdx].gpa)
maxIdx = j;
}
if (maxIdx != i)
{
// Міняємо місцями цілі структури — всі поля рухаються разом
Student temp = arr[i];
arr[i] = arr[maxIdx];
arr[maxIdx] = temp;
}
}
}
void printAll(const Student* arr, int size)
{
for (int i = 0; i < size; ++i)
std::cout << (i + 1) << ". " << arr[i].name
<< " — GPA: " << arr[i].gpa << "\n";
}
int main()
{
const int SIZE = 5;
Student students[SIZE] =
{
{ "Олена Коваль", 20, 3.90 },
{ "Максим Дяченко", 22, 3.40 },
{ "Тетяна Бондар", 21, 3.75 },
{ "Дмитро Мельник", 19, 2.80 },
{ "Аліна Петренко", 20, 3.60 },
};
std::cout << "До сортування:\n";
printAll(students, SIZE);
sortByGpaDesc(students, SIZE);
std::cout << "\nПісля сортування (за GPA, за спаданням):\n";
printAll(students, SIZE);
return 0;
}
Рядки 26–29: обмін двох структур через тимчасову змінну Student temp. Компілятор копіює всі поля цілком — name, age, gpa — разом. Ніяких паралельних масивів, що потребують синхронного обміну.
std::swap з <algorithm>, яка робить саме це — обмінює два об'єкти будь-якого типу. std::swap(arr[i], arr[maxIdx]) ідентичне трьом рядкам вище, але лаконічніше.Вкладені структури
Мотивація
Структура може містити поля, що самі є структурами. Це природно відображає ієрархічну природу реальних сутностей: студент має адресу, адреса має місто і вулицю.
#include <iostream>
#include <string>
struct Address
{
std::string city;
std::string street;
int buildingNumber;
};
struct Person
{
std::string name;
int age;
Address homeAddress; // вкладена структура
};
void printPerson(const Person& p)
{
std::cout << p.name << ", " << p.age << " р.\n";
std::cout << " Адреса: " << p.homeAddress.city
<< ", вул. " << p.homeAddress.street
<< ", " << p.homeAddress.buildingNumber << "\n";
}
int main()
{
Person person =
{
"Олена Коваль",
20,
{ "Київ", "Хрещатик", 22 } // ініціалізатор для Address
};
printPerson(person);
// Зміна вкладеного поля через ланцюжок крапок
person.homeAddress.city = "Львів";
person.homeAddress.street = "Проспект Свободи";
person.homeAddress.buildingNumber = 1;
std::cout << "\nПісля переїзду:\n";
printPerson(person);
return 0;
}
Доступ до полів вкладеної структури — через ланцюжок операторів крапка: person.homeAddress.city. Кожна крапка «занурює» на один рівень вглиб ієрархії. При передачі person у функцію за const Person& — вся вкладена структура Address також недоступна для запису.
Само-референтна структура і зв'язаний список
Що таке само-референтна структура
Структура може містити вказівник на саму себе — це само-референтна структура, що є атомарним будівельним блоком зв'язаних списків, дерев і графів:
struct Node
{
int value; // корисні дані
Node* next; // вказівник на наступний вузол того ж типу
};
Поле next — це Node* (вказівник), а не Node як значення. Це принципово: вкласти Node у Node неможливо — це призвело б до нескінченно великого типу. Але Node* — це адреса, розмір якої завжди відомий (8 байт на 64-бітній системі).
Будуємо однозв'язний список
#include <iostream>
struct Node
{
int value;
Node* next;
};
Node* prepend(Node* head, int value)
{
Node* newNode = new Node;
newNode->value = value;
newNode->next = head;
return newNode;
}
void printList(const Node* head)
{
const Node* cur = head;
while (cur != nullptr)
{
std::cout << cur->value;
if (cur->next) std::cout << " -> ";
cur = cur->next;
}
std::cout << " -> nullptr\n";
}
int countNodes(const Node* head)
{
int count = 0;
for (const Node* cur = head; cur != nullptr; cur = cur->next)
++count;
return count;
}
void freeList(Node* head)
{
while (head != nullptr)
{
Node* next = head->next;
delete head;
head = next;
}
}
int main()
{
Node* list = nullptr;
list = prepend(list, 10);
list = prepend(list, 20);
list = prepend(list, 30);
list = prepend(list, 40);
std::cout << "Список: ";
printList(list);
std::cout << "Вузлів: " << countNodes(list) << "\n";
freeList(list);
list = nullptr;
return 0;
}
freeList — означає витік пам'яті: вузли, виділені через new, залишаться у heap після завершення програми. У реальних системах витоки пам'яті призводять до вичерпання ресурсів.Практика
Рівень 1 — Базовий
struct Book { std::string title; std::string author; int year; double price; };. Ініціалізуйте масив Book catalog[6]. Напишіть void printCatalog(const Book* arr, int size) з вирівняними стовпцями через <iomanip>. Напишіть const Book* findByAuthor(const Book* arr, int size, const std::string& author) — повертає першу книгу автора або nullptr.struct GPS { double lat; double lon; }; і struct City { std::string name; GPS coords; int population; };. Ініціалізуйте масив City cities[5] (Київ, Львів, Харків, Одеса, Дніпро). Напишіть функцію double approxDistance(const GPS& a, const GPS& b) — наближена відстань у км (евклідова в градусах × 111). Знайдіть місто, найближче до Києва.Рівень 2 — Алгоритми
Дано Student students[6]. Напишіть:
void sortByName(Student* arr, int size)— за алфавітом (std::string <).void sortByAge(Student* arr, int size)— за зростанням віку.
Виведіть масив до і після кожного сортування.
Розширте LinkedList.cpp:
Node* append(Node* head, int value)— додає вузол у кінець.Node* removeFirst(Node* head)— видаляє перший вузол, повертає новий head.bool contains(const Node* head, int value)— чи є значення у списку.
Побудуйте [1, 2, 3, 4, 5] через append, перевірте contains(3), видаліть перший елемент, виведіть результат.
Резюме
🗂️ Масиви структур
- Один масив замість трьох паралельних — поля завжди разом
const T&у range-for — ітерація без копіювання- Обмін елементів через
tempабоstd::swap— атомарно для всіх полів
🔍 Алгоритми на колекціях
- Пошук:
const T*— вказівник абоnullptr - Сортування: порівняння поля структури через
<,==,> - Можна сортувати за різними полями — різні функції-компаратори
🪆 Вкладені структури
- Поле структури = інша структура:
person.homeAddress.city const T&«заморожує» всю ієрархію вкладених полів- Ієрархія відображає природну структуру реальних сутностей
🔗 Само-референтна struct
Node* next— основа зв'язаних списків і дерев- Вузли живуть у heap через
new, пам'ять звільняється черезdelete - Витік пам'яті = відсутній
freeListнаприкінці
struct є семантичною помилкою та що про це думає сам Бйорн Страуструп.Структури у функціях
Три способи передачі структур у функції C++: за значенням, константним посиланням і вказівником. Фабричні функції, функції-трансформації, NRVO та патерни проектування з struct.
Патерни struct та межі застосування
Архітектурні патерни використання struct: Record, Value Object, Result type, Named Parameters. Семантика значення, struct vs class і позиція Страуструпа щодо методів у struct.