CodeGym /Kursy /C++ SELF /Kolekcja modeli: std::vector<Model>

Kolekcja modeli: std::vector<Model>

C++ SELF
Poziom 19, Lekcja 3
Dostępny

1. CRUD i podstawa danych dla listy zadań

Kiedy po raz pierwszy tworzysz Task t{1, "Buy milk", Status::Todo};, wszystko wygląda pięknie… aż do momentu, gdy pojawia się drugie zadanie. Potem trzecie. Potem chcesz „pokaż wszystko”, „znajdź po id”, „oznacz jako zrobione”, „usuń błędne”. I nagle okazuje się, że rozrzucone pojedyncze zmienne to jak próba przechowywania biblioteki w stosie kartek na podłodze: technicznie możliwe, ale potem sam będziesz się pod tym grzebać.

W C++ rolę „półki dla jednakowych obiektów” często pełni std::vector<T>. To dynamiczna tablica: potrafi przechowywać wiele elementów jednego typu (Task), rozszerzać się w razie potrzeby i dawać dostęp do elementów po indeksie. W tym wykładzie weźmiemy nasz Task i zrobimy z niego mały „tracker zadań” w konsoli — bez skomplikowanych parserów, bez plików, bez magii. Po prostu uczciwe funkcje i uczciwy std::vector.

Idea CRUD „na palcach”

Słowo CRUD brzmi jakby zostało wymyślone, żeby przestraszyć początkujących na rozmowach kwalifikacyjnych. W rzeczywistości to po prostu cztery podstawowe działania na danych: create, read, update, delete. Robimy CRUD codziennie w życiu: dodaliśmy kontakt do telefonu (Create), otworzyliśmy listę kontaktów (Read), zmieniliśmy numer (Update), usunęliśmy stary kontakt (Delete). Programy robią to samo — tylko zamiast telefonu mamy std::vector.

Tak to będzie wyglądać w naszym mini‑programie. Świadomie trzymamy wszystko proste i czytelne.

CRUD Działanie użytkownika Co robimy w kodzie Typowy rezultat
Create dodać zadanie
push_back(Task{...})
zadanie pojawia się na liście
Read pokaż / znajdź
pętla po vector
widzimy zadania / dostajemy indeks
Update zmienić pola
tasks[idx].status = ...
zadanie zaktualizowane
Delete usunąć zadanie
erase(begin()+idx)
zadanie znika, pozostałe się przesuwają

Zwróć uwagę: CRUD — to nie „funkcja bazy danych”, to sposób myślenia o dowolnych kolekcjach. Nawet jeśli nie masz serwera, a prostą listę w pamięci — CRUD nadal dobrze opisuje to, co się dzieje.

Task + std::vector<Task> jako „szkielet” aplikacji

Zanim napiszemy operacje, trzeba uzgodnić kształt danych. Już wprowadziliśmy struct i enum class, więc teraz po prostu złożymy podstawę: typ Task i kontener std::vector<Task> tasks;. To jak przygotować stół przed gotowaniem: jeśli nie ma noża, sałatka będzie… kreatywna, ale krótko.

#include <string>
#include <vector>

enum class Status { Todo, InProgress, Done };

struct Task {
    int id;
    std::string title;
    Status status = Status::Todo;
};

Z perspektywy C++ to zwykłe deklaracje. Ważniejsze jest to: teraz typ Task — jednostka znaczenia, a std::vector<Task> — „pudełko”, w którym takich jednostek może być dowolnie dużo.

3. Create: dodajemy zadania do vector

Naiwny start wygląda tak: gdzieś w main() piszemy tasks.push_back(Task{...}); i cieszymy się. Ale gdy dodawanie zaczyna wymagać logiki (np. „id musi być unikalne”, „title nie może być puste”, „zwrócić sukces/błąd”), main() zamienia się w bałagan. Dlatego warto od razu zrobić funkcję AddTask(...). Nawet jeśli na początku to jedna linia — to „punkt rozszerzenia”, gdzie potem dodamy walidacje.

Zaczniemy od prostego: funkcja przyjmuje vector przez referencję, bo chcemy modyfikować kolekcję, i zadanie przez const&, aby nie kopiować stringa niepotrzebnie.

#include <vector>

bool AddTask(std::vector<Task>& tasks, const Task& t) {
    tasks.push_back(t);
    return true;
}

Teraz bool wygląda „zbyt poważnie” dla takiej prostej funkcji, ale to zabezpieczenie na przyszłość: niedługo AddTask może zwracać false, jeśli zadanie jest niepoprawne lub id już zajęte.

Aby utworzyć zadanie i dodać je, robisz tak:

#include <string>
#include <vector>

void DemoCreate(std::vector<Task>& tasks) {
    AddTask(tasks, Task{1, "Buy milk", Status::Todo});
    AddTask(tasks, Task{2, "Pay rent", Status::InProgress});
}

Ważne jest, by poczuć myśl: dodajemy cały model, a nie „trzy osobne wartości”. To znacząco zmniejsza ryzyko pomylenia title i id, zwłaszcza gdy pól przybędzie.

4. Read: wypis listy i wyszukiwanie po id

Odczyt zwykle zaczyna się od dwóch codziennych rzeczy: pokazać wszystkie elementy i znaleźć jeden element. Pokaż wszystkie — to po prostu przejść po vector pętlą. Znaleźć jeden — to też iteracja, ale zatrzymujemy się, gdy trafimy na potrzebne id. Tak, to wyszukiwanie liniowe i na dużych danych nie jest najszybsze. Ale na naszym poziomie ważniejsze jest, że jest przezroczyste, zrozumiałe i łatwe do debugowania.

Wypis listy „tymczasowo i prosto”

Jeszcze nie robimy „jednolitego ładnego formatu”, więc wypisujemy maksymalnie bezpośrednio: id, title i status jedną literą. Dla statusu zrobimy małą funkcję, żeby std::cout nie cierpiał, bo enum class sam nie wypisuje się do strumienia.

#include <iostream>

char StatusChar(Status s) {
    switch (s) {
        case Status::Todo:       return 'T';
        case Status::InProgress: return 'P';
        case Status::Done:       return 'D';
    }
    return '?';
}

I teraz wypis jednej task:

#include <iostream>

void PrintTask(const Task& t) {
    std::cout << t.id << " [" << StatusChar(t.status) << "] " << t.title << '\n';
    // przykład: 2 [P] Pay rent
}

A wypis całej listy — zwykły range‑for:

#include <vector>

void PrintAll(const std::vector<Task>& tasks) {
    for (const Task& t : tasks) {
        PrintTask(t);
    }
}

Wyszukiwanie: zwracamy indeks lub -1

Teraz kluczowy fragment: wyszukiwanie po id. Mamy subtelność: vector::size() zwraca size_t, a my chcemy „-1 jako nie znalezione”. size_t jest bez znaku, i -1 tam zamienia się w bardzo dużą liczbę.

Dlatego wygodny „edukacyjny” kontrakt jest taki: funkcja wyszukująca zwraca int, a wewnątrz pętli używa size_t, i przy znalezieniu robi static_cast<int>(i).

#include <cstddef>
#include <vector>

int FindIndexById(const std::vector<Task>& tasks, int id) {
    for (std::size_t i = 0; i < tasks.size(); ++i) {
        if (tasks[i].id == id) {
            return static_cast<int>(i);
        }
    }
    return -1;
}

To jeden z najbardziej użytecznych wzorców na początku nauki C++: wewnątrz bezpiecznie i poprawnie typowo, na zewnątrz wygodny kontrakt.

5. Update: zmieniamy status i nazwę zadania

Modyfikacja zwykle wygląda podobnie: znajdujemy element, upewniamy się, że go znaleźliśmy, i dopiero potem zmieniamy potrzebne pola. Początkujący często robią odwrotnie: „najpierw zmienię, potem sprawdzę” — i program pada tam, gdzie nie powinien. Dlatego właściwa kolejność to nie „styl”, lecz podstawowa higiena.

Zrobimy dwa scenariusze aktualizacji: oznaczyć zadanie jako wykonane i zmienić nazwę zadania. Oba będą zwracać bool, żeby wywołujący mógł uczciwie powiedzieć użytkownikowi „nie znaleziono”.

Oznaczyć zadanie jako wykonane

#include <cstddef>
#include <vector>

bool MarkDone(std::vector<Task>& tasks, int id) {
    int idx = FindIndexById(tasks, id);
    if (idx == -1) return false;

    tasks[static_cast<std::size_t>(idx)].status = Status::Done;
    return true;
}

Tu jest mała, ale ważna myśl: nie odwołujemy się do tasks[idx] bezpośrednio, bo idx jest int, a indeksowanie oczekuje typu bez znaku. Wyraźnie rzutujemy idx na size_t po sprawdzeniu, że idx nie jest -1. To przypadek, gdy static_cast nie jest „czczą pedanterią”, lecz sygnałem dla kompilatora i czytelnika, że wszystko sprawdzono.

Można przedstawić logikę MarkDone tak:

flowchart TD
    A["Wywołano MarkDone(tasks, id)"] --> B["idx = FindIndexById(...)"]
    B --> C{idx == -1?}
    C -- tak --> D[return false]
    C -- nie --> E["tasks[idx].status = Done"]
    E --> F[return true]

Zmiana nazwy zadania

Zmiana nazwy jest podobna, ale pojawia się walidacja wejścia: nowy tytuł nie powinien być pusty.

#include <cstddef>
#include <string>
#include <vector>

bool RenameTask(std::vector<Task>& tasks, int id, const std::string& newTitle) {
    if (newTitle.empty()) return false;

    int idx = FindIndexById(tasks, id);
    if (idx == -1) return false;

    tasks[static_cast<std::size_t>(idx)].title = newTitle;
    return true;
}

Zwróć uwagę na „psychologię” takiego kodu: czyta się go z góry na dół jak zestaw strażników na wejściu. Jeśli coś jest nie tak — wychodzimy wczesnym return. I tylko jeśli wszystko w porządku — wykonujemy zmianę.

6. Delete: usuwanie przez erase i przesuwanie elementów

Usuwanie w std::vector to jedna z częstszych przyczyn „dziwnych bugów” u początkujących. Powód prosty: vector przechowuje elementy gęsto. Jeśli usuniesz jeden element, wszystkie elementy po prawej „przesuwają się w lewo”, żeby nie zostawić dziury. To oznacza, że indeksy po usunięciu zmieniają się, i jeśli gdzieś zapamiętałeś indeks lub referencję do elementu, może stać się nieprawidłowa.

Zrobimy usuwanie „po id” w dwóch krokach: znajdziemy indeks, potem usuniemy po indeksie przez erase.

#include <cstddef>
#include <vector>

bool RemoveById(std::vector<Task>& tasks, int id) {
    int idx = FindIndexById(tasks, id);
    if (idx == -1) return false;

    tasks.erase(tasks.begin() + static_cast<std::size_t>(idx));
    return true;
}

Ważne zrozumieć mechanikę erase: przyjmuje on iterator, a nie indeks. Ale póki nie zagłębiamy się w iteratory, wystarczy pamiętać praktyczną formułę: „iterator do i‑tego elementu” to begin() + i. Dla vector to działa, bo jego iteratory są „dodawalne”, jak indeksy.

Jeszcze jedna praktyczna uwaga: jeśli wypisujesz listę z numerami wierszy (0, 1, 2…), a potem usuwasz element, numery się zmienią. Dlatego warto rozróżniać dwa pojęcia: id — część danych, a idx — pozycja w vector. Pozycja jest zmienna, id zwykle powinno być stabilne.

7. Mini‑CLI: łączenie CRUD w jeden cykl

Teraz już umiemy robić wszystkie cztery operacje CRUD jako osobne funkcje. Zostało powiązać je w małą aplikację konsolową, żeby można było „poczuć ręką”: dodać kilka zadań, wypisać listę, oznaczyć jako wykonane, usunąć. Zrobimy maksymalnie prosty interfejs: menu cyframi i wprowadzanie id, a tytuł czytamy przez std::getline, żeby spacje nie psuły wejścia.

Na początek — druk menu:

#include <iostream>

void PrintMenu() {
    std::cout << "1) Add\n2) List\n3) Done\n4) Rename\n5) Remove\n0) Exit\n";
}

Teraz — jedna ważna pomocnicza funkcja: bezpieczne wczytanie całego wiersza. Znamy już fakt, że po std::cin >> number w buforze zostaje '\n', więc przed getline często trzeba „zjeść pozostałość wiersza”. W idealnym świecie robi się to ładniej, ale teraz zrobimy prosty i zrozumiały wariant.

#include <iostream>
#include <string>

std::string ReadLine() {
    std::string s;
    std::getline(std::cin, s);
    return s;
}

Szkielet main(): jest dłuższy niż 10 linii, więc pokażemy go fragmentami. Zaczynamy od inicjalizacji i pętli:

#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<Task> tasks;
    int nextId = 1;

    while (true) {
        PrintMenu();
        int cmd = -1;
        std::cin >> cmd;
        if (cmd == 0) break;

        std::cin.ignore(10000, '\n'); // zjadamy '\n' po liczbie
        // dalej obsługa poleceń...
    }
}

Teraz dodawanie zadania — bezpośrednio w pętli. Czytamy tytuł linią, tworzymy Task i dodajemy:

if (cmd == 1) {
    std::cout << "Title: ";
    std::string title = ReadLine();

    Task t{nextId, title, Status::Todo};
    AddTask(tasks, t);
    ++nextId;
}

Przegląd listy — po prostu wypis:

if (cmd == 2) {
    PrintAll(tasks);
}

Oznacz jako wykonane:

if (cmd == 3) {
    std::cout << "Id: ";
    int id = 0;
    std::cin >> id;
    std::cin.ignore(10000, '\n');

    if (!MarkDone(tasks, id)) {
        std::cout << "Task not found\n";
    }
}

Usuwanie:

if (cmd == 5) {
    std::cout << "Id: ";
    int id = 0;
    std::cin >> id;
    std::cin.ignore(10000, '\n');

    if (!RemoveById(tasks, id)) {
        std::cout << "Task not found\n";
    }
}

Tak, to póki co „naiwny konsolowy UI”, ale robi najważniejsze: sprawia, że funkcje CRUD działają razem. A kiedy funkcje są oddzielone, niemal fizycznie czujesz, jak łatwiej czytać kod: main() staje się dyspozytorem, a nie „garnkiem z wszystkim”.

8. Typowe błędy przy pracy z std::vector<Model> i CRUD

Błąd nr 1: mylić id z indeksem (idx).
To jeden z najbardziej podstępnych błędów, bo najpierw wszystko „jakby działa”. Dodałeś zadania, id zgadza się z pozycją (1‑sze zadanie mniej więcej na pozycji 0, 2‑gie — na pozycji 1), i wydaje się: „po co szukać, użyję tasks[id]”. Potem usuwasz element przez erase, elementy się przesuwają, i tasks[id] nagle wskazuje nie na to zadanie. Warto wcześnie przyzwyczaić się do idei: id żyje wewnątrz modelu, a indeks to tymczasowa współrzędna w kontenerze.

Błąd nr 2: nie sprawdzać, czy idx == -1, i od razu odwoływać się do tasks[idx].
Taki kod zwykle pada „niespodziewanie”, czyli dokładnie wtedy, gdy użytkownik wpisał nieistniejące id. Szczególnie frustrujące, że program nie zawodzi w momencie wpisu, lecz przy dostępie do pamięci — i początkującemu wydaje się, że „zepsuł się vector”. W rzeczywistości vector nie ma z tym nic wspólnego: po prostu poprosiłeś go o otwarcie szuflady pod ujemnym numerem.

Błąd nr 3: zwracać z wyszukiwania size_t i próbować użyć -1 jako „nie znalezione”.
To klasyczna pułapka signed/unsigned. -1 zapisany w size_t staje się ogromną dodatnią liczbą. Dalej robisz tasks[idx] — i lecicie w odczyt pamięci „gdzieś w kosmos”. Jeśli używacie kontraktu „-1 jako nie znalezione”, zwracajcie int. Jeśli chcecie zwracać size_t, trzeba zmienić kontrakt. Na przykład zwracać tasks.size() jako „nie znalezione”. Ale w ramach tego dnia trzymamy prosty wariant z int.

Błąd nr 4: usuwać element w pętli i dalej iterować po indeksach jakby nic się nie stało.
Nawet jeśli nie używasz iteratorów, logika prosta: po erase wszystkie elementy po prawej przesunęły się. Jeśli usunąłeś tasks[i] i potem zrobiłeś ++i, pominąłeś następny element (został przesunięty na miejsce i). W tej lekcji usuwamy „po jednym elemencie po id”, więc problem nie wychodzi, ale warto pamiętać: erase zmienia strukturę vector, więc pętla wymaga ostrożności.

Błąd nr 5: rozmazać logikę CRUD po main() i stracić kontrolę.
Dopóki program jest mały, wydaje się, że funkcje są zbędne. Ale gdy pojawi się drugi scenariusz aktualizacji lub trzecia walidacja, main() zamienia się w płótno z zagnieżdżonymi if, i każda zmiana staje się ryzykowna. Wyciągnięcie operacji CRUD do funkcji to nie „architektura dla architektury”, lecz zwykła oszczędność nerwów: jedna operacja — jedno miejsce opisu.

1
Zadanie
C++ SELF, poziom 19, lekcja 3
Niedostępne
Trzy notatki
Trzy notatki
1
Zadanie
C++ SELF, poziom 19, lekcja 3
Niedostępne
Wyszukiwanie użytkownika
Wyszukiwanie użytkownika
1
Zadanie
C++ SELF, poziom 19, lekcja 3
Niedostępne
Zamknięcie zgłoszenia
Zamknięcie zgłoszenia
1
Zadanie
C++ SELF, poziom 19, lekcja 3
Niedostępne
Katalog książek
Katalog książek
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION