C++

Масиви структур і вкладені структури

Масиви структур як заміна паралельним масивам, пошук і сортування колекцій, вкладені struct, динамічні масиви структур та само-референтна структура як основа зв

Масиви структур і вкладені структури

Повернення до початкової проблеми

У першій статті цієї серії ми сформулювали проблему: три паралельних масиви для одного логічного поняття — хиткий, небезпечний дизайн:

// Антипатерн: три незалежних масиви для одного поняття
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 елементів одразу ініціалізовані коректно. Якщо ні — числові поля містять невизначені значення.

Ініціалізація списком

Найчистіший спосіб — агрегатний ініціалізатор для масиву, де кожен елемент сам ініціалізується агрегатно:

ClassRoom.cpp
#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;
}
./ClassRoom
$ ./ClassRoom
Ім'я Вік GPA
--------------------------------
Олена Коваль 20 3.90
Максим Дяченко 22 3.40
Тетяна Бондар 21 3.75
Дмитро Мельник 19 2.80
Аліна Петренко 20 3.60
Execution finished with exit code 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& — ітеруємося без копій.


Пошук у масиві структур

Лінійний пошук за полем

Search.cpp
#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;
}
./Search
$ ./Search
Знайдено: Тетяна Бондар, GPA: 3.75
Кращий: Олена Коваль (GPA: 3.90)
Execution finished with exit code 0.

findByName повертає const Student* — вказівник на оригінальний елемент масиву, а не копію. Перед використанням результату — обов'язкова перевірка на nullptr.


Сортування масиву структур

Сортування масиву структур демонструє ключову перевагу агрегування: при заміщенні елементів усі поля рухаються разом — жодна «координата» не загубиться:

SortStudents.cpp
#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;
}
./SortStudents
$ ./SortStudents
До сортування:
1. Олена Коваль — GPA: 3.9
2. Максим Дяченко — GPA: 3.4
3. Тетяна Бондар — GPA: 3.75
4. Дмитро Мельник — GPA: 2.8
5. Аліна Петренко — GPA: 3.6
Після сортування (за GPA, за спаданням):
1. Олена Коваль — GPA: 3.9
2. Тетяна Бондар — GPA: 3.75
3. Аліна Петренко — GPA: 3.6
4. Максим Дяченко — GPA: 3.4
5. Дмитро Мельник — GPA: 2.8
Execution finished with exit code 0.

Рядки 26–29: обмін двох структур через тимчасову змінну Student temp. Компілятор копіює всі поля цілком — name, age, gpa — разом. Ніяких паралельних масивів, що потребують синхронного обміну.

У C++ стандартна бібліотека надає std::swap з <algorithm>, яка робить саме це — обмінює два об'єкти будь-якого типу. std::swap(arr[i], arr[maxIdx]) ідентичне трьом рядкам вище, але лаконічніше.

Вкладені структури

Мотивація

Структура може містити поля, що самі є структурами. Це природно відображає ієрархічну природу реальних сутностей: студент має адресу, адреса має місто і вулицю.

NestedStruct.cpp
#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;
}
./NestedStruct
$ ./NestedStruct
Олена Коваль, 20 р.
Адреса: Київ, вул. Хрещатик, 22
Після переїзду:
Олена Коваль, 20 р.
Адреса: Львів, вул. Проспект Свободи, 1
Execution finished with exit code 0.

Доступ до полів вкладеної структури — через ланцюжок операторів крапка: person.homeAddress.city. Кожна крапка «занурює» на один рівень вглиб ієрархії. При передачі person у функцію за const Person& — вся вкладена структура Address також недоступна для запису.


Само-референтна структура і зв'язаний список

Що таке само-референтна структура

Структура може містити вказівник на саму себе — це само-референтна структура, що є атомарним будівельним блоком зв'язаних списків, дерев і графів:

struct Node
{
    int   value;  // корисні дані
    Node* next;   // вказівник на наступний вузол того ж типу
};

Поле next — це Node* (вказівник), а не Node як значення. Це принципово: вкласти Node у Node неможливо — це призвело б до нескінченно великого типу. Але Node* — це адреса, розмір якої завжди відомий (8 байт на 64-бітній системі).

Будуємо однозв'язний список

LinkedList.cpp
#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;
}
./LinkedList
$ ./LinkedList
Список: 40 -> 30 -> 20 -> 10 -> nullptr
Вузлів: 4
Execution finished with exit code 0.
Пропустити freeList — означає витік пам'яті: вузли, виділені через new, залишаться у heap після завершення програми. У реальних системах витоки пам'яті призводять до вичерпання ресурсів.

Практика

Рівень 1 — Базовий

Рівень 2 — Алгоритми


Резюме

🗂️ Масиви структур

  • Один масив замість трьох паралельних — поля завжди разом
  • const T& у range-for — ітерація без копіювання
  • Обмін елементів через temp або std::swap — атомарно для всіх полів

🔍 Алгоритми на колекціях

  • Пошук: const T* — вказівник або nullptr
  • Сортування: порівняння поля структури через <, ==, >
  • Можна сортувати за різними полями — різні функції-компаратори

🪆 Вкладені структури

  • Поле структури = інша структура: person.homeAddress.city
  • const T& «заморожує» всю ієрархію вкладених полів
  • Ієрархія відображає природну структуру реальних сутностей

🔗 Само-референтна struct

  • Node* next — основа зв'язаних списків і дерев
  • Вузли живуть у heap через new, пам'ять звільняється через delete
  • Витік пам'яті = відсутній freeList наприкінці
У фінальній статті серії «Патерни struct та межі застосування» ми розглянемо практичні архітектурні патерни: Record, Value Object, Result type, Named Parameters — і завершимо чесним розбором того, чому методи у struct є семантичною помилкою та що про це думає сам Бйорн Страуструп.
Copyright © 2026