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
Obrazek goblina wykonany przez LuizMeloJeś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 życiaenemy_strength[ index ]
opisuje punkty siły
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
.
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.
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
.
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:
boss.name = "Ogr";
Używamy kropki .
, do odniesienia się do pola obiektu. W ten sam sposób,
możemy np. zmodyfikować siłę wroga:
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:
#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:
void print_enemy_info(Enemy enemy)
{
std::cout << enemy.name << " posiada "
<< enemy.health << " hp i "
<< enemy.strength << " siły."
<< std::endl;
}
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
#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:
std::vector< Enemy > enemies;
Poniżej przykład jak dodawać do takiej 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);
}
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
.
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:
/// 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ń";
}
Wąż zadał łącznie 0 obrażeń
Potencjalne błędy
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
Nieprawidłowa kolejność
Upewnij się, że struktura jest zdefiniowana przed jej pierwszym użyciem.
Przykład błędnego kodu:
// ❌ 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;
};
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:
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;
};