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 strukturystruct Enemy{	std::string	name;	float		health;	float		strength;};/// Funkcja wyświetlająca informacje o przeciwnikuvoid print_enemy_info(Enemy enemy){	std::cout	<< enemy.name		<< " posiada "				<< enemy.health		<< " hp i "				<< enemy.strength	<< " siły."				<< std::endl;}/// Funkcja główna programuint 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 strukturystruct 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 zdefiniowaniemvoid print_enemy_info(Enemy enemy){	std::cout	<< enemy.name		<< " posiada "				<< enemy.health		<< " hp i "				<< enemy.strength	<< " siły."				<< std::endl;}/// Utworzenie strukturystruct 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 strukturystruct Enemy{	std::string	name;	float		health;	float		strength;};/// Funkcja wyświetlająca informacje o przeciwnikuvoid print_enemy_info(Enemy enemy){	std::cout	<< enemy.name		<< " posiada "				<< enemy.health		<< " hp i "				<< enemy.strength	<< " siły."				<< std::endl;}/// Funkcja główna programuint 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 strukturystruct 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 zdefiniowaniemvoid print_enemy_info(Enemy enemy){	std::cout	<< enemy.name		<< " posiada "				<< enemy.health		<< " hp i "				<< enemy.strength	<< " siły."				<< std::endl;}/// Utworzenie strukturystruct 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;};