Przejdź do głównej zawartości

Struktury

W tej lekcji nauczysz się tworzenia typów danych, złożonych z wielu mniejszych elementów, czyli tego co w C++ nazywamy strukturami.

Motywacja

Prezentacja przeciwnika - Goblin
Obrazek goblina wykonany przez LuizMelo

Jeśli np. tworząc grę 🎮, chcemy zawrzeć w swoim programie przeciwników, zwykle będziemy musieli o każdym z nich zapisac kilka informacji.

Zastanów się: jakie dane o wrogach w grze mogą się przydać? Może to być np.:

  • nazwa 👾
  • życie 💚
  • siła 💪

itd...

Korzystając z dotychczas nabytej wiedzy, gdybyśmy chcieli napisać program, który przechowuje te informacje, moglibyśmy zrobić to np. tak:

#include <string>

int main() {
std::string enemy_name = "Goblin";
float enemy_health = 50;
float enemy_strength = 12;
// ...
}

Gdy będziemy chcieli mieć w grze więcej przeciwników, napotkamy na pewnien problem, a właściwie uniedogodnienie:

Jeśli skorzystamy w tym celu z wielu tablic:

std::vector< std::string >	enemy_names;
std::vector< float > enemy_health;
std::vector< float > enemy_strength;

to każdy przeciwnik będzie opisany pod jednakowym indeksem w tych tablicach:

  • enemy_names[ index ] opisuje nazwę
  • enemy_health[ index ] opisuje punkty życia
  • enemy_strength[ index ] opisuje punkty siły
Uwaga

Ten sposób wiąże się z "rozrzuceniem" informacji o pojedynczym przeciwniku, po wielu tablicach.

Dodanie jednego wroga do zbioru, w takim programie wyglądałoby tak:

enemy_names.push_back("Goblin");
enemy_health.push_back(50);
enemy_strength.push_back(10);

Im więcej różnych informacji chcemy o przeciwnikach przechować, tym będzie to bardziej uciążliwe. Na szczęście tutaj z pomocą przychodzą nam struktury.

Tworzenie struktury

Przypomnijmy sobie, jakie dane potrzebujemy przechować:

  • nazwa 👾
  • życie 💚
  • siła 💪

Zaraz dodamy strukturę, dzięki której, będziemy mogli utworzyć obiekt, który zawiera w sobie te 3 rzeczy.

#include <string>

struct Enemy
{
std::string name;
float health;
float strength;
};

int main()
{
// Na razie pusto
}

Powyższy kod wprowadza nową strukturę - Enemy.

Zapamiętaj

Struktura to opis, wzorzec, receptura na to, jak utworzyć obiekt (w tym wypadku wroga).

Żeby utworzyć strukturę, piszemy po słowie kluczowym struct jej nazwę, następnie między nawiasami klamrowymi { } umieszczamy jej zawartość.

Zawartością mogą być np. zmienne składowe.

Średnik!

Zwróć uwagę, na obowiązkowy średnik po nawiasie klamrowym, zamykającym definicję struktury:

struct Enemy
{
std::string name;
float health;
float strength;
};

Obiekty

Popatrz jak stworzyć obiekt, który korzysta ze wzoru Enemy:

int main()
{
Enemy boss;
}

W ten sposób, zawarliśmy te wszystkie 3 pola (name, health i strength) wewnątrz jednej zmiennej boss.

Nazewnictwo

Od teraz będziemy mówili, że boss jest obiektem typu Enemy. To oznacza, że został stworzony według wzoru Enemy.

Dostęp do pól

Tak jak wyżej wspomniałem, boss zawiera w sobie 3 rzeczy (pola) tj. składa się z trzech zmiennych. Żeby dostać się do konkretnej składowej tego obiektu, musimy użyć następującego zapisu:

Ustaw nazwę bossa na 'Ogr'
boss.name = "Ogr";

Używamy kropki ., do odniesienia się do pola obiektu. W ten sam sposób, możemy np. zmodyfikować siłę wroga:

Modyfikowanie pól obiektu
boss.strength	= 50; // Ustawiam siłę na 50

// Boss włącza tryb "furia" - siła zwiększona
// Życie zmniejszone o połowę
boss.strength += 25;
boss.health *= 0.5f;

... lub wyświetlić informacje o nim:

Korzystanie z pól obiektu
#include <iostream>
#include <string>

struct Enemy
{
std::string name;
float health;
float strength;
};

int main()
{
// Tworzę obiekt bossa
Enemy boss;
// Przypisuję temu obiektowi konkretne wartości
boss.name = "Ogr";
boss.health = 250;
boss.strength = 50;

std::cout << boss.name << " posiada "
<< boss.health << " hp i "
<< boss.strength << " siły."
<< std::endl;
}

Przekazywanie do funkcji

Nic nie stoi na przeszkodzie, żeby stworzyć funkcję, która przyjmuje obiekt pewnej struktury jako parametr. Dobrym przykładem bedzie właśnie wyświetlanie informacji o wrogu:

Funkcja wyświetlająca informacje o przeciwniku
void print_enemy_info(Enemy enemy)
{
std::cout << enemy.name << " posiada "
<< enemy.health << " hp i "
<< enemy.strength << " siły."
<< std::endl;
}
Kolejność

print_enemy_info wymaga istnienia typu Enemy przed zdefiniowaniem samej funkcji. Oznacza to, że musimy umieścić funkcję pod utworzeniem struktury (zobacz przykład niżej).

Korzystając w powyższych informacji, utworzymy sobie "grę", która będzie posiadała dwóch przeciwników:

  • zwykły przeciwnik 👹:
    Goblin wojownik, 60 życia, 14 siły

  • boss 💀:
    Ogr, 250 życia, 50 siły

Fragment gry z Ogrem i Goblinem
#include <iostream>
#include <string>

/// Utworzenie struktury
struct Enemy
{
std::string name;
float health;
float strength;
};

/// Funkcja wyświetlająca informacje o przeciwniku
void print_enemy_info(Enemy enemy)
{
std::cout << enemy.name << " posiada "
<< enemy.health << " hp i "
<< enemy.strength << " siły."
<< std::endl;
}

/// Funkcja główna programu
int main()
{
// Tworzę obiekt goblina i bossa
Enemy boss;
Enemy goblin;

// Ustawiam goblinowi odpowiednie wartości
goblin.name = "Goblin wojownik";
goblin.health = 60;
goblin.strength = 14;

// Ustawiam bossowi odpowiednie wartości
boss.name = "Ogr";
boss.health = 250;
boss.strength = 50;

// Wyświetlam informacje o każdym z nich:
print_enemy_info(goblin);
print_enemy_info(boss);
}

Umieszczanie wewnątrz tablicy

Obiekty możemy umieszczać wewnątrz tablic tak samo jak normalne zmienne:

Tablica przeciwników
std::vector< Enemy > enemies;

Poniżej przykład jak dodawać do takiej tablicy:

Wykorzystanie tablicy
// ...

int main()
{
std::vector< Enemy > enemies;

// (opcjonalnie)
// Blok kodu, by ograniczyć widoczność
// zmiennych utworzonych wewnątrz
{
// Tworzę goblina 👉 lokalnie 👈
Enemy goblin;
// Ustawiam goblinowi odpowiednie wartości
goblin.name = "Goblin wojownik";
goblin.health = 60;
goblin.strength = 14;

// Dodaję goblina do tablicy
enemies.push_back( goblin );
}
// 👈 od tego momentu goblin istnieje tylko w tablicy enemies

// Wyświetl wszystkich przeciwników:
for (Enemy enemy : enemies)
print_enemy_info(enemy);
}
Przykład

Po zapoznaniu się z tą lekcją, przejrzyj ten przykładowy program: 👾 Arena walki wraz z jego omówieniem. Zobaczysz tam zastosowanie tablic i struktur w praktyce.

Domyślne wartości pól

Elementom struktury możemy nadać domyślne wartości, przez co nie będziemy musieli ich za każdym razem wypełniać.

Dobrym przykładem użycia domyślnej wartości jest zmienna, która przechowuje całkowitą ilość obrażeń, które zadał przeciwnik. Na początek dla każdego wroga, ta wartość będzie musiała być równa 0.

Wartość zmiennych

Jeśli pozostawisz pole struktury bez domyślnej wartości, np.:

struct Car
{
int number_of_wheels; // ilośc kół samochodu
};

to nie oznacza to, że number_of_wheels na początku otrzyma wartość 0, musisz to zrobić ręcznie!

Żeby przypisać domyślną wartość do pola struktury używamy zwykłej inicjalizacji, znaną z tworzenia zmiennych:

Domyślna wartość dla 'total_damage' ⚔
/// Utworzenie struktury
struct Enemy
{
std::string name;
float health;
float strength;

float total_damage = 0; // ilość obrażeń
};

Teraz gdy stworzymy jakiegoś wroga:

Enemy snake; // np. wąż

to wartość

snake.total_damage

będzie równa 0.

Możesz się o tym przekonać, np. wyświetlając ją:

int main() {
Enemy snake;
snake.name = "Wąż";
// 🟡 Uwaga, nie ustawiam ręcznie wartości total_damage

std::cout << snake.name
<< " zadał łącznie "
<< snake.total_damage
<< " obrażeń";
}
Wynik
Wąż zadał łącznie 0 obrażeń

Potencjalne błędy

Uwaga!

Ta sekcja wymaga rozbudowy. Możesz nam pomóc edytując tą stronę.

Oto lista popularnych błędów związanych z tą lekcją:

Brak średnika po definicji

Tylko dla przypomnienia.

Nieprawidłowa kolejność

Upewnij się, że struktura jest zdefiniowana przed jej pierwszym użyciem.

Przykład błędnego kodu:

🔴 Niepoprawna kolejność
// ❌ Błąd: użycie "Enemy" przed zdefiniowaniem
void print_enemy_info(Enemy enemy)
{
std::cout << enemy.name << " posiada "
<< enemy.health << " hp i "
<< enemy.strength << " siły."
<< std::endl;
}

/// Utworzenie struktury
struct Enemy
{
std::string name;
float health;
float strength;
};
Ciekawostka

Ten problem jest możliwy do rozwiązania w inny, wygodniejszy sposób niż przenoszenie funkcji print_enemy_info pod definicję struktury, jednak o tzw. forward declaration wspomnimy w dalszej części kursu.

Modyfikacja wewnątrz definicji struktury

Zmiennych nie możemy modyfikować wewnątrz definicji struktury. Możliwe jest jedynie przypisanie początkowej wartości:

🔴 Próba modyfikacji w złym miejscu
struct Enemy
{
std::string name;
float health;
float strength;

int total_damage = 0; // OK ✅

// ❌ Błąd: próba modyfikacji w złym miejscu
health = 250;
};

Struktury

W tej lekcji nauczysz się tworzenia typów danych, złożonych z wielu mniejszych elementów, czyli tego co w C++ nazywamy strukturami.

Motywacja

Prezentacja przeciwnika - Goblin
Obrazek goblina wykonany przez LuizMelo

Jeśli np. tworząc grę 🎮, chcemy zawrzeć w swoim programie przeciwników, zwykle będziemy musieli o każdym z nich zapisac kilka informacji.

Zastanów się: jakie dane o wrogach w grze mogą się przydać? Może to być np.:

  • nazwa 👾
  • życie 💚
  • siła 💪

itd...

Korzystając z dotychczas nabytej wiedzy, gdybyśmy chcieli napisać program, który przechowuje te informacje, moglibyśmy zrobić to np. tak:

#include <string>

int main() {
std::string enemy_name = "Goblin";
float enemy_health = 50;
float enemy_strength = 12;
// ...
}

Gdy będziemy chcieli mieć w grze więcej przeciwników, napotkamy na pewnien problem, a właściwie uniedogodnienie:

Jeśli skorzystamy w tym celu z wielu tablic:

std::vector< std::string >	enemy_names;
std::vector< float > enemy_health;
std::vector< float > enemy_strength;

to każdy przeciwnik będzie opisany pod jednakowym indeksem w tych tablicach:

  • enemy_names[ index ] opisuje nazwę
  • enemy_health[ index ] opisuje punkty życia
  • enemy_strength[ index ] opisuje punkty siły
Uwaga

Ten sposób wiąże się z "rozrzuceniem" informacji o pojedynczym przeciwniku, po wielu tablicach.

Dodanie jednego wroga do zbioru, w takim programie wyglądałoby tak:

enemy_names.push_back("Goblin");
enemy_health.push_back(50);
enemy_strength.push_back(10);

Im więcej różnych informacji chcemy o przeciwnikach przechować, tym będzie to bardziej uciążliwe. Na szczęście tutaj z pomocą przychodzą nam struktury.

Tworzenie struktury

Przypomnijmy sobie, jakie dane potrzebujemy przechować:

  • nazwa 👾
  • życie 💚
  • siła 💪

Zaraz dodamy strukturę, dzięki której, będziemy mogli utworzyć obiekt, który zawiera w sobie te 3 rzeczy.

#include <string>

struct Enemy
{
std::string name;
float health;
float strength;
};

int main()
{
// Na razie pusto
}

Powyższy kod wprowadza nową strukturę - Enemy.

Zapamiętaj

Struktura to opis, wzorzec, receptura na to, jak utworzyć obiekt (w tym wypadku wroga).

Żeby utworzyć strukturę, piszemy po słowie kluczowym struct jej nazwę, następnie między nawiasami klamrowymi { } umieszczamy jej zawartość.

Zawartością mogą być np. zmienne składowe.

Średnik!

Zwróć uwagę, na obowiązkowy średnik po nawiasie klamrowym, zamykającym definicję struktury:

struct Enemy
{
std::string name;
float health;
float strength;
};

Obiekty

Popatrz jak stworzyć obiekt, który korzysta ze wzoru Enemy:

int main()
{
Enemy boss;
}

W ten sposób, zawarliśmy te wszystkie 3 pola (name, health i strength) wewnątrz jednej zmiennej boss.

Nazewnictwo

Od teraz będziemy mówili, że boss jest obiektem typu Enemy. To oznacza, że został stworzony według wzoru Enemy.

Dostęp do pól

Tak jak wyżej wspomniałem, boss zawiera w sobie 3 rzeczy (pola) tj. składa się z trzech zmiennych. Żeby dostać się do konkretnej składowej tego obiektu, musimy użyć następującego zapisu:

Ustaw nazwę bossa na 'Ogr'
boss.name = "Ogr";

Używamy kropki ., do odniesienia się do pola obiektu. W ten sam sposób, możemy np. zmodyfikować siłę wroga:

Modyfikowanie pól obiektu
boss.strength	= 50; // Ustawiam siłę na 50

// Boss włącza tryb "furia" - siła zwiększona
// Życie zmniejszone o połowę
boss.strength += 25;
boss.health *= 0.5f;

... lub wyświetlić informacje o nim:

Korzystanie z pól obiektu
#include <iostream>
#include <string>

struct Enemy
{
std::string name;
float health;
float strength;
};

int main()
{
// Tworzę obiekt bossa
Enemy boss;
// Przypisuję temu obiektowi konkretne wartości
boss.name = "Ogr";
boss.health = 250;
boss.strength = 50;

std::cout << boss.name << " posiada "
<< boss.health << " hp i "
<< boss.strength << " siły."
<< std::endl;
}

Przekazywanie do funkcji

Nic nie stoi na przeszkodzie, żeby stworzyć funkcję, która przyjmuje obiekt pewnej struktury jako parametr. Dobrym przykładem bedzie właśnie wyświetlanie informacji o wrogu:

Funkcja wyświetlająca informacje o przeciwniku
void print_enemy_info(Enemy enemy)
{
std::cout << enemy.name << " posiada "
<< enemy.health << " hp i "
<< enemy.strength << " siły."
<< std::endl;
}
Kolejność

print_enemy_info wymaga istnienia typu Enemy przed zdefiniowaniem samej funkcji. Oznacza to, że musimy umieścić funkcję pod utworzeniem struktury (zobacz przykład niżej).

Korzystając w powyższych informacji, utworzymy sobie "grę", która będzie posiadała dwóch przeciwników:

  • zwykły przeciwnik 👹:
    Goblin wojownik, 60 życia, 14 siły

  • boss 💀:
    Ogr, 250 życia, 50 siły

Fragment gry z Ogrem i Goblinem
#include <iostream>
#include <string>

/// Utworzenie struktury
struct Enemy
{
std::string name;
float health;
float strength;
};

/// Funkcja wyświetlająca informacje o przeciwniku
void print_enemy_info(Enemy enemy)
{
std::cout << enemy.name << " posiada "
<< enemy.health << " hp i "
<< enemy.strength << " siły."
<< std::endl;
}

/// Funkcja główna programu
int main()
{
// Tworzę obiekt goblina i bossa
Enemy boss;
Enemy goblin;

// Ustawiam goblinowi odpowiednie wartości
goblin.name = "Goblin wojownik";
goblin.health = 60;
goblin.strength = 14;

// Ustawiam bossowi odpowiednie wartości
boss.name = "Ogr";
boss.health = 250;
boss.strength = 50;

// Wyświetlam informacje o każdym z nich:
print_enemy_info(goblin);
print_enemy_info(boss);
}

Umieszczanie wewnątrz tablicy

Obiekty możemy umieszczać wewnątrz tablic tak samo jak normalne zmienne:

Tablica przeciwników
std::vector< Enemy > enemies;

Poniżej przykład jak dodawać do takiej tablicy:

Wykorzystanie tablicy
// ...

int main()
{
std::vector< Enemy > enemies;

// (opcjonalnie)
// Blok kodu, by ograniczyć widoczność
// zmiennych utworzonych wewnątrz
{
// Tworzę goblina 👉 lokalnie 👈
Enemy goblin;
// Ustawiam goblinowi odpowiednie wartości
goblin.name = "Goblin wojownik";
goblin.health = 60;
goblin.strength = 14;

// Dodaję goblina do tablicy
enemies.push_back( goblin );
}
// 👈 od tego momentu goblin istnieje tylko w tablicy enemies

// Wyświetl wszystkich przeciwników:
for (Enemy enemy : enemies)
print_enemy_info(enemy);
}
Przykład

Po zapoznaniu się z tą lekcją, przejrzyj ten przykładowy program: 👾 Arena walki wraz z jego omówieniem. Zobaczysz tam zastosowanie tablic i struktur w praktyce.

Domyślne wartości pól

Elementom struktury możemy nadać domyślne wartości, przez co nie będziemy musieli ich za każdym razem wypełniać.

Dobrym przykładem użycia domyślnej wartości jest zmienna, która przechowuje całkowitą ilość obrażeń, które zadał przeciwnik. Na początek dla każdego wroga, ta wartość będzie musiała być równa 0.

Wartość zmiennych

Jeśli pozostawisz pole struktury bez domyślnej wartości, np.:

struct Car
{
int number_of_wheels; // ilośc kół samochodu
};

to nie oznacza to, że number_of_wheels na początku otrzyma wartość 0, musisz to zrobić ręcznie!

Żeby przypisać domyślną wartość do pola struktury używamy zwykłej inicjalizacji, znaną z tworzenia zmiennych:

Domyślna wartość dla 'total_damage' ⚔
/// Utworzenie struktury
struct Enemy
{
std::string name;
float health;
float strength;

float total_damage = 0; // ilość obrażeń
};

Teraz gdy stworzymy jakiegoś wroga:

Enemy snake; // np. wąż

to wartość

snake.total_damage

będzie równa 0.

Możesz się o tym przekonać, np. wyświetlając ją:

int main() {
Enemy snake;
snake.name = "Wąż";
// 🟡 Uwaga, nie ustawiam ręcznie wartości total_damage

std::cout << snake.name
<< " zadał łącznie "
<< snake.total_damage
<< " obrażeń";
}
Wynik
Wąż zadał łącznie 0 obrażeń

Potencjalne błędy

Uwaga!

Ta sekcja wymaga rozbudowy. Możesz nam pomóc edytując tą stronę.

Oto lista popularnych błędów związanych z tą lekcją:

Brak średnika po definicji

Tylko dla przypomnienia.

Nieprawidłowa kolejność

Upewnij się, że struktura jest zdefiniowana przed jej pierwszym użyciem.

Przykład błędnego kodu:

🔴 Niepoprawna kolejność
// ❌ Błąd: użycie "Enemy" przed zdefiniowaniem
void print_enemy_info(Enemy enemy)
{
std::cout << enemy.name << " posiada "
<< enemy.health << " hp i "
<< enemy.strength << " siły."
<< std::endl;
}

/// Utworzenie struktury
struct Enemy
{
std::string name;
float health;
float strength;
};
Ciekawostka

Ten problem jest możliwy do rozwiązania w inny, wygodniejszy sposób niż przenoszenie funkcji print_enemy_info pod definicję struktury, jednak o tzw. forward declaration wspomnimy w dalszej części kursu.

Modyfikacja wewnątrz definicji struktury

Zmiennych nie możemy modyfikować wewnątrz definicji struktury. Możliwe jest jedynie przypisanie początkowej wartości:

🔴 Próba modyfikacji w złym miejscu
struct Enemy
{
std::string name;
float health;
float strength;

int total_damage = 0; // OK ✅

// ❌ Błąd: próba modyfikacji w złym miejscu
health = 250;
};