Skocz do zawartości

Tablice w Arduino – #4 – tablica jest wskaźnikiem


Pomocna odpowiedź

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.

rysunki_5.thumb.jpg.b828aae2e1819c7e39597b80375b947e.jpg

Spis treści

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

image.thumb.png.cb443f88e0a5537f88b04bb9ea1b9c9d.png

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.

t6.thumb.png.6c020d1862f92e6fcd685709e45be919.png

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).

image.thumb.png.c787cdaf8eeb378a0fd9e3d73e1ba257.png

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.

image.thumb.png.d4222a15efd84a52fd7021522e2929f3.png

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 przez Gieneq
  • Lubię! 1
Link to post
Share on other sites
  • Gieneq zmienił tytuł na: Tablice w Arduino – #4 – tablica jest wskaźnikiem

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 🙂 

Link to post
Share on other sites

Dołącz do dyskusji, napisz odpowiedź!

Jeśli masz już konto to zaloguj się teraz, aby opublikować wiadomość jako Ty. Możesz też napisać teraz i zarejestrować się później.
Uwaga: wgrywanie zdjęć i załączników dostępne jest po zalogowaniu!

Anonim
Dołącz do dyskusji! Kliknij i zacznij pisać...

×   Wklejony jako tekst z formatowaniem.   Przywróć formatowanie

  Dozwolonych jest tylko 75 emoji.

×   Twój link będzie automatycznie osadzony.   Wyświetlać jako link

×   Twoja poprzednia zawartość została przywrócona.   Wyczyść edytor

×   Nie możesz wkleić zdjęć bezpośrednio. Prześlij lub wstaw obrazy z adresu URL.

×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.