Popularny post Gieneq Napisano Sierpień 31, 2021 Popularny post Udostępnij Napisano Sierpień 31, 2021 (edytowany) W ostatniej części omówiłem typowe sposoby tworzenia tablic i skończyliśmy na omówieniu koncepcji skakania pomiędzy zmiennymi posługując się operacjami na wskaźnikach. W tej części wykorzystamy tę koncepcję i potraktujemy tablicę jako wskaźnik, dzięki czemu możliwe będzie przyspieszenie niektórych operacji. Spis treści #1 – wstęp i praktyka #2 – organizacja pamięci #3 – tablice w pamięci #4 – tablica jest wskaźnikiem (czytasz ten artykuł) #5 – znaki, cstring #6 – argumenty funkcji #7 – przykład #8 – tablice wielowymiarowe #9 – tablice dynamiczne #10 – zakończenie Adres tablicy W jednym z ostatnio omawianych przykładów, wypisaliśmy adresy elementów tablicy: #define LOCKERS_COUNT 5 int lockers[LOCKERS_COUNT] = {1, 0, 1}; void setup() { Serial.begin(9600); for (int i = 0; i < LOCKERS_COUNT; i++) { Serial.print(F("0x")); Serial.println((int)&lockers[i], HEX); } } W wyniku możemy otrzymać takie dane: 0x100 0x102 0x104 0x106 0x108 Nigdzie jednak nie podaliśmy jaki jest adres samej tablicy, dlatego pora na najważniejszą informację dotycząca tablic: Nazwa tablicy jest zarazem adresem jej początkowego (zerowego) elementu. Tablica nie jest typem danych wiec nie ma adresu, jej adresem jest adres zerowego elementu, a nazwa jako adres obszaru pamięci może być stosowana wymiennie. W praktyce oznacza to że oba zapisy są poprawne: int *addr_1 = lockers; int *addr_2 = &lockers[0]; Można sprawdzić czy adresy są faktycznie takie same wypisując je: 0x100 0x100 To samo a jednak różne Czy skoro uzyskaliśmy ten sam adres w pamięci, czy można dojść do wniosku, że nazwa tablicy i wskaźnik są tym samym? Choć w obu przypadkach uzyskamy informację o adresie, to nazwa tablicy jest nieco bogatsza w informacje. Prześledźmy to na przykładzie, tylko tym razem użyjemy tablicy 1 B zmiennych char: char tab[5] = {'a', 'b', 'c', 'd', 'e'}; void setup() { Serial.begin(9600); char *addr = tab; Serial.print(F("Sizeof(tab): ")); Serial.println(sizeof(tab)); Serial.print(F("Sizeof(addr): ")); Serial.println(sizeof(addr)); Serial.print(F("Sizeof(&tab[0]): ")); Serial.println(sizeof(&tab[0])); Serial.print(F("Sizeof(tab[0]): ")); Serial.println(sizeof(tab[0])); } Użyty tu operator sizeof() zazwyczaj działa na zmiennej lub jej typie (jawnie wpisanej nazwie typu danych) i zwraca rozmiar zmiennej. Jednak w przypadku tablicy jest nieco inaczej, bo operator zwróci rozmiar obszaru zajmowanego przez tablicę: Sizeof(tab): 5 Sizeof(addr): 2 Sizeof(&tab[0]): 2 Sizeof(tab[0]): 1 Uzyskana wartość 5 oznacza, że tablica 1 B elementów int zajmuje 5 × 1 B pamięci. Zatem pierwsza różnica polega na tym jakie są dodatkowe informacje. Tablica niesie informację o całym obszarze pamięci, a wskaźnik niesie informację tylko o adresie, stąd używając operatora sizeof() uzyskaliśmy rozmiar ale nie wskazywanej wartości, tylko zmiennej typu danych wskaźnika – adres 2 B (16 bitowy) – w końcu adres musi być jakoś przechowany w pamięci. Dysponując tablicą jesteśmy w stanie uzyskać rozmiar bloku pamięci, rozmiar typu danych i nawet odtworzyć rozmiar tablicy wyrażony w liczbie elementów: int tab[] = {1, 2, 3, 4, 5}; int tab_len = sizeof(tab) / sizeof(tab[0]); // wynik 5 lub posługując się makrem: #define ARRLEN(x) (sizeof(x)/sizeof(x[0])) int tab_len = ARRLEN(tab); W prostych zadaniach najpewniej rozmiar tablicy (zwłaszcza statycznej) będzie znany więc takie obliczenia będą niepotrzebne. Jest to jednak dobre ćwiczenie na użycie operatora sizeof(), który może się przydać w zadaniach, gdzie trzeba wyznaczyć rozmiar bloku pamięci. Przykładem może być fragment funkcji z biblioteki Arduino: size_t Print::printNumber(unsigned long n, uint8_t base) { char buf[8 * sizeof(long) + 1]; // Assumes 8-bit chars plus zero byte. char *str = &buf[sizeof(buf) - 1]; ... Jak widać operator sizeof() pozwala wyznaczyć rozmiar bufora zwalniając programistę z wiedzy o rozmiarze typu danych long. Innym przykładem użycia może być złożony obiekt jakim jest klasa wektor (taka nieco lepsza tablica dynamiczna). Nie dość, że jako szablon (ang. template) może przyjąć dowolny typ danych (w tym obiekty klas) to może zmieniać swój rozmiar w czasie działania. Implementacja tak złożonego obiektu nie mogłaby obyć się bez tej wiedzy. Druga różnica tyczy się próby zmiany adresu. Jak przekonaliśmy się w poprzedniej części da się dodać coś do wskaźnika i w konsekwencji odwołać się do innego adresu. Mając wskaźnik uzyskany z tablicy jest to możliwe: int *addr = tab; addr++; //ok Ale próbując tego samego na tablicy już się nie uda: tab++; // błąd Dzieje się tak ponieważ spowodowałoby to utratę informacji o faktycznym adresie tablicy. Zatem wracając do regułki z początku można ją wzbogacić uzyskując trafniejszą definicję: Nazwa tablicy jest zarazem niezmiennym adresem jej początkowego (zerowego) elementu. Warto znać różne sposoby. Nawet jeżeli sami ich tak szybko nie użyjemy, to przeglądając kody programów bibliotek na pewno trafimy na takie przypadki. Dlatego teraz przećwiczymy alternatywny sposób poruszania się po tablicach – przy pomocy adresów. Indeksowanie bez nawiasów Skoro tablica jest dobrodziejstwem, w którym możemy śmiało skakać wskaźnikami bez obawy, że (w obrębie tablicy) coś naruszymy to warto z tego skorzystać. Zdefiniujmy więc tablicę, adres do niej i przy pomocy pętli prześledźmy jej wartości: int tab[5] = {1, 2, 3, 4, 5}; int *addr = tab; void setup() { Serial.begin(9600); for (int i = 0; i < 5; i++) Serial.println(*addr++); } W wyniku otrzymamy to co podaliśmy przy inicjalizacji tablicy: 1 2 3 4 5 Przy inicjalizacji tablicy co prawda posługujemy się nawiasami kwadratowymi (choć da się inaczej), ale reszta jest już inna. Co właściwie oznacza ten zapis? Mamy tu pewne uproszczenie – w jednej linii realizujemy pobranie wskazywanej wartości i operację postinkrementacji (zwiększenie zmiennej, ale dopiero po odczytaniu jej wartości). Używając notacji z nawiasami kwadratowymi można posłużyć się zmienną i, czy w tym zapisie jest to możliwe? Tak, jak najbardziej: for (int i = 0; i < 5; i++) Serial.println(*(tab + i)); Dodanie czegoś do tablicy nie narusza jej niezmiennego adresu, zwracany wynik jest nowym adresem, z którego odczytujemy wskazywaną wartość. Na razie wygląda to jakbyśmy uczyli się dokładnie tego samego zapisu ale w trochę przekombinowany sposób. Okazuje się, że nie jest on taki wcale zbędny. Są sytuacje w których będzie on łatwiejszy i szybszy od odpowiednika z typowymi tablicami, zwłaszcza gdy będziemy chcieli odwołać się do pewnych obszarów tablicy i wykonać na nich działania. Dygresja o dodawaniu (i odejmowaniu też) Jak można było zauważyć posłużyłem się pojęciem postinkrementacji. Sama inkrementacja, czyli zwiększenie zmiennej o 1 jest szerszym pojęciem pod którym kryją się 2 operacje o nieco innym sposobie dojścia do tego samego efektu: preinkrementacja i postinkrementacja: preinkrementacja (++v) dokonuje bezpośredniego zwiększenia zmiennej, w efekcie przyległe operacje działają na zwiększonej zmiennej, postinkrementacja (v++) dokonuje zwiększenia zmiennej, ale na tymczasowej kopii, w efekcie przyległe operacje działają na wartości oryginalnej (niezwiększonej), a po zakończeniu działań zmienna jest modyfikowana. Do czego to może służyć? Tak jak w poprzednim przykładzie, świadomie wypisaliśmy zmienną, aby w tej samej linii ją zwiększyć, ale dopiero po wykonaniu przyległej operacji dereferencji *. Poza kwestią funkcjonalną, operacje te różnią się szybkością wykonania, ta pierwsza z racji bezpośredniego działania na pamięci jest szybsza. Czy zatem pisanie ++i jest szybsze niż i++ w pętli for? Nie... jeżeli mamy włączoną optymalizację. W przypadku pętli for kolejność inkrementacji nie jest tak istotna, bo nie odwołujemy się podczas niej do wartości. Kompilator zoptymalizuje te działanie do szybszej wersji. Podobnie samodzielna operacja postinkrementacji zostanie zoptymalizowana do szybszego wariantu. Oczywiście na podobnej zasadzie działają operacje dekrementacji (zmniejszania). Porównanie wskaźników Skoro wiemy, że wskaźnik jest w 8 bitowych mikrokontrolerach AVR liczbą 16 bitową, to pewnie inne operacje na wskaźnikach też są możliwe. Bez problemu możemy użyć operatora porównania == a także różne warianty operatorów nierówności. Prześledźmy to na przykładzie: int tab[5] = {1, 2, 3, 4, 5}; int *addr = tab; void setup() { Serial.begin(9600); Serial.println(tab == &tab[0] ? F("Equal") : F("Different")); for (int *addr = tab; addr < &tab[4]; addr++) Serial.println(*addr); } W wyniku otrzymamy: Equal 1 2 3 4 W pierwszej linii wypisaliśmy wynik porównania dwóch wskaźników i jak już wiemy wskaźnik od nazwy tablicy i wskaźnik elementu zerowego są sobie równe. W pętli for przeprowadziliśmy iterację posługując się wskaźnikiem w miejscu typowej zmiennej int i. Jak widać przy każdym obiegu pętli następuje sprawdzenie, czy adres jest mniejszy od adresu ostatniego elementu w tablicy. Stąd wniosek – adresy można ze sobą porównywać. Posługując się przykładem to tak jakbyśmy szli wzdłuż ulicy i szukali konkretnego budynku. Widząc budynki o niższych adresach wiemy, że powinniśmy iść zgodnie z rosnącymi adresami i odwrotnie. Podsumowanie W tej części dotknęliśmy bardzo ciekawy temat, który często budzi strach u początkujących programistów. Przemieszanie gwiazdek, ampersandów, plusów i minusów już nie powinien stanowić problemu. W kolejnych częściach przejdziemy do bardzo popularnego zastosowania tablic – przechowywania ciągów znaków (napisów) czyli tzw. stringów. Edytowano Październik 29, 2021 przez Gieneq 5 Link do komentarza Share on other sites More sharing options...
Popularny post Gieneq Wrzesień 27, 2021 Autor tematu Popularny post Udostępnij Wrzesień 27, 2021 Ta część kursu może przydać się początkującym programistom, którzy starają się omijać podejrzanie wyglądające notacje z gwiazdkami 🌟 Nie jest to takie trudne, wiec zachęcam do lektury 🙂 3 Link do komentarza Share on other sites More sharing options...
Pomocna odpowiedź
Bądź aktywny - zaloguj się lub utwórz konto!
Tylko zarejestrowani użytkownicy mogą komentować zawartość tej strony
Utwórz konto w ~20 sekund!
Zarejestruj nowe konto, to proste!
Zarejestruj się »Zaloguj się
Posiadasz własne konto? Użyj go!
Zaloguj się »