Serwis Edukacyjny
w I-LO w Tarnowie
obrazek

Materiały dla uczniów liceum

  Wyjście       Spis treści       Wstecz       Dalej  

obrazek

Autor artykułu: mgr Jerzy Wałaszek
Konsultacje: Wojciech Grodowski, mgr inż. Janusz Wałaszek

©2024 mgr Jerzy Wałaszek
I LO w Tarnowie

obrazek

Warsztat

Kurs języka C

Struktury

SPIS TREŚCI
Podrozdziały

Definiowanie struktur

Struktura (ang. structure) jest złożonym typem danych. Poznaliśmy już jeden złożony typ danych: tablice. W tablicy można przechowywać wiele wartości tego samego typu. W strukturze możesz przechowywać wartości różnych typów w tzw. polach (ang. fields).

Struktura jest typem danych, który służy do tworzenia zmiennych strukturalnych. Najpierw ten typ należy zdefiniować. Definicja typu struktury wygląda następująco:

struct nazwa_typu
{
  typ pole_1;
  typ pole_2;
  ...
  typ pole_n
};

Zwróć uwagę, że za definicją typu strukturalnego umieszcza się średnik. Typ strukturalny po zdefiniowaniu możesz używać do tworzenia zmiennych podobnie jak typy wbudowane int, double, float itp.

struct nazwa_typu strukturalnego nazwa_zmiennej.

Na przykład:

struct TPunkt // definicja typu strukturalnego TPunkt
{
  float x;
  float y;
};
...
struct TPunkt a,b,c;

W powyższym przykładzie zdefiniowany został typ strukturalny TPunkt. Możesz go traktować jako plan budowy przyszłych zmiennych. Każda struktura typu TPunkt zawiera w sobie dwa pola o nazwach x i y. W polach tych program może umieścić liczby zmiennoprzecinkowe typu float. Po zdefiniowaniu typu strukturalnego następuje definicja trzech zmiennych: a, b i c. Każda z tych zmiennych jest strukturą typu TPunkt, a zatem zawiera w sobie dwa pola x i y typu float.

Jeśli planujesz tylko jednorazowe użycie struktury w swoim programie, to nie musisz tworzyć jawnego typu strukturalnego. Zmienne tworzysz bezpośrednio po definicji struktury:

struct
{
  float x;
  float y;
} a,b,c;

W takim przypadku powstają trzy zmienne a, b i c i każda jest strukturą, która zawiera dwa pola x i y. Nazwa typu struktury nie została tutaj nazwana. Nie polecam jednak tego sposobu (niemniej czasami jest to użyteczne). Definiowanie typu strukturalnego nic nie kosztuje – jest to tylko informacja dla kompilatora. Nie zwiększa ona wielkości programu wynikowego. Zdefiniowany typ strukturalny może być wykorzystywane w różnych funkcjach do tworzenia struktur. Polecam definiować typy strukturalne na samym początku programu i dobrze je opisywać.


Zmienną strukturalną można zainicjować podobnie jak tablicę przez podanie wartości jej pól w klamerkach. Dostęp do pola struktury, które jest normalną zmienną (chyba że poprzedziłeś definicję zmiennej słowem const), uzyskujemy poprzez nazwę zmiennej, operator kropka oraz nazwę pola. uruchom poniższy program:

/*
 Struktury
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 22.10.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

// Definicja typu strukturalnego
struct TPracownik
{
  char i[10];
  char n[12];
  int w,p;
};

int main()
{
  struct TPracownik p1 = {"Jan","Kowalski",44,2150};
  struct TPracownik p2 = {"Grzegorz","Liska",52,1800};
  struct TPracownik p3 = {"Anna","Matuszewska",31,12700};

  setlocale(LC_ALL,"");

  printf("+--------------------------------------+\n"
         "|Firmowa baza danych pracowniczych 2016|\n"
         "+------------+----------+----+---------+\n"
         "|NAZWISKO    |IMIĘ      |WIEK|PENSJA   |\n"
         "+------------+----------+----+---------+\n");
  printf("|%-12s|%-10s| %2d |%5d PLN|\n", p1.n, p1.i, p1.w, p1.p);
  printf("|%-12s|%-10s| %2d |%5d PLN|\n", p2.n, p2.i, p2.w, p2.p);
  printf("|%-12s|%-10s| %2d |%5d PLN|\n", p3.n, p3.i, p3.w, p3.p);
  printf("+------------+----------+----+---------+\n");

  return 0;
}

Co więcej, struktury mogą być elementami tablic, jak każde inne zmienne. Umożliwia to efektywne przetwarzanie danych w pętli. Uruchom poniższy program:

/*
 Struktury
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 22.10.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

// Definicja typu strukturalnego
struct TPracownik
{
  char i[10];
  char n[12];
  int w,p;
};

int main()
{
  struct TPracownik t[] = {
                            {"Jan","Kowalski",44,2150},
                            {"Grzegorz","Liska",52,1800},
                            {"Anna","Matuszewska",31,12700},
                            {"Barbara","Nowakowska",35,1750},
                            {"Kazimierz","Grochowski",48,3600},
                            {"","",0,0} // ostatni rekord
                          };
  int i;

  setlocale(LC_ALL,"");

  printf("+--------------------------------------+\n"
         "|Firmowa baza danych pracowniczych 2016|\n"
         "+------------+----------+----+---------+\n"
         "|NAZWISKO    |IMIĘ      |WIEK|PENSJA   |\n"
         "+------------+----------+----+---------+\n");
  for(i = 0; t[i].w; i++)
    printf("|%-12s|%-10s| %2d |%5d PLN|\n", t[i].n, t[i].i, t[i].w, t[i].p);
  printf("+------------+----------+----+---------+\n");

  return 0;
}

Zwróć uwagę na inicjalizację tablicy. Wbrew pozorom nie różni się ona od inicjalizacji tablic ze zwykłymi zmiennymi:

int t[] = {1, 7, 9, 3, 8};

Tutaj elementami tablicy są struktury. Zatem inicjalizacja każdego elementu wymaga inicjalizacji struktury. Dlatego wewnątrz klamerek inicjujących zawartość tablicy mamy dla każdej struktury osobne klamerki z danymi, które zostaną wstawione do jej pól. Zasada jest zatem taka sama jak dla zwykłych typów: inicjujemy poszczególne komórki-struktury.

Odwołanie do komórki o danym indeksie i jest odwołaniem do struktury, która w tej komórce się znajduje. Jeśli chcemy uzyskać dostęp do określonego pola tej struktury, używamy operatora kropka i nazwy pola:

t [ i ].w odwołuje się do pola w struktury umieszczonej w komórce i-tej tablicy t.

Koniec tablicy oznaczamy strukturą, której pole w (wiek) ma wartość 0. W ten sposób pętla rozpoznaje koniec danych i kończy wyświetlanie zawartości tablicy.


Na początek:  podrozdziału   strony 

Przetwarzanie struktur

Pola struktur są normalnymi zmiennymi, dlatego można do nich stosować wszystkie instrukcje dozwolone dla zmiennych (no, prawie wszystkie, ponieważ niektóre mogą nie mieć sensu dla struktur).

Uruchom poniższy program:

/*
 Struktury
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 22.10.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <locale.h>

// Definicja typu strukturalnego
struct Txy
{
  float x,y;
};

int main()
{
  struct Txy a;

  setlocale(LC_ALL,"");

  a.x = 1;
  while(a.x <= 6)
  {
    a.y = sqrt(a.x);
    printf("Pierwiastek kwadratowy z %5.3f wynosi %5.3f\n", a.x, a.y);
    a.x += 0.125;
  }

  return 0;
}

Oczywiście to samo uzyskalibyśmy za pomocą dwóch zmiennych typu x i y. Jednak tutaj te zmienne zostały ze sobą powiązane faktem, że znajdują się w jednej strukturze. Dzięki zgrupowaniu zmiennych uzyskujemy nad nimi większą kontrolę. Szczególnie wtedy, gdy struktura zawiera wiele pól. Na szczęście takie skomplikowane struktury danych nie są zbyt często stosowane w świecie małych mikrokontrolerów. Jednak warto coś wiedzieć na ich temat – a nóż przerzucisz się w niedalekiej przyszłości na potężne mikrokontrolery 32- lub 64-bitowe z mnóstwem pamięci, gdzie można uruchamiać zaawansowane oprogramowanie.

Porównanie dwóch struktur

Jest to jedna z operacji, której nie da się przeprowadzić prosto przy pomocy operatorów porównań == , !=, <, >... Powodem jest to, iż operatory te działają tylko na typach prostych. W przypadku typów złożonych, a takimi są tablice i struktury, musisz przeprowadzić porównanie poszczególnych elementów obu struktur. Poza tym niektóre z tych operatorów nie mają nawet dobrze zdefiniowanego sensu dla struktur. Co to na przykład znaczy, że struktura a jest większa od struktury b (a > b ?).

Mówimy, że dwie struktury są równe, jeśli odpowiadające sobie pola w obu strukturach zawierają tę samą wartość. Inaczej struktury te są różne.

Porównanie wykonujemy za pomocą operatora == oraz koniunkcji logicznej &&:

(a.pole_1==b.pole_1)&&(a.pole_2==b.pole_2)&&...&&(a.pole_n==b.pole_n)

Jeśli wynikiem tego wyrażenia jest 1 (prawda), to struktury a i b są równe. Jeśli wynikiem jest 0 (fałsz), to struktury a i b są różne.
Uruchom poniższy program:

/*
 Struktury
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 22.10.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

// Definicja typu strukturalnego
struct Ts
{
  int a,b;
  float x;
};

int main()
{
  struct Ts s1 = {1,8,3.141592};
  struct Ts s2 = {1,8,3.141592};

  setlocale(LC_ALL,"");

  printf("s1 %s s2\n", (s1.a==s2.a)&&(s1.b==s2.b)&&(s1.x==s2.x)?"==":"!=");

  return 0;
}

Zmień w programie dane inicjalizacyjne jednej ze struktur, skompiluj i uruchom program ponownie.

Kopiowanie struktur

Struktury można kopiować tak samo jak zwykłe zmienne za pomocą operatora przypisania:

/*
 Struktury
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 22.10.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

// Definicja typu strukturalnego
struct Ts
{
  int a,b;
  float x;
};

int main()
{
  struct Ts s1 = {1,8,3.141591};
  struct Ts s2;

  setlocale(LC_ALL,"");

  s2 = s1;
  printf("s2.a = %d\n"
         "s2.b = %d\n"
         "s2.x = %f\n", s2.a, s2.b, s2.x);

  return 0;
}

Struktura w pamięci komputera zajmuje pewien obszar, w którym są rozmieszczone jej pola. Kopiowanie polega na skopiowaniu bajt po bajcie zawartości obszaru jednej struktury do obszaru drugiej struktury, która musi być tego samego typu. W ten sposób pola drugiej struktury przyjmują dokładnie te same wartości co pola w pierwszej strukturze.

Należy postępować ostrożnie, gdy kopiujemy struktury zawierające wskaźniki. W takim przypadku zostaje skopiowany adres umieszczony we wskaźniku, natomiast wskazywany obiekt nigdzie nie jest kopiowany. W efekcie wskaźniki w obu strukturach wskazują na ten sam obiekt. Uruchom program:

/*
 Struktury
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 22.10.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

// Definicja typu strukturalnego
struct Ts
{
  int x;
  int *p; // wskaźnik!!
};

int main()
{
  struct Ts a,b;

  setlocale(LC_ALL,"");

  a.x = 10;
  a.p = &a.x; // pole p wskazuje na pole x w a
  b = a; // do struktury b kopiujemy strukturę a
  *b.p = 99; // zmieniamy zawartość danej wskazywanej przez p w b
  // Co się zmieniło?
  printf("a.x = %d\n"
         "b.x = %d\n", a.x, b.x);

  return 0;
}

Co robi ten program? Tworzy dwie struktury typu Ts o nazwach a i b. W strukturze typu Ts znajdują się dwa pola. Jedno typu int o nazwie x, a drugie będące wskaźnikiem do danej typu int i mające nazwę p. Po uruchomieniu program umieszcza w polu x struktury a liczbę 10. Następnie w jej polu p umieszcza adres pola x (użyty tutaj jest operator adresu &), czyli pole p wskazuje na pole a tej samej struktury. Teraz następuje skopiowanie struktury a do struktury b. Kopiowanie przenosi wartości obu pól. Zatem w b.x znajdzie się a.x, a w b.p znajdzie się b.p. Pole p struktury b nie wskazuje jednak na pole b.x  lecz wciąż na a.x. Jeśli teraz do obiektu wskazywanego przez b.p wstawimy liczbę 99, to trafi ona do struktury a, a nie do b. Oba wskaźniki a.p i b.p wskazują ten sam obiekt w pamięci, ponieważ zawierają ten sam adres. W efekcie otrzymasz:

a.x = 99
b.x = 10
Po kopiowaniu wskaźniki należy odpowiednio uaktualnić. Pamiętaj o tym, ponieważ tego typu błędy są później trudne do wyłapania w większych programach.

Zerowanie struktury

Strukturę można wyzerować przez wyzerowanie jej poszczególnych pól. Jednak przy dużej liczbie pól może to być żmudne. Istnieje prostszy sposób, który wykorzystuje wskaźnik. Zasada jest następująca:

Tworzymy wskaźnik typu void *. Tego rodzaju wskaźnik nie wskazuje żadnych konkretnych danych. Dlatego można mu przypisać adres dowolnego obiektu. We wskaźniku umieszczamy zatem adres zmiennej strukturalnej, którą chcemy wyzerować. Następnie w pętli, która wykonuje się tyle razy ile wynosi rozmiar struktury, zerujemy bajt pamięci wskazywany przez wskaźnik, po czym wskaźnik przesuwamy na adres następnego bajtu.

Uruchom program:

/*
 Struktury
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 22.10.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

// Definicja typu strukturalnego
struct Ts
{
  char t[10];
  int a,b;
  float x,y;
};

int main()
{
  struct Ts s = {"Hejka!",6,125,6.28,-15.44};
  void * p = &s; // w p adres struktury s
  int i;

  setlocale(LC_ALL,"");

  printf("Przed:\n%s\n%d\n%d\n%f\n%f\n\n",s.t,s.a,s.b,s.x,s.y);
  for(i = sizeof(s); i; i--) *(char *)p++ = 0;
  printf("Po:\n%s\n%d\n%d\n%f\n%f\n\n",s.t,s.a,s.b,s.x,s.y);

  return 0;
}

Zwróć uwagę na przypisanie wewnątrz pętli:

*(char *)p++ = 0;

Jak rozumieć ten zapis. W nawiasie mamy tzw. rzutowanie (ang. casting). Rzutowanie mówi kompilatorowi, aby potraktował następne wyrażenie jako wyrażenie podanego w rzutowaniu typu. Tutaj kompilator ma potraktować wskaźnik p tak, jakby wskazywał daną typu char, czyli po prostu jedną komórkę pamięci. Bez rzutowania kompilator zgłosi błąd. W komórce pamięci wskazywanej przez wskaźnik p zostaje umieszczone zero, po czym wskaźnik jest zwiększany i wskazuje na następną komórkę pamięci.

Gdy pętla for się zakończy, cały obszar struktury zostanie wyzerowany. A dlaczego pętla zlicza wstecz? Po prostu po to, aby w każdym obiegu nie obliczać rozmiaru struktury. Wewnątrz pętli nie korzystamy z wartości i. Istotna jest tylko liczba obiegów.


Na początek:  podrozdziału   strony 

Wskaźniki do struktur

Wskaźnik do struktury definiujemy następująco:
struct nazwa_struktury * wskaźnik;

Dostęp do pól struktury uzyskujemy za pomocą operatora strzałki:

wskaźnik -> pole_struktury

Uruchom program:

/*
 Struktury
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 22.10.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <locale.h>

struct TPunkt
{
  float x,y;
  char n;
};

// Funkcja wyświetla współrzędne punktu
//-------------------------------------
void pp(struct TPunkt * p)
{
  printf("Punkt %c:(%5.2f,%5.2f)\n", p->n, p->x, p->y);
}

// Funkcja oblicza odległość dwóch punktów
float len(struct TPunkt * a, struct TPunkt * b)
{
  float x = b->x - a->x;
  float y = b->y - a->y;

  return sqrt(x*x + y*y);
}

// Funkcja wyświetla wyniki
void pr(struct TPunkt * a, struct TPunkt * b)
{
  pp(a);
  pp(b);
  printf("Odleglosc od %c do %c = %5.2f\n\n",
         a->n, b->n, len(a, b));
}

int main()
{
  struct TPunkt a = {1,0,'A'};
  struct TPunkt b = {2,1,'B'};
  struct TPunkt c = {2,2,'C'};

  setlocale(LC_ALL,"");

  pr(&a,&b);
  pr(&b,&c);
  pr(&c,&a);

  return 0;
}

Na początek:  podrozdziału   strony 

Unie

Unia (ang. union) nie jest właściwie strukturą danych, chociaż definiujemy ją podobnie:
union nazwa_typu_unii
{
  typ pole_1;
  typ pole_2;
  ...
  typ pole_n;
};

Po zdefiniowaniu typu unii, wykorzystujemy go do definicji zmiennych:

union nazwa_typu_unii zmienna;

Zmienne można również definiować bez nazywania typu unii:

union
{
  typ pole_1;
  typ pole_2;
  ...
  typ pole_n;
} zmienna;

W takim przypadku powstaje zmienna unii posiadająca podane w definicji pola.

Struktura pozwala na jednoczesne przechowywanie wielu danych w swoich polach, ponieważ każde z nich w pamięci jest umieszczone w innym miejscu. Unia pozwala na przechowywanie tylko jednej danej naraz, ponieważ wszystkie pola unii są umieszczone w tym samym miejscu w pamięci. Na przykład:

struct xx
{
  char a;
  int b;
  float c;
};
 
union yy
{
  char a;
  int b;
  float c;
};
obrazek   obrazek

W strukturze xx mamy trzy pola: a, b i c. W tych polach program może umieścić trzy różne dane i struktura będzie je przechowywała.

W unii yy również mamy trzy pola: a, b i c. Jednak zajmują one ten sam obszar pamięci, dlatego w danej chwili unia może przechowywać dane tylko w jednym z tych pól. Unia pozwala efektywnie wykorzystać pamięć, szczególnie wtedy, gdy jest jej mało. Dzięki niej istnieje możliwość wykorzystania tego samego obszaru pamięci do różnych celów.

W pamięci struktura zajmuje taki obszar, aby pomieścił wszystkie jej pola. Unia natomiast zajmuje taki obszar, aby pomieścił jej największe pole.

/*
 Unie
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 27.10.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

int main()
{
  struct
  {
    char a;
    int b;
    float c;
  } s;

  union
  {
    char a;
    int b;
    float c;
  } u;

  setlocale(LC_ALL,"");

  printf("Rozmiar struktury: %2d B\n"
         "Rozmiar unii : %2d B\n",
         sizeof(s),sizeof(u));

  return 0;
}

Wynikiem działania programu jest:

Rozmiar struktury: 12 B
Rozmiar unii     :  4 B

Przeanalizujmy to. Zmienna s jest strukturą o trzech polach: a typu char (1 bajt), b typu int (4 bajty) i c typu float (4 bajty). Jeśli zsumujesz rozmiary tych pól, otrzymasz 1 + 4 + 4 = 9 bajtów. Tymczasem program pokazuje 12 bajtów. Błąd? Nie, po prostu dane są układane w pamięci, tak aby mikroprocesor Pentium szybko je pobierał. W architekturze 32-bitowej pamięć jest widziana przez mikroprocesor jako komórki 32-bitowe, które mogą dzielić się na dwie dane 16-bitowe lub na 4 dane 8-bitowe:

obrazek

Komórki 32-bitowe posiadają adresy podzielne przez 4. Komórki 16-bitowe posiadają adresy podzielne przez 2. Takie rozwiązanie pozwala mikroprocesorowi pobierać dane w jednym cyklu dostępu do pamiąci.

Typy int oraz float posiadają rozmiar 4 bajtów i są umieszczane w pamięci w komórkach o adresach podzielnych przez 4. Dlatego w strukturach mogą występować puste miejsca (niezajęte przez dane). Dla przykładu, struktura s z naszego programu jest rozmieszczona w pamięci w sposób następujący:

pole a puste b c
komórki 8b   1   2   3   4   5   6   7   8   9 10 11 12
komórki 32b 1 2 3

Teraz rachunek nam się zgodzi. Ponieważ pierwsze pole a jest typu char, to zajmuje w pamięci tylko jedną komórkę 8-bitową. Drugie pole b jest typu int i zajmuje jedną komórkę 32-bitową. Z tego powodu nie może być umieszczone zaraz za polem a, ponieważ się nie zmieści. Umieszczone zostaje pod adresem podzielnym przez 4. Za polem a powstają trzy niewykorzystywane w strukturze komórki 8-bitowe. Dlatego cała struktura zajmuje 1 + 3 + 4 + 4 = 12 bajtów.

A teraz unia: otrzymujemy wynik 4, ponieważ jest to maksymalny rozmiar danych, które w unii będą przechowywane:

  a      
  b
  c
komórki 8b 1 2 3 4

Unia przechowuje tylko zawartość jednego pola. Dostęp do pól odbywa się identycznie jak w strukturze: za pomocą operatora kropka.

/*
 Unie
 (C)2016 mgr Jerzy Wałaszek
 Data utworzenia: 27.10.2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

int main()
{
  union
  {
    char a;
    int b;
    float c;
  } u;

  setlocale(LC_ALL,"");

  u.c = 3.141592;
  printf("Pole a = %c\n"
          "Pole b = %d\n"
          "Pole c = %f\n\n", u.a, u.b, u.c);
  u.b = 1234567890;
  printf("Pole a = %c\n"
         "Pole b = %d\n"
         "Pole c = %f\n\n", u.a, u.b, u.c);
  u.a = 'A';
  printf("Pole a = %c\n"
         "Pole b = %d\n"
         "Pole c = %f\n\n", u.a, u.b, u.c);

  return 0;
}

Program tworzy unię zawierająca trzy pola. Następnie wpisuje dane do kolejnych pól i wyświetla zawartość unii. Zwróć uwagę, że poprawnie będzie wyświetlone tylko to pole, do którego wprowadzono dane. Odczyt pozostałych pól daje jakieś przypadkowe wyniki.

Zapraszam do następnego rozdziału.


Na początek:  podrozdziału   strony 

Zespół Przedmiotowy
Chemii-Fizyki-Informatyki

w I Liceum Ogólnokształcącym
im. Kazimierza Brodzińskiego
w Tarnowie
ul. Piłsudskiego 4
©2024 mgr Jerzy Wałaszek

Materiały tylko do użytku dydaktycznego. Ich kopiowanie i powielanie jest dozwolone
pod warunkiem podania źródła oraz niepobierania za to pieniędzy.

Pytania proszę przesyłać na adres email: i-lo@eduinf.waw.pl

Serwis wykorzystuje pliki cookies. Jeśli nie chcesz ich otrzymywać, zablokuj je w swojej przeglądarce.

Informacje dodatkowe.