Serwis Edukacyjny w I-LO w Tarnowie Materiały dla uczniów liceum |
Wyjście Spis treści Wstecz Dalej
Autor artykułu: mgr Jerzy
Wałaszek |
©2024 mgr Jerzy Wałaszek
|
Małe mikrokontrolery AVR i PIC posiadają od kilkudziesięciu komórek do kilku... kilkunastu tysięcy. Nie jest to dużo, jednak mikrokontrolery są zwykle używane do rozwiązywania prostych zadań i tyle pamięci przeważnie wystarcza (zawsze można zastosować bardziej zaawansowany mikrokontroler, a niektóre modele pozwalają dołączać pamięć zewnętrzną). Niemniej bez względu na liczbę dostępnych komórek pamięci, budowa pamięci jest podobna we wszystkich mikrokontrolerach:
W tablicy mieliśmy indeksy komórek, w pamięci mamy adresy. Jednak znaczenie obu jest takie samo: mają wybrać określoną komórkę do operacji. W tablicy komórki przechowywały informację ściśle określonego typu. W przypadku pamięci jest nieco inaczej. Niektóre dane mogą zajmować więcej niż jedną komórkę pamięci. W języku C wprowadza się zatem pojęcie adresu obiektu (np. zmiennej). Jeśli obiekt zajmuje w pamięci kilka komórek, to jego adres jest zawsze adresem pierwszej zajmowanej komórki:
Adres zmiennej nazywamy w języku C wskaźnikiem (ang. pointer) lub wskazaniem (ang. reference). Wskaźnik "wskazuje" pierwszą komórkę obszaru pamięci, w którym przechowywana jest zmienna. Adres zmiennej możesz zawsze otrzymać przy pomocy operatora adresu &. Uruchom poniższy program:
/* Wskaźniki (C)2016 mgr Jerzy Wałaszek Data utworzenia: 9.10.2016 */ #include <stdio.h> #include <stdlib.h> #include <locale.h> int main() { char a,b,c; int d,e,f; setlocale(LC_ALL,""); printf("Adres zmiennej a: %p\n",&a); printf("Adres zmiennej b: %p\n",&b); printf("Adres zmiennej c: %p\n",&c); printf("Adres zmiennej d: %p\n",&d); printf("Adres zmiennej e: %p\n",&e); printf("Adres zmiennej f: %p\n",&f); return 0; } |
W programie zastosowaliśmy nowy format: %p, który oznacza, że skojarzony z nim parametr jest adresem, wskaźnikiem. Wskaźniki są wyświetlane przez funkcję printf jako liczby szesnastkowe. Konkretne wartości adresów są nam zwykle niepotrzebne, ponieważ i tak na różnych komputerach mogą być inne, jednak same adresy mogą być bardzo użyteczne, co zobaczysz dalej w tym rozdziale. Zwróć uwagę, że adresy zmiennych a, b i c są rozłożone co 1, ponieważ typ char zajmuje w pamięci tylko 1 bajt, czyli jedną komórkę. Adresy zmiennych d, e i f są rozłożone co 4, ponieważ typ int (w IBM-PC, w mikrokontrolerach może to być np. 2 bajty) zajmuje 4 bajty.
Definicja zmiennej, która zawiera wskaźnik, jest następująca:
typ * nazwa; |
W porównaniu ze zwykłą zmienną dochodzi tutaj znak gwiazdki, który właśnie informuje kompilator, że dana zmienna będzie zawierała adres obiektu podanego typu:
char * pa; // wskaźnik do danej typu char int * pb; // wskaźnik do danej typu int unsigned * pc; // wskaźnik do danej typu unsigned int float * pd; // wskaźnik do danej typu float |
Istnieje również specjalny typ znacznika, który nie wskazuje żadnego konkretnego typu obiektu (to właśnie jest odpowiednik czystego adresu komórki):
void * p; // adres komórki pamięci |
Czasem taki wskaźnik jest przydatny.
Zmienną wskaźnikową możemy używać w wyrażeniach jako wskazywany przez nią obiekt, jeśli poprzedzimy jej nazwę znakiem gwiazdki.
Napiszmy kilka programów, które pokażą cechy znaczników.
/* Wskaźniki (C)2016 mgr Jerzy Wałaszek Data utworzenia: 910 */ #include <stdio.h> #include <stdlib.h> #include <locale.h> int main() { int * p; // wskaźnik do zmiennej typu int int a,b; setlocale(LC_ALL,""); a = 10; b = 99; p = &a; // p wskazuje zawartość a printf("a = %d\n", * p); p = &b; printf("b = %d\n", * p); return 0; } |
W programie tworzymy wskaźnik p do danych typu int. Następnie tworzymy dwie zmienne typu int i nadajemy im różne wartości. Teraz w p umieszczamy adres zmiennej a (za pomocą operatora adresu &). Zwróć uwagę, że w pierwszej funkcji printf odwołujemy się do danej wskazywanej przez p (za pomocą operatora *). Ponieważ p wskazuje na a, to zostanie wyświetlona zawartość zmiennej a. Dalej w p umieszczamy adres zmiennej b. W drugiej funkcji printf odwołujemy się ponownie do danych wskazywanych przez p, jednak teraz p wskazuje już inną zmienną. W efekcie zostanie wyświetlona zawartość zmiennej b.
Jeśli wskaźnik zawiera adres o wartości 0, to oznacza to, że nie wskazuje żadnego obiektu. Jest to tzw. wskaźnik zerowy (ang. null pointer).
Ten przykład pokazuje, jak umieścić adres zmiennej we wskaźniku. Jednakże adres musi być odpowiedniego typu, inaczej zostanie zgłoszony błąd. Spróbuj uruchomić poniższy program:
/* Wskaźniki (C)2016 mgr Jerzy Wałaszek Data utworzenia: 10.10.2016 */ #include <stdio.h> #include <stdlib.h> #include <locale.h> int main() { int * p; // wskaźnik do zmiennej typu int float a,b; setlocale(LC_ALL,""); a = 10; b = 99; p = &a; // p wskazuje zawartość a printf("a = %d\n", * p); p = &b; printf("b = %d\n", * p); return 0; } |
Przy kompilacji pojawią się ostrzeżenia dla wierszy zawierających polecenia:
p = &a; p = &b; |
Ostrzeżenie brzmi:
warning: assignment from incompatible pointer type ostrzeżenie: przypisanie niekompatybilnego typu wskaźnika |
O co tutaj chodzi? We wskaźniku do typu int umieszczamy wskaźnik do typu float. Komputer nie zablokował tych operacji, ponieważ wychodzi z założenia, że wiesz co robisz. Jednak ostrzega cię, że nie jest to standardowa operacja i może prowadzić do błędów. I prowadzi: liczba typu float jest inaczej zapisywana w pamięci od liczby typu int i na wydruku otrzymasz bezsensowne wartości – to pouczający przykład, że typy są jednak potrzebne: dzięki nim komputer wie jak interpretować informację zapisaną ciągiem bitów.
Kolejny program wykorzystuje wskaźnik do zapisania wartości w określonym miejscu pamięci:
/* Wskaźniki (C)2016 mgr Jerzy Wałaszek Data utworzenia: 10.10.2016 */ #include <stdio.h> #include <stdlib.h> #include <locale.h> int main() { float * p; // wskaźnik do zmiennej typu float float a,b; setlocale(LC_ALL,""); p = &a; // p wskazuje zmienną a *p = 1.55; // w a umieszczamy 1.55 p = &b; // p wskazuje zmienną b *p = 9.99; // w b umieszczamy 9.99 printf("a = %4.2f b = %4.2f\n\n",a,b); return 0; } |
W tym programie zwróć uwagę na dwie instrukcje:
p = &a; // odwołuje się do zmiennej p *p = 1.55; // odwołuje się do obiektu wskazywanego przez p |
Zapamiętaj: jeśli nazwę wskaźnika poprzedzisz znakiem gwiazdki *, to otrzymasz wskazywany obiekt. Bez tego znaku odwołujesz się do adresu przechowywanego w zmiennej wskaźnikowej. W tym programie *p oznacza raz zmienną a, a później zmienną b. Dlatego wprowadzane liczby trafiają kolejno do zmiennych a i do b.
Ponieważ wskaźniki są adresami obiektów w pamięci komputera, nie ma specjalnie sensu ich mnożenie, dzielenie czy pierwiastkowanie. Istnieje jednak kilka operacji arytmetycznych, które dla wskaźników są bardzo użyteczne i często stosowane przez programistów.
Przypomnijmy, tablica jest ciągiem elementów tego samego typu, które w pamięci komputera są ułożone obok siebie. Jeśli ustawimy wskaźnik na adres określonego elementu tablicy, to zwiększenie wskaźnika operatorem ++ spowoduje, że wskaźnik zacznie wskazywać kolejny element tablicy. Podobnie zmniejszenie wskaźnika operatorem -- spowoduje, że wskaźnik zacznie wskazywać element poprzedni w tablicy.
Uruchom program:
/* Wskaźniki (C)2016 mgr Jerzy Wałaszek Data utworzenia: 11.10.2016 */ #include <stdio.h> #include <stdlib.h> #include <locale.h> int main() { int n = 5; // liczba elementów w tablicy float t[] = {1.55,3.99,7.18,0.56,-2.51}; float * p; // wskaźnik do danej typu float int i; setlocale(LC_ALL,""); p = &t[0]; // p wskazuje pierwszy element tablicy for(i = 1; i <= n; i++) // ta pętla wykonuje się n razy printf("%6.2f",*p++); printf("\n\n"); --p; // p wskazuje ostatni element tablicy for(i = 1; i <= n; i++) // ta pętla wykonuje się n razy printf("%6.2f",*p--); printf("\n\n"); return 0; } |
W programie tworzymy tablicę 5-cio elementową, która przechowuje liczby zmiennoprzecinkowe. Najpierw ustawiamy w zmiennej p adres pierwszej komórki tablicy. W pętli wykonywanej 5 razy wyświetlamy:
printf("%6.2f",*p++); |
Ponieważ operator ++
jest za zmienną p, to w
wyrażeniu zostanie użyta zawartość p sprzed modyfikacji. *p jest
elementem tablicy wskazywanym przez p. Po wyświetleniu tego
elementu p zostaje zwiększone o 1. W tym przypadku nie jest to
zwiększenie o wartość słownie 1, lecz o jeden rozmiar obiektu
typu float (czyli o 4 bajty). W
rezultacie tego zwiększenia p wskazuje kolejny element typu
float w tablicy t[]. Gdy pętla się zakończy p wskazuje poza
ostatni element tablicy t[]. Zmniejszamy p, aby wskazywało
ostatni element tablicy i uruchamiamy drugą pętlę, która
wykonuje się również 5 razy. Tutaj wyświetlamy:
printf("%6.2f",*p--); |
Ponieważ operator --
jest za zmienną p, to w
wyrażeniu będzie użyta bieżąca zawartość p, czyli adres kolejnej
od końca komórki tablicy t[]. Zawartość tej komórki
(*p) jest wyświetlana, po czym wskaźnik
zostaje zmniejszony o jeden rozmiar obiektu typu float, czyli o
4 bajty. W efekcie wskazuje w tablicy poprzednią komórkę.
Program wypisuje zatem komórki tablicy od pierwszej do ostatniej, a później od ostatniej do pierwszej.
Do przypisania adresu pierwszej komórki tablicy t[] wskaźnikowi p użyty został operator adresu &:
p = &t[0]; |
Zmień ten wiersz na:
p = t; |
i uruchom program ponownie.
Okazuje się, że program działa dokładnie tak samo. Co z tego wynika? Wynika, że operacje
p = &t[0]; |
oraz
p = t; |
są równoważne! Skoro tak, to t jest po prostu adresem pierwszej komórki tablicy.
Zapamiętaj:
Jeśli p jest wskaźnikiem pewnego elementu
tablicy, to p++ ustawia w p adres kolejnego
elementu, a p-- ustawia w p adres poprzedniego
elementu tej tablicy. Stosując wskaźniki uważaj,
aby nie wyjść poza ostatnią lub pierwszą komórkę
tablicy. Jeśli t jest tablicą, to wartość t jest adresem jej pierwszego elementu, czyli jest wskaźnikiem tego elementu. |
Dodanie do wskaźnika wartości całkowitej k daje w wyniku adres wskazujący element leżący o k komórek dalej w tablicy. Podobnie odjęcie od wskaźnika wartości całkowitej k daje adres elementu, który leży o k komórek wcześniej w tablicy.
Uruchom program:
/* Wskaźniki (C)2016 mgr Jerzy Wałaszek Data utworzenia: 11.10.2016 */ #include <stdio.h> #include <stdlib.h> #include <locale.h> int main() { int n = 5; // liczba elementów w tablicy float t[] = {1.55,3.99,7.18,0.56,-2.51}; float * p; // wskaźnik do danej typu float int i; setlocale(LC_ALL,""); p = t; // p wskazuje pierwszy element tablicy for(i = 0; i < n; i++) // i - indeksy kolejnych elementów printf("%6.2f",*(p + i)); printf("\n\n"); for(i = 0; i < n; i++) // i - indeksy kolejnych elementów printf("%6.2f",t[i]); printf("\n\n"); return 0; } |
Obie pętle dają identyczny wynik. Co z tego wynika? Jeśli p wskazuje pierwszy element tablicy, to *(p + i) jest równoważne t[i]. A skoro t i p są wskaźnikami, to może ta równoważność idzie dalej? Uruchom następny program:
/* Wskaźniki (C)2016 mgr Jerzy Wałaszek Data utworzenia: 11.10.2016 */ #include <stdio.h> #include <stdlib.h> #include <locale.h> int main() { int n = 5; // liczba elementów w tablicy float t[] = {1.55,3.99,7.18,0.56,-2.51}; float * p; // wskaźnik do danej typu float int i; setlocale(LC_ALL,""); p = t; // p wskazuje pierwszy element tablicy for(i = 0; i < n; i++) // i - indeksy kolejnych elementów printf("%6.2f",*(t + i)); printf("\n\n"); for(i = 0; i < n; i++) // i - indeksy kolejnych elementów printf("%6.2f",p[i]); printf("\n\n"); return 0; } |
I znów dostajemy identyczny wynik. Jaki stąd wniosek? Że zmienna t jest po prostu wskaźnikiem i można ją stosować w wyrażeniach wskaźnikowych jako wskaźnik pierwszego elementu tablicy. Z kolei do elementów tablicy możemy się alternatywnie odwoływać za pomocą indeksów. Ale skoro obie zmienne p i t są wskaźnikami, które wskazują ten sam obiekt, to do i-tej komórki tablicy możemy się odwołać albo przez p, albo przez t w sposób identyczny:
*(p + i) lub p[i] *(t + i) lub t[i] |
Czy jest zatem różnica pomiędzy p i t? Jest. Wskaźnik p jest zwykłą zmienną i można zmieniać jego zawartość. Natomiast t jest tzw. stałą (pomówimy o nich później), czyli wartością, której w programie zmienić nie można. Zatem p może wskazywać dowolny obiekt float w pamięci komputera. Natomiast t zawsze wskazuje tylko i wyłącznie pierwszą komórkę tablicy, czyli t[0].
Zapamiętaj:
Jeśli p wskazuje tablicę, to *(p + i) lub p[i]
jest i-tą komórką tej tablicy. Oba zapisy są
równoważne. Jeśli t jest tablicą, to *(t + i) lub t[i] jest i-tą komórką tej tablicy. Oba zapisy są równoważne. |
Wewnętrznie zapis t[i] jest zawsze rozwijany w *(t + i). Ale dla nas w sumie nie ma to większego znaczenia. Stosowanie zapisu jednego lub drugiego jest kwestią gustu.
Zdefiniowana jest również różnica dwóch wskaźników:
Jeśli dwa wskaźniki p i r wskazują komórki tej samej tablicy, a r wskazuje komórkę o wyższym indeksie, to różnica r - p jest równa liczbie komórek pomiędzy wskazywanymi komórkami plus 1:
Uruchom poniższy program:
/* Wskaźniki (C)2016 mgr Jerzy Wałaszek Data utworzenia: 11.10.2016 */ #include <stdio.h> #include <stdlib.h> #include <locale.h> int main() { int t[] = {5,8,3,2,9,7,4,6,1,73,62,21}; // jakaś tablica int *p,*r,c,i; setlocale(LC_ALL,""); p = &t[2]; r = &t[11]; c = r - p; // liczba komórek pomiędzy printf("Pomiedzy %d a %d jest %d komorek:\n\n",*p,*r,c-1); for(i = 1; i < c; i++) printf("%d ",p[i]); printf("\n\n"); return 0; } |
W programie tworzymy pewną tablicę. Następnie we wskaźnikach p i r umieszczamy adresy dwóch komórek tej tablicy, w p adres komórki o mniejszym indeksie, a w r adres komórki o większym indeksie. W c umieszczamy różnicę wskaźników. Jest to liczba komórek od komórki wskazywanej przez p do komórki wskazywanej przez r. Liczba komórek pomiędzy tymi dwoma komórkami jest o 1 mniejsza. Wypisujemy komórki, które się znajdują pomiędzy tymi wybranymi.
Do odczytu danych w języku C stosuje się kilka różnych funkcji, z których najpopularniejszą jest prawdopodobnie funkcja scanf() o następującej składni:
scanf(format,&zmienna_1,&zmienna_2...); |
format | – | jest tekstem, który zawiera znaki
formatujące określające typ wprowadzanej danej.
Stosuje się podobne znaki jak w printf. Opis formatu
rozpoczyna się od znaku %, za którym podajemy
literowo typ danej. Wygląda to następująco: %c –
pojedynczy znak |
&zmienna | – | Za tekstem formatującym umieszczamy adresy zmiennych (wskaźniki), w których mają zostać umieszczone dane. Typ zmiennej powinien odpowiadać formatowi umieszczonemu w tekście formatującym. |
Użycie funkcji scanf() najlepiej zobrazuje prosty przykład:
/* Wskaźniki (C)2016 mgr Jerzy Wałaszek Data utworzenia: 11.10.2016 */ #include <stdio.h> #include <stdlib.h> #include <locale.h> int main() { float r, p, o, pi = 3.1415926535; setlocale(LC_ALL,""); printf("Obliczanie pola i obwodu koła\n" "-----------------------------\n\n"); printf("Podaj r = "); scanf("%f",&r); // odczytujemy promień do zmiennej r p = pi * r * r; // obliczamy pole koła o = 2 * pi * r; // obliczamy obwód koła printf("\n"); printf("Pole = %9.2f\n" "Obwód = %9.2f\n\n",p,o); return 0; } |
Pewien kłopot może pojawić się przy odczycie tekstu. Tekst umieszczony zostanie w tablicy znakowej. Musisz zarezerwować w programie odpowiednio dużą tablicę, aby pomieściła cały tekst. Funkcja scanf() odczytuje tekst wyraz po wyrazie. Znakami rozdzielającymi wyrazy są znaki białe (odstępy spacje) oraz znak końca wiersza. Na końcu odczytanego tekstu umieszczany jest automatycznie znak NUL, który w języku C oznacza koniec tekstu.
Uruchom program:
/* Wskaźniki (C)2016 mgr Jerzy Wałaszek Data utworzenia: 11.10.2016 */ #include <stdio.h> #include <stdlib.h> #include <locale.h> int main() { char nazwisko[25]; char miasto[25]; int wiek; float wzrost; setlocale(LC_ALL,""); printf("Zbieranie danych osobowych\n" "==========================\n\n"); printf("Nazwisko : "); scanf("%24s",nazwisko); printf("Miasto : "); scanf("%24s",miasto); printf("Wiek : "); scanf("%d",&wiek); printf("Wzrost [m] : "); scanf("%f",&wzrost); printf("\n\nMasz na nazwisko %s,\n" "twoje miasto to %s,\n" "wiek lat %d i wzrost %4.2f m.\n\n", nazwisko,miasto,wiek,wzrost); return 0; } |
W programie użyliśmy funkcji scanf do odczytania nazwiska oraz miasta do odpowiednich tablic. Zwróć uwagę na kilka rzeczy:
Program działa poprawnie z nazwiskami i miastami jednowyrazowymi, jednakże całkiem się załamie przy nazwisku, np. trójczłonowym: Maria Rokita Małecki (przykładowo). Dlaczego tak się dzieje? Funkcja scanf czyta pojedyncze wyrazy. Zatem z wprowadzonego tekstu odczyta słowo Maria jako nazwisko, następnie odczyta drugie słowo Rokita jako miasto, a Małeckiego będzie chciała zinterpretować jako wiek, co oczywiście się jej nie uda. W efekcie powstanie błąd konwersji i dostaniesz misz masz. Nie będę się zajmował wyłapywaniem tutaj błędów, ponieważ zaciemnia to ideę programu. Po prostu scanf nie nadaje się zbytnio do odczytywania wiersza znaków, a coś takiego jest nam tutaj potrzebne. Na szczęście w bibliotece stdio jest zdefiniowana funkcja gets, która odczytuje z konsoli cały wiersz znaków. Składnia tej funkcji jest następująca:
gets(tablica); |
Funkcja odczytuje wiersz znaków aż do napotkania znaku nowego wiersza \n. Odczytane znaki są umieszczane kolejno we wskazanej tablicy, a znak nowego wiersza zostanie zastąpiony znakiem NUL (o kodzie zero). Jest jednak pewien problem: funkcja nie sprawdza, czy nie przekroczono rozmiaru tablicy. Dla prostoty załóżmy jednak, że ani nazwisko, ani nazwa miasta nie przekroczą 49 znaków. Uruchom program:
/* Wskaźniki (C)2016 mgr Jerzy Wałaszek Data utworzenia: 11.10.2016 */ #include <stdio.h> #include <stdlib.h> #include <locale.h> int main() { char nazwisko[50]; char miasto[50]; int wiek; float wzrost; setlocale(LC_ALL,""); printf("Zbieranie danych osobowych\n" "==========================\n\n"); printf("Nazwisko : "); gets(nazwisko); printf("Miasto : "); gets(miasto); printf("Wiek : "); scanf("%d",&wiek); printf("Wzrost [m] : "); scanf("%f",&wzrost); printf("\n\nMasz na nazwisko %s,\n" "twoje miasto to %s,\n" "wiek lat %d i wzrost %4.2f m.\n\n", nazwisko,miasto,wiek,wzrost); return 0; } |
Teraz wszystko będzie w porządku (wpisz: nazwisko: Maria Rokita Małecki, miasto: Nowy Jork, wiek: 45, wzrost: 1.82). Funkcja gets jest dużo prostsza od scanf i nie potrafi interpretować odczytywanych danych. Za to potrafi wczytać cały wiersz znaków. Obecnie nie zaleca się stosowania funkcji gets, ponieważ nie jest ona bezpieczna – niewiadomo ile znaków odczyta z konsoli, jeśli zbyt dużo, to dojdzie do przepełnienia bufora i nadpisania obszaru pamięci poza buforem. Zaleca się stosować funkcję fgets, jednak ta używa strumieni plikowych, a tego zagadnienia nie chcę tutaj omawiać (związane jest ściśle z dużymi komputerami PC).
Aby mieć przedsmak programowania mikrokontrolerów, załóżmy, że nie mamy do dyspozycji funkcji scanf. Natomiast udało nam się zaprogramować odpowiednik funkcji gets. Naszym zadaniem jest odczytanie liczby dziesiętnej w zakresie od 0 do 4 miliardów (liczba całkowita bez znaku), która będzie potrzebna później do jakiś obliczeń. Jak to zrobić?
Wiele osób nie rozumie różnicy pomiędzy zapisem liczby a jej wartością. Różnica jest mniej więcej taka sama jak pomiędzy zapisem jakiegoś słowa a znaczeniem tego słowa. Zapis liczby zbudowany jest z ciągu znaków ASCII, a jej wartość jest wielkością matematyczną, pojęciem matematycznym, które od zapisu nie zależy – daną wartość można zapisać w różny sposób, np. 100 (sto dziesiętnie) wygląda jak 1100100 w systemie dwójkowym, 144 w systemie ósemkowym, 64 w systemie szesnastkowym. Jak widzisz, zapisy są różne, lecz wartość wciąż wynosi 100: dla ludzi zapis jest równoważny wartości, ponieważ tak nas nauczono w szkole. Jeśli zapis liczby 100 umieścimy w pewnej tablicy znakowej t[], to będzie to wyglądało tak:
t[0] zawiera 49 – kod ASCII cyfry 1 t[1] zawiera 48 – kod ASCII cyfry 0 t[2] zawiera 48 – kod ASCII cyfry 0 t[3] zawiera 0 – kod ASCII NUL, czyli koniec tekstu. |
Całość zajmuje w pamięci 4 bajty: 49,48,48,0.Teraz widać dokładnie, o co chodzi. Naszym zadaniem jest przekształcenie tych bajtów w ładną wartość 100, którą komputer zapisze sobie w jakiejś zmiennej.
Jeśli mamy dany ciąg cyfr liczby pozycyjnej o podstawie p, to wartość obliczamy ze wzoru:
Wzór ten nie jest zbyt wygodny, ponieważ należy w nim obliczać potęgi podstawy. Istnieje prostsza metoda zwana schematem Hornera:
Zobaczmy na przykładzie, jak to działa. Załóżmy, że mamy liczbę 7854:
7854 | W ← 0 | |
7854 | W ← W·10 + 7 = 0 + 7 = 7 | |
7854 | W ← W·10 + 8 = 70 + 8 = 78 | |
7854 | W ← W·10 + 5 = 780 + 5 = 785 | |
7854 | W ← W·10 + 4 = 7850 + 4 = 7854 |
Jak widzisz zasada schematu Hornera jest prosta i pozwala policzyć wartość liczby pozycyjnej zapisanej w dowolnym systemie pozycyjnym. Co więcej, schemat Hornera może być stosowany do obliczania wartości liczby w locie, w miarę jak pobieramy z wejścia kolejne jej cyfry. Nawet mnożenie przez 10 da się zastąpić dodawaniem:
a += a; // a <-- 2a b = a; // zapamiętujemy 2a w b a += a; // a <-- 4a a += a; // a <-- 8a a += b; // a <-- 10a |
Taki ciąg operacji dodawania wykonujemy na mikrokontrolerze, który nie posiada sprzętowej operacji mnożenia. Wykonywane jest to bardzo szybko i sprawnie.
/* Wskaźniki (C)2016 mgr Jerzy Wałaszek Data utworzenia: 11.10.2016 */ #include <stdio.h> #include <stdlib.h> #include <locale.h> int main() { char *p,bufor[11]; unsigned w = 0; setlocale(LC_ALL,""); printf("DEC (0...4294967295) : "); scanf("%10s",bufor); for (p = bufor; *p; p++) { w *= 10; w += *p - '0'; } printf("\nLiczba = %u\n\n",w); return 0; } |
W programie tworzymy tablicę znakową o pojemności 11 znaków. W tablicy będą umieszczane cyfry odczytanej z konsoli liczby za pomocą funkcji gets. Dostęp do tych cyfr odbywa się poprzez wskaźnik p. Na początku pętli we wskaźniku p umieszczamy adres pierwszej cyfry w buforze. Pętla wykonuje się do momentu, aż wskaźnik p wskaże znak NUL kończący tekst. W każdym obiegu w (wartość liczby, ustawiana na początku programu na 0) jest mnożone przez 10, a następnie do w zostaje dodana wartość cyfry. Tutaj pojawia się nowy element: stała znakowa '0'. Jeśli w apostrofach (nie w cudzysłowach) umieścimy dowolny znak, to otrzymamy kod ASCII tego znaku (co uwalnia programistę od pamiętania kodów). Aby otrzymać wartość liczbową cyfry należy od kodu ASCII tej cyfry odjąć kod ASCII cyfry zero. Stała '0' ma wartość 48, co jest właśnie kodem cyfry 0.
Gdy pętla się zakończy w zmiennej w będzie wartość wprowadzonej liczby.
Program bez wielkich zmian można przystosować do odczytywania liczb ósemkowych:
/* Wskaźniki (C)2016 mgr Jerzy Wałaszek Data utworzenia: 11.10.2016 */ #include <stdio.h> #include <stdlib.h> #include <locale.h> int main() { char *p,bufor[12]; unsigned w = 0; setlocale(LC_ALL,""); printf("OCT (0...37777777777) : "); scanf("%11s",bufor); for (p = bufor; *p; p++) { w *= 8; // podstawa równa 8! w += *p - '0'; } printf("\nLiczba = %u\n\n",w); return 0; } |
Oraz do liczb dwójkowych:
/* Wskaźniki (C)2016 mgr Jerzy Wałaszek Data utworzenia: 11.10.2016 */ #include <stdio.h> #include <stdlib.h> #include <locale.h> int main() { char *p,bufor[33]; unsigned w = 0; setlocale(LC_ALL,""); printf("BIN (0...11111111111111111111111111111111) : "); scanf("%32s",bufor); for (p = bufor; *p; p++) { w += w; // podstawa równa 2! w += *p - '0'; } printf("\nLiczba = %u\n\n",w); return 0; } |
Trochę kłopotu jest z odczytem liczb szesnastkowych, ponieważ cyframi mogą również być literki małe i duże. Zasada konwersji znaku cyfry na jej wartość jest następująca:
/* Wskaźniki (C)2016 mgr Jerzy Wałaszek Data utworzenia: 11.10.2016 */ #include <stdio.h> #include <stdlib.h> #include <locale.h> int main() { char *p,c,bufor[9]; unsigned w = 0; setlocale(LC_ALL,""); printf("HEX (0...FFFFFFFF) : "); scanf("%8s",bufor); for (p = bufor; *p; p++) { w *= 16; // podstawa równa 16! c = *p - '0'; if(c > 9) c -= 7; if(c > 15) c -= 32; w += c; } printf("\nLiczba = %u\n\n",w); return 0; } |
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:
Serwis wykorzystuje pliki cookies. Jeśli nie chcesz ich otrzymywać, zablokuj je w swojej przeglądarce.
Informacje dodatkowe.