typedef Jest to słowo kluczowe, które służy do definiowania typów pochodnych np.: typedef stara_nazwa nowa_nazwa; typedef int mojInt; typedef int* WskNaInt;
od tej pory można używać typów mojInt i WskNaInt.
Często używa się typedef w jednej instrukcji razem z definicją typu
Typ wyliczeniowy Służy do tworzenia zmiennych, które mogą przyjmować tylko pewne z góry ustalone wartości: enum Nazwa {WARTOSC_1, WARTOSC_2, WARTOSC_N }; Na przykład można w ten sposób stworzyć zmienną przechowującą kierunek: enum Kierunek {W_GORE, W_DOL, W_LEWO, W_PRAWO};
enum Kierunek ruch = W_GORE;
Gdzie "Kierunek" to typ zmiennej, wcześniej określony, a "ruch" nazwa zmiennej, o takim typie. Zmienną tę można teraz wykorzystać na przykład w instrukcji switch switch(ruch) { case W_GORE: printf("w górę\n"); break; case W_DOL: printf("w dół\n"); break; default: printf("gdzieś w bok\n");
Tradycyjnie przechowywane wielkości zapisuje się wielkimi literami (W_GORE, W_DOL).
Tak naprawdę C przechowuje wartości typu wyliczeniowego jako liczby całkowite (zakres typu signed int), o czym można się łatwo przekonać: ruch = W_DOL; printf("%i\n", ruch); /* wypisze 1 */
Kolejne wartości to po prostu liczby całkowite: domyślnie pierwsza to zero, druga jeden itp. Możemy przy deklarowaniu typu wyliczeniowego zmienić domyślne przyporządkowanie: enum Kierunek { W_GORE, W_DOL = 8, W_LEWO, W_PRAWO }; printf("%i %i\n", W_DOL, W_LEWO); /* wypisze 8 9 */
Co więcej liczby mogą się powtarzać i wcale nie muszą być ustawione w kolejności rosnącej: enum Kierunek { W_GORE = 5, W_DOL = 5, W_LEWO = 2, W_PRAWO = -1 }; printf("%i %i\n", W_DOL, W_LEWO); /* wypisze 5 2 */
Traktowanie przez kompilator typu wyliczeniowego jako liczby pozwala na wydajną ich obsługę, ale stwarza niebezpieczeństwa:
Można przypisywać pod typ wyliczeniowy liczby, nawet nie mające odpowiednika w wartościach, a kompilator może o tym nawet nie ostrzec: ruch = 40; Lub przypisać pod typ wyliczeniowy, np. liczby: enum Kierunek { W_GORE, W_DOL, W_LEWO = -1, W_PRAWO }; Co spowoduje nadanie tej samej wartości 0 dla elementów W_GORE i W_PRAWO, a to może skutkować błędem kompilacji, np. w przytoczonym powyżej użyciu instrukcji switch.
Struktury Struktury to specjalny typ danych mogący przechowywać wiele wartości w jednej zmiennej. Od tablic jednakże różni się tym, iż te wartości mogą być różnych typów.
Struktury definiuje się w następujący sposób: struct Struktura { int pole1; int pole2; char pole3;
gdzie "Struktura" to nazwa tworzonej struktury. Nazewnictwo, ilość i typ pól definiuje programista według własnego uznania.
Zmienną posiadającą strukturę tworzy się podając jako jej typ nazwę struktury. struct Struktura zmiennaS;
Dostęp do poszczególnych pól uzyskuje się przy pomocy operatora wyboru składnika: kropki ('.'). zmiennaS.pole1 = 60; /* przypisanie liczb do pól */ zmiennaS.pole2 = 2; zmiennaS.pole3 = 'a'; /* a teraz znaku */
typedef struct Deklaracja typu strukturalnego
Definicja nienazwanego typu strukturalnego:stackoverflow question: how-to-properly-use-typedef-for-structs-in-c
typedef struct { int x; int y; } TypPunktowy;
TypPunktowy to jest alias do typu krótka nazwa typu ( pełna nazwa to: struct TypPunktowy)
W obrębie struktury nie można odwoływać się do siebie za pomocą krótkiej nazwy.
Struktury jako argumenty funkcji mogą być użyte na 2 sposoby:stackoverflow questions : passing-struct-to-function przekazywanie wartości bez możliwości ich zmiany ( ang. pass by value) przekazywanie wartości z możliwością ich zmiany ( ang. pass by reference)
/* https://en.cppreference.com/w/c/language/struct_initialization
gcc s.c -Wall -Wextra ./a.out
* / # include
// definicja typu typedef struct { char name; char sex; int age; } person_t;
// zmienna person_t p = {"Tom", 'M', 19};
int main(void) {
printf("person name = %s, sex = %d age = %d \n", p.name, p.sex, p.age);
return 0; }
Unie Unie to kolejny sposób prezentacji danych w pamięci. Na pierwszy rzut oka wyglądają bardzo podobnie do struktur: union Nazwa { typ1 nazwa1; typ2 nazwa2; /* ... */
Na przykład: union LiczbaLubZnak { int calkowita; char znak; double rzeczywista;
Pola w unii nakładają się na siebie w ten sposób, że w danej chwili można w niej przechowywać wartość tylko jednego typu. Unia zajmuje w pamięci tyle miejsca, ile zajmuje największa z jej składowych. W powyższym przypadku unia będzie miała prawdopodobnie rozmiar typu double czyli często 64 bity, a całkowita i znak będą wskazywały odpowiednio na pierwsze cztery bajty lub na pierwszy bajt unii (choć nie musi tak być zawsze). Dlaczego tak? Taka forma często przydaje się np. do konwersji pomiędzy różnymi typami danych. Możemy dzięki unii podzielić zmienną 32-bitową na cztery składowe zmienne o długości 8 bitów każda.
Do konkretnych wartości pól unii odwołujemy się, podobnie jak w przypadku struktur, za pomocą kropki: union LiczbaLubZnak liczba; liczba.calkowita = 10; printf("%d\n", liczba.calkowita);
Zazwyczaj użycie unii ma na celu zmniejszenie zapotrzebowania na pamięć, gdy naraz będzie wykorzystywane tylko jedno pole i jest często łączone z użyciem struktur.
Przyjrzyjmy się teraz przykładowi, który powinien dobitnie zademonstrować działanie unii: #include
struct adres_bajtowy { __uint8_t a; __uint8_t b; __uint8_t c; __uint8_t d;
union adres { __uint32_t ip; struct adres_bajtowy badres;
int main () { union adres addr; addr.badres.a = 192; addr.badres.b = 168; addr.badres.c = 1; addr.badres.d = 1; printf ("Adres IP w postaci 32-bitowej zmiennej: %08x\n",addr.ip); return 0;
Zauważyłeś pewien ciekawy efekt? Jeśli uruchomiłeś ten program na typowym komputerze domowym (rodzina i386) na ekranie zapewne pojawił Ci się taki oto napis:
Adres IP w postaci 32-bitowej zmiennej: 0101a8c0
Dlaczego jedynki są na początku zmiennej, skoro w programie były to dwa ostatnie bajty (pola c i d struktury)? Jest to problem kolejności bajtów. Aby dowiedzieć się o nim więcej przeczytaj rozdział przenośność programów. Zauważyłeś zatem, że za pomocą tego programu w prosty sposób zamieniliśmy cztery zmienne jednobajtowe w jedną czterobajtową. Jest to tylko jedno z możliwych zastosowań unii.
Inicjacja struktur i unii Jeśli tworzymy nową strukturę lub unię możemy zaraz po jej deklaracji wypełnić ją określonymi danymi. Rozważmy tutaj przykład: struct moja_struct { int a; char b; } moja = {1,'c'};
W zasadzie taka deklaracja nie różni się niczym od wypełnienia np. tablicy danymi. Jednak standard C99 wprowadza pewne udogodnienie zarówno przy deklaracji struktur, jak i unii. Polega ono na tym, że w nawiasie klamrowym możemy podać nazwy pól struktury lub unii którym przypisujemy wartość, np.: struct moja_struct { int a; char b; } moja = {.b = 'c'}; /* pozostawiamy pole a niewypełnione żadną konkretną wartością */
Wspólne własności typów wyliczeniowych, unii i struktur Warto zwrócić uwagę, że język C++ przy deklaracji zmiennych typów wyliczeniowych, unii lub struktur nie wymaga przed nazwą typu słowa kluczowego typedef. Na przykład poniższy kod jest poprawnym programem C++: enum Enum { A, B, C }; union Union { int a; float b; }; struct Struct { int a; float b; }; int main() { Enum e; Union u; Struct s; e = A; u.a = 0; s.a = 0; return e + u.a + s.a; } Nie jest to jednak poprawny kod C i należy o tym pamiętać szczególnie jeżeli uczysz się języka C korzystając z kompilatora C++.
Częstym idiomem w C jest użycie typedef od razu z definicją typu, by uniknąć pisania enum, union czy struct przy deklaracji zmiennych danego typuDifference between 'struct' and 'typedef struct' in C++? . typedef struct struktura { int pole; } Struktura;
Struktura s1; struct struktura s2;
W tym przypadku zmienne s1 i s2 są tego samego typu, który ma 2 nazwy: pełną: struct struktura i skróconą: Struktura
Możemy też zrezygnować z pełnej nazwy struktury i pozostawić tylko skróconą: typedef struct { int pole; } Struktura;
Struktura s1;
Jeśli chcemy utworzyć strukturę rekurencyjną to wewnątrz struktury możemy użyć tylko nazwy długiej, nie krótkiej: typedef struct Wezel {// długa nazwa typu double re; double im; int level; struct Wezel *poprzedni; /* poprzedni węzeł */ } TWezel; // krótka nazwa typu
Należy również pamiętać, że po klamrze zamykającej definicje musi następować średnik. Brak tego średnika jest częstym błędem powodującym czasami niezrozumiałe komunikaty błędów. Jedynym wyjątkiem jest natychmiastowa definicja zmiennych danego typu, na przykład: struct Struktura { int pole; } s1, s2, s3; lub też definicja nowego typu, jak w poprzednim bloku kodowym (TWezel).
Definicja typów wyliczeniowych, unii i struktur jest lokalna do bloku. To znaczy, możemy zdefiniować strukturę wewnątrz jednej z funkcji (czy wręcz wewnątrz jakiegoś bloku funkcji) i tylko tam będzie można używać tego typu.
Wskaźnik na unię i strukturę Podobnie, jak na każdą inną zmienna, wskaźnik może wskazywać także na unię lub strukturę. Oto przykład: typedef struct { int p1, p2; } Struktura;
int main () { Struktura s = { 0, 0 }; Struktura *wsk = &s; wsk->p1 = 2; wsk->p2 = 3; return 0;
Zapis wsk->p1 jest (z definicji) równoważny (*wsk).p1, ale bardziej przejrzysty i powszechnie stosowany. Wyrażenie wsk.p1 spowoduje błąd kompilacji (strukturą jest *wsk a nie wsk).
Zobacz też Powszechne praktyki - konstruktory i destruktory
Pola bitowe Struktury mają pewne dodatkowe możliwości w stosunku do zmiennych. Mowa tutaj o rozmiarze elementu struktury. W przeciwieństwie do zmiennej może on mieć nawet 1 bit!. Aby móc zdefiniować taką zmienną musimy użyć tzw. pola bitowego. Wygląda ono tak: struct moja { unsigned int a1:4, /* 4 bity */ a2:8, /* 8 bitów (często 1 bajt) */ a3:1, /* 1 bit */ a4:3; /* 3 bity */
Wszystkie pola tej struktury mają w sumie rozmiar 16 bitów, jednak możemy odwoływać się do nich w taki sam sposób, jak do innych elementów struktury. W ten sposób efektywniej wykorzystujemy pamięć, jednak istnieją pewne zjawiska, których musimy być świadomi przy stosowaniu pól bitowych. Więcej na ten temat w rozdziale przenośność programów.
Pola bitowe znalazły zastosowanie głównie w implementacjach protokołów sieciowych.
Listy
rodzaje list: lista jednokierunkowa – w każdym elemencie listy jest przechowywane odniesienie tylko do jednego sąsiada (następnika lub poprzednika) lista dwukierunkowa – w każdym elemencie listy jest przechowywane odniesienie zarówno do następnika, jak i poprzednika elementu w liście. Taka reprezentacja umożliwia swobodne przemieszczanie się po liście w obie strony lista cykliczna – następnikiem ostatniego elementu jest pierwszy element. Po liście można więc przemieszczać się cyklicznie. Nie ma w takiej liście charakterystycznego ogona (ani głowy), często rozpoznawanego po tym, że jego następnik jest pusty (NULL) lista z wartownikiem – lista z wyróżnionym elementem zwanym wartownikiem. Jest to specjalnie oznaczony element niewidoczny dla programisty wykorzystującego listę. Pusta lista zawiera wtedy tylko wartownika. Zastosowanie wartownika znacznie upraszcza implementację operacji na listach.
Studium przypadku: lista jednokierunkowa