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 enemyName = "Goblin";
float enemyHealth = 50;
float enemyStrength = 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 >  enemyNames;
std::vector< float > enemyHealth;
std::vector< float > enemyStrength;

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

  • enemyNames[ index ] opisuje nazwę
  • enemyHealth[ index ] opisuje punkty życia
  • enemyStrength[ 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:

enemyNames.push_back("Goblin");
enemyHealth.push_back(50);
enemyStrength.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 printEnemyInfo(Enemy enemy)
{
std::cout << enemy.name << " posiada "
<< enemy.health << " hp i "
<< enemy.strength << " siły."
<< std::endl;
}
Kolejność

printEnemyInfo 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 printEnemyInfo(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:
printEnemyInfo(goblin);
printEnemyInfo(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)
printEnemyInfo(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 numberOfWheels; // ilośc kół samochodu
};

to nie oznacza to, że numberOfWheels 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 'totalDamage' ⚔
/// Utworzenie struktury
struct Enemy
{
std::string name;
float health;
float strength;

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

Teraz gdy stworzymy jakiegoś wroga:

Enemy snake; // np. wąż

to wartość

snake.totalDamage

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 totalDamage

std::cout << snake.name
<< " zadał łącznie "
<< snake.totalDamage
<< " 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 printEnemyInfo(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 printEnemyInfo 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 totalDamage = 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 enemyName = "Goblin";
float enemyHealth = 50;
float enemyStrength = 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 >  enemyNames;
std::vector< float > enemyHealth;
std::vector< float > enemyStrength;

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

  • enemyNames[ index ] opisuje nazwę
  • enemyHealth[ index ] opisuje punkty życia
  • enemyStrength[ 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:

enemyNames.push_back("Goblin");
enemyHealth.push_back(50);
enemyStrength.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 printEnemyInfo(Enemy enemy)
{
std::cout << enemy.name << " posiada "
<< enemy.health << " hp i "
<< enemy.strength << " siły."
<< std::endl;
}
Kolejność

printEnemyInfo 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 printEnemyInfo(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:
printEnemyInfo(goblin);
printEnemyInfo(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)
printEnemyInfo(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 numberOfWheels; // ilośc kół samochodu
};

to nie oznacza to, że numberOfWheels 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 'totalDamage' ⚔
/// Utworzenie struktury
struct Enemy
{
std::string name;
float health;
float strength;

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

Teraz gdy stworzymy jakiegoś wroga:

Enemy snake; // np. wąż

to wartość

snake.totalDamage

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 totalDamage

std::cout << snake.name
<< " zadał łącznie "
<< snake.totalDamage
<< " 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 printEnemyInfo(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 printEnemyInfo 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 totalDamage = 0; // OK ✅

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