Ads_700x200

czwartek, 11 lipca 2013

STRUKTURY w C - wyjaśnijmy sobie dokładnie podstawy

Witam,

Jest sobie środek nocy, 1:48 a mnie naszło na napisanie mini poradnika na temat struktur w języku C, jak zwykle w aspekcie programowania mikrokontrolerów. Co było natchnieniem do tego poradnika? Szczególnie jeden z ostatnich maili jaki otrzymałem w tej sprawie, a tak na prawdę wyrażał on w skrócie wiele innych podobnych pytań docierających do mnie drogą mailową, przez Skype czy forum. Te wszystkie pytania wiążą się właśnie z niezrozumieniem podstaw definiowania i deklarowania struktur, dlatego mam nadzieję, że po tym co napiszę, opiszę - sporo się wyjaśni.




Zacytuję wręcz to pytanie z maila:

"W Twojej książce "Mikrokontrolery AVR Język C Podstawy programowania" deklarujesz struktury tak:

struct {
        uint8_t zmienna1;
        uint8_t zmienna2;
        ....;
} silnik;

Natomiast wszędzie w internecie spotkałem się z nieco innym zapisem:

struct silnik {
        uint8_t zmienna1;
        uint8_t zmienna2;
        ....;
} ;

Czym to sie różni?"

W odpowiedzi na tego maila napisałem co następuje ;)

To zależy jak i do czego potrzebujesz użyć struktur, bo obie zaprezentowane przez ciebie formy są poprawne ale wbrew pozorom nie oznaczają tego samego. Wyobraźmy sobie taki przykładowy zapis:

struct IMIONA {
        uint8_t ala;
        uint8_t ola;
} ;

jest to typowa (uwaga!) DEKLARACJA struktury ale jeszcze nie definicja, i teraz jeśli chcemy przygotować zmienną na podstawie takiej deklaracji to należy napisać tak:

struct IMIONA imiona;

Zobacz musisz użyć słówka "struct" przed nazwą struktury, dzięki temu popełniliśmy już teraz DEFINICJĘ, zmiennej "imiona", co spowodowało zarezerwowanie ( w tym przypadku ) pamięci RAM.

ale można dokładnie to samo uzyskać również tak:

struct IMIONA {
        uint8_t ala;
        uint8_t ola;
} imiona;

bo teraz "imiona" na końcu struktury, przed średnikiem to już DEFINICJA zmiennej typu strukturalnego "IMIONA". Jak widzisz - dwa ostatnie zapisy różnią się literalnie ale oznaczają to samo. Lubimy sobie jednak upraszczać życie, i w przypadku powyżej także moglibyśmy uprościć ten zapis do takiej formy, spójrz:

struct {
        uint8_t ala;
        uint8_t ola;
} imiona1, imiona2;

Czyli stworzyliśmy powyżej strukturę tzw "anonimową" bez nazwy, ale od razu przed kończącym średnikiem  podajemy nazwę albo jak widzisz nazwy (może ich być kilka) zmiennych, które chcemy utworzyć (uwaga!) od razu ZDEFINIOWAĆ, czyli zarezerwować także pamięć RAM. Myślę że to ważne wyjaśnienia ale...

... ale ja praktycznie bardzo rzadko korzystam z takich form posługiwania się strukturami, dlatego że często je wykorzystuję a nierzadko okazuje się, że w trakcie pisania kodu, potrzeba mi nagle wiele różnych zmiennych strukturalnych ... dlatego stosuję dostępne w języku C narzędzie jakim jest "typedef". Służy ono do tworzenia własnych typów zmiennych. A w przypadku struktur znakomicie ułatwia pracę z nimi i wprowadza sporo logiki oraz możliwości powtarzalności w kodzie. Stosując typedef, korzystam tylko z tzw nazwy skróconej struktury, czyli tej po zamkniętym nawiasie klamrowym, przed średnikiem. Ale najpierw zobaczmy jak to wygląda podstawowa wersja nowego typu strukturalnego, żeby wszystko było jasne:

typedef struct IMIONA {
        uint8_t ala;
        uint8_t ola;
} ;

Definiując nowy typ (a nie jest to równoznaczne z DEFINICJĄ zmiennej), nie musimy podawać nazwy po zamkniętym nawiasie klamrowym, chociaż moglibyśmy (ale o ty za chwilę), tyle że taka nazwa nie byłaby nadal DEFINICJĄ zmiennej, ponieważ definiujemy tutaj tylko nowy typ danych. No dobrze to jak teraz powołać do życia nową zmienną typu strukturalnego "IMIONA" ? Niestety w takim przypadku znowu musimy użyć słówka "struct" i wyjdzie na to samo o czym była mowa w pierwszym przykładzie, spójrz:

struct IMIONA imiona;

Bez tego słówka "struct" nie da rady w tak przygotowanej nowej definicji typu i tu nie widać przewagi o jakiej wspominam ;) Ale dla mnie byłoby, że tak powiem, uciążliwe ciągłe posługiwanie się tym słówkiem "struct", dlatego dobrze, że stwórcy C wymyślili możliwość wskazania tzw "NAZWY SKRÓCONEJ", dla nowo powoływanego do życia typu, z użyciem "typedef". Dlatego zobacz mogę to zrobić tak:

typedef struct IMIONA {
        uint8_t ala;
        uint8_t ola;
} TIMIONA;

Widzisz? dodałem za nawiasem klamrowym skróconą nazwę "TIMIONA", nie musi ona być różna od jawnej nazwy struktury, w tym przypadku "IMIONA". I teraz UWAGA! .... (najlepsze nadchodzi) ;) ... Zobacz jak teraz może wyglądać w kodzie programu DEFINICJA nowej zmiennej tego typu:

TIMIONA imiona;

Nie musiałem użyć słówka "struct", mniej pisania ;) ... i jak dla mnie przejrzyście. Ale skoro można tak, to już pewnie się domyślasz, że tak jak na początku mówiłem, można pozbyć się także pełnej nazwy tej struktury "IMIONA" czyli będzie anonimowa ? NIE, będziemy się posługiwać nazwą skróconą. Kolejne uproszczenie może zatem wyglądać tak:

typedef struct {
        uint8_t ala;
        uint8_t ola;
} TIMIONA;

i dalej tak samo DEFINICJA np, kilku nowych zmiennych:

TIMIONA imiona1, imiona2, imiona3;

Przyznasz chyba że to DUŻO wygodniejsze w pisaniu?

Wszystko dobrze, ale czy to oznacza, że możemy zapomnieć raz na zawsze o korzystaniu także z pełnej nazwy struktury, gdy definiujemy nowy typ za pomocą "typedef" ?

NIE, specjalnie podawałem od podstaw jaką drogą się tworzy to wszystko i ew skraca, żebyś wiedział w pewnych specyficznych przypadkach, jak tego użyć. Proszę bardzo najlepiej omówić to na żywym przykładzie dla lepszego zrozumienia. Wyobraź więc sobie, że chcemy tym razem powołać nowy typ strukturalny za pomocą "typedef" ale chcielibyśmy w ramach tej struktury umieścić wskaźnik, dokładnie takiego samego typu strukturalnego. Załóżmy że zaczynamy tworzyć nie mając pełnej wiedzy takiego potworka:

typedef struct {
        uint8_t ala;
        uint8_t ola;
        TIMIONA * zbior_imion; // -- tu próbujemy dać typ
} TIMIONA;

Ale jest wielki problem, ponieważ kompilator przy kompilacji od początku pliku linia po linii, napotyka nasz wskaźnik i niestety :( przykro mi ale nie zna jeszcze nazwy TIMIONA, ponieważ ona pojawi się dopiero w kolejnej linii, więc wywali BŁĄD KOMPILACJI, i koniec. No to wydaje się, że wpadliśmy w pułapkę ? Nie, na szczęście przypomina nam się fakt, że możemy jeszcze posłużyć się pełną nazwą struktury, definiując nowy typ, o tak:

typedef struct IMIONA {
        uint8_t ala;
        uint8_t ola;
        struct IMIONA * zbior_imion; // posługujemy się teraz typem z nazwy pełnej
} TIMIONA;

Jak widzisz, szczęśliwie udało nam się wybrnąć z wydawać by się mogło patowej sytuacji. Dla określenia typu wskaźnika, nadaliśmy poza nazwą skróconą "TIMIONA" także pełną nazwę "IMIONA", dzięki której również można powołać nową zmienną ale pamiętając o słówku "struct". Udało się - BINGO! ;)

Innym przykładem tego zagadnienia może być rozwiązanie zagadki z mojego poradnika o grze SNAKE na LCD - oglądałeś ten poradnik ? jak nie to obejrzyj:

http://mirekk36.blogspot.com/2012/12/wskazniki-struktury-callbacki-jazda-bez.html

Tam chcąc z kolei wyjaśnić dobrze posługiwanie się pustym typem "void" wyjaśnić różne meandry jego zastosowania, i przy okazji pokazałem jeszcze kolejną możliwość obejścia tego kłopotu, o którym mowa wyżej, a można byłoby to zrobić mniej więcej tak, w naszych przykładach:

typedef struct {
        uint8_t ala;
        uint8_t ola;
        void* zbior_imion; // -- użyliśmy pustego typu wskaźnikowego void*
TIMIONA;

Dzięki czemu później, gdybyśmy w kodzie chcieli podstawić jakąś zmienną pod ten wskaźnik, np kolejną zmienną imiona:


TIMIONA imiona1, imiona2;

imiona1.zbior_imion = &imiona2;

I od tego momentu, wskaźnik "zbior_imion" byłby już nie typu "void" a typu TIMIONA. Zapamiętaj! jeśli się jednak da zrobić coś bez rzutowania na typ wskaźnikowy "void*" to zawsze wybierz tę pierwszą opcję, a rzutowania na typ "void*" staraj się unikać i zostawiać na najtrudniejsze do jawnego rzutowania przypadki.

To tyle. Dla jednych pewnie będzie to tylko tyle ale dla innych aż tyle. Ja jestem zdania, że warto sobie tak podsumować podstawową wiedzę o strukturach, bo brak tej wiedzy skutkuje później nie tylko ogromnymi problemami podczas pisania kodu z ich użyciem ale co gorsze, zniechęca w ogóle do wykorzystywania struktur na co dzień ;) .... gdy tymczasem są to PRZEPIĘKNE mechanizmy w języku C, szczególnie w połączeniu z uniami, wskaźnikami i tablicami.

Na koniec małe uzupełnienie, odpowiedzmy sobie na dwa pytania:

1. czy nazwę struktury lub nazwę skróconą trzeba pisać dużymi literami?

Oczywiście, że nie, jednak dla mnie jest to o wiele bardziej przejrzysty sposób

2. dlaczego do skróconej nazwy podczas definicji nowego typu, dodaję literkę T na początku?

Po to aby w kodzie szybko ocenić wzrokiem później, że chodzi o nowo powołany przeze mnie TYP

są to więc jak widzisz czysto praktyczne zasady ;)

Jeśli będziesz miał(-a) jeszcze jakieś pytania do tego co tu opisałem - to śmiało pisz, pytaj w tym artykule ;)

Na koniec - gorąco polecam uzupełnienie do tego poradnika w postaci mini kursu pod tym linkiem na naszym forum:


;)

25 komentarzy:

  1. Co oznacza ta linijka imiona1.zbior_imion = (znak AND - amperstand)imiona2;?

    OdpowiedzUsuń
    Odpowiedzi
    1. Spróbuj sobie na blogu w tekście wstawić znak & i zobacz co się stanie ? ;) to znak formatujący tekst i się rozjeżdża

      oczywiście że chodzi o

      imiona1.zbior_imion = &imiona2;

      tutaj nie wiem czy się rozjedzie tekst jak to napiszę - może nie - aobaczymy

      znak AND (amperstand) to przecież operator pobierania adresu w C

      Usuń
  2. Tomek dziękuję ci BARDZO! ;) zrobiłem wg tej metody, którą poleciłeś i zadziałało .... widzisz? ... taki ze mnie html-owiec jak .... no właśnie ;)

    Dzięki za cenną poradę.

    OdpowiedzUsuń
  3. Widzę :) W razie co zawsze służę pomocą.

    OdpowiedzUsuń
  4. Zapamiętam ;) Człowiek uczy się całe życie ;)

    OdpowiedzUsuń
  5. czy istnieje możliwość aby Pan, te artykuły, które publikuje, udostępniał także w formacie pdf do pobrania?

    OdpowiedzUsuń
  6. Jest chyba taki widget, który zamienia artykuł z Bloga na pdf i wysyła go na maila. Tylko nie bardzo widzę w tym sens.

    OdpowiedzUsuń
  7. Witam,

    Fajny artykuł. Mógłby Pan jeszcze dodać połączenie struktur z funkcjami, gdy funkcja przyjmuje jako parametr strukture.

    Jeszcze zastanawia mnie użycie wskaźnika, gdy jest w środku struktury.

    OdpowiedzUsuń
    Odpowiedzi
    1. ależ wszystko jest i to na tym blogu ;) ... trzeba troszkę poszperać w kąciku C

      np:

      http://mirekk36.blogspot.com/2012/12/wskazniki-struktury-callbacki-jazda-bez.html

      Usuń
  8. Rewelacyjny artykuł. Czy możesz mi napisać jak zadeklarować taką strukturę w plik.h jako extern by mieć do niej dostęp globalny.

    OdpowiedzUsuń
    Odpowiedzi
    1. Tak skorzystaj w *.h z możliwości utworzenia nowego typu za pomocą typedef. Czyli to o czym piszę na samym końcu artykułu. Potem taki plik *.h możesz dołączać do różnych plików *.c dzięki czemu zawsze będzie on widoczny. I tak samo możesz przez pliki *.h rozprzestrzeniać już samą strukturę poprzez jej deklarację ze słówkiem extern. Dokładnie tak samo jak to się dzieje ze zmiennymi.

      Ciężko to tak opisać tu w odpowiedzi na blogu, jak coś to wpadnij na nasze przyjazne forum (zpraszam)

      www.forum.atnel.pl

      to omówimy to na przykładzie - albo zajrzyj do książki:

      http://atnel.pl/mikrokontrolery-avr-jezyk-c.html

      jeśli akurat ją posiadasz i zobacz jak tam opisuję to w przypadku zwykłych zmiennych.

      Usuń
  9. W jaki sposób można zapisać dane do tabeli z strukturą,


    //struktura
    typedef struct {
    uint8_t a;
    uint8_t b;
    uint8_t c;
    uint8_t d;
    }T_ZBIOR;

    //przykładowe dane w strukturze
    zbior[0].a=a;
    zbior[0].b=b;
    zbior[0].c=a+b;
    zbior[0].d=156;

    czy jest jakiś prostszy sposób uzupełnienia tabeli, niż przypisując każdy element struktury z osobna, jak to jest zrobione wyżej.

    OdpowiedzUsuń
    Odpowiedzi
    1. Ale co to znaczy dla ciebie prostszy sposób ?

      Jeśli chcesz mieć w każdym polu tablicy struktur (a mam nadzieję że miałeś na myśli taką np definicję)

      T_ZBIOR zbior[ n ];

      i każde pole każdej struktury w tej tablicy ma mieć inne wartości (unikalne) to doprecyzuj co masz na myśli pisząc "prostszy sposób" ?

      Usuń
    2. jak uzupełnić tabele z typem strukturalnym nie wypisując nazwę tabeli[x].element struktury. Tylko na przykład
      tabela[x]={el.st1, el.st2, ...}.

      Usuń
    3. A skąd ci przyszło do głowy wpisywać nazwy elementów tablicy no coś ty ? tak się nigdy nie robi
      struct {
      uint8_t a;
      uint8_t b;
      uint8_t c;
      } TTAB;

      TTAB tab[2] = {

      {2,3,4}, // struktura / element 1
      {5,8,1} // struktura / element 2

      };

      Usuń
    4. Dzięki jesteś Wielki :)

      Usuń
  10. Gratuluje i dziękuje, świetny artykuł :)

    OdpowiedzUsuń
  11. Jak zwykle artykuł na najwyższym poziome - wszystko jasno i przystępnie!
    Mam jednak takie pytanie. Tworzę program, którego projekt chciałbym podzielić na następujące pliki - main.c - główny plik programu, func.c, func.h - definicje i deklaracje funkcji, macro.h - wszelkie makra, var.h - wszelkie zmienne. Teraz w func.c chciałbym napisać funkcję, do której chcę przekazać wskaźnik na strukturę, o tak:
    void PID_control(PIDTYPE * str)
    W którym pliku i w jaki sposób powinienem zdefiniować nowy typ PIDTYPE? Czy powinno to wyglądać, tak?
    typedef struct PIDTYPE{
    };
    PID TYPE nowa_struktura;

    Chciałbym umieścić definicję i deklarację w pliku var.h, ale tak żebym mógł stworzyć funkcję wykorzystująca ten typ w pliku func.c.

    Mam nadzieję, że dobrze wyjaśniłem mój problem. Generalnie chodzi mi o zachowanie maksymalnej przejrzystości i porządku w programie.

    OdpowiedzUsuń
    Odpowiedzi
    1. panie kolego w języku C definicje zmiennych umieszcza się w plikach *.c a nie to co ty tu planujesz w pliku *. chcesz utworzyć definicję zmiennej tego typu strukturalnego.

      reasumując:

      typedef struct {
      } PIDTYPE;

      TAKĄ definicję TYPU (uważaj - definicję TYPU a nie definicję zmiennej!) umieszczasz w swoim pliku *.h !!!

      taką DEFINICJĘ zmiennej:

      PIDTYPE nowa_struktura;

      umieszczasz w swoim pliku *.c

      i teraz uważaj pan jeszcze raz!!! ....

      zaś DEKLARACJĘ zmiennej - taką:

      extern PIDTYPE nowa_struktura;

      umieszczasz w swoim pliku func.h

      jasne ? (czyli zarówno DEFINICJA TYPU jak i DEKLARACJA zmiennej siedzą w *.h a tylko DEFINICJA zmiennej w *.c) i QNIEC

      Usuń
  12. Witam,
    mam pytanie czy w tym fragmencie, w miejscu, które oznaczyłem gwiazdkami nie brakuje słówka "struct"?:

    "
    typedef struct IMIONA {
    uint8_t ala;
    uint8_t ola;
    } TIMIONA;

    Widzisz? dodałem za nawiasem klamrowym skróconą nazwę "TIMIONA", musi ona być różna od jawnej nazwy struktury, w tym przypadku "IMIONA". I teraz UWAGA! .... (najlepsze nadchodzi) ;) ... Zobacz jak teraz może wyglądać w kodzie programu DEFINICJA nowej zmiennej tego typu:

    **** TIMIONA imiona;
    "

    OdpowiedzUsuń
    Odpowiedzi
    1. No sorki ale skoro definiujemy nowy typ TIMIONA to po jakiego grzyba pisać struct ? Coś kolega albo przeoczył słówko typedef, albo nie wie co ono znaczy ? ... albo nie wiem co ...

      Usuń
    2. oczywiście nie brakuje słowa struct w miejscu twoich gwiazdek ;)

      Usuń
  13. Dopiero zaczynam naukę C na AVR-ów i jak dla mnie artykuł rewelacja, super uzupełnienie do książki. Pisz Mistrzu tak dalej.

    OdpowiedzUsuń
  14. typedef to ZUO bo znakomicie utrudnia zrozumienie programu. A już szczególnie paskudny jest typedef wskaźnika.
    typedef struct {
    int pole;
    int orne;
    } *PTR2PB;

    PTR2PB pb;

    Brrrrrrr.... :-(

    OdpowiedzUsuń