Serwis Edukacyjny
w I-LO w Tarnowie
obrazek

Materiały dla uczniów liceum

  Wyjście       Spis treści       Wstecz       Dalej  

obrazek

Autor artykułu: mgr Jerzy Wałaszek
Konsultacje: Wojciech Grodowski, mgr inż. Janusz Wałaszek

©2024 mgr Jerzy Wałaszek
I LO w Tarnowie

obrazek

Warsztat

Kurs języka C

Wskaźniki

SPIS TREŚCI
Podrozdziały

Co to jest wskaźnik

Każdy współczesny komputer posiada pamięć (ang. memory), w której przechowuje dane oraz wykonywany program (mikrokontrolery, którymi będziemy się zajmować w tym artykule, posiadają tzw. architekturę harwardzką, w której występuje osobna pamięć na dane oraz osobna pamięć na program). Pamięć jest urządzeniem elektronicznym, które zapamiętuje bity. Bity są organizowane w większe grupy (np. bajty – grupy 8 bitów), do których ma dostęp mikrokontroler. Struktura pamięci bardzo przypomina strukturę tablicy, a nawet stosuje się podobną terminologię. Pamięć składa się z komórek (ang. memory cells), w których przechowywana jest informacja w postaci grupy bitów. Każda komórka pamięci posiada adres, czyli numer umożliwiający mikrokontrolerowi uzyskanie dostępu do zawartej w niej informacji. W komputerze dużym, jakim niewątpliwie jest twój IBM-PC, liczba komórek pamięci sięga miliardów, np. mój komputer posiada na pokładzie 16GB pamięci, czyli 16 miliardów komórek, z czego pewnie połowę marnuje Windows 10, ale nie o tym będzie tutaj mowa :).

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:

obrazek

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:

obrazek

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.


Na początek:  podrozdziału   strony 

Zmienne wskaźnikowe

Zmienna wskaźnikowa (ang. pointer variable) jest zmienną, która zawiera wskaźnik do innej zmiennej. W języku C wskaźnik nie jest tylko adresem komórki w pamięci komputera. Określa on również rodzaj, typ wskazywanego obiektu. Na początku może ci się to wydać niezbyt jasne, ale takie podejście ma swoje zalety. Zapamiętaj: wskaźnik jest adresem konkretnego obiektu.

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.


Na początek:  podrozdziału   strony 

Wskaźniki i tablice

W tym podrozdziale zobaczysz, że wskaźniki i tablice mają ze sobą bardzo wiele wspólnego.

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.


Na początek:  podrozdziału   strony 

Odczyt danych

W kursie tym staram się nie stosować zbyt dużo funkcji wejścia/wyjścia, ponieważ możesz ich nie spotkać w środowisku mikrokontrolerów, a one są celem tego kursu języka C (kod takich funkcji jest duży i zwykle nie mieści się w ograniczonej pamięci mikrokontrolera, dodatkowo do działania wymagają one konsoli z ekranem i klawiaturą, a tych może w ogóle nie być w projektowanym urządzeniu). Wprowadziłem funkcję printf(), aby czytelnie wyświetlać wyniki tworzone przez program. Z odczytywaniem danych wstrzymywałem się do momentu omówienia adresów i wskaźników. Funkcje odczytujące dane potrzebują adresu zmiennej, aby odczytane dane w tej zmiennej umieścić. Adres zmiennej jest wskaźnikiem. Zatem zastosowanie funkcji odczytu wymago dobrego rozumienia pojęcia adresów i wskaźników.

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
%d – liczba dziesiętna ze znakiem
%u – liczba dziesiętna bez znaku
%o – liczba całkowita ósemkowa
%x – liczba całkowita szesnastkowa, można również stosować %X
%f – liczba zmiennoprzecinkowa
%s – ciąg znaków do napotkania spacji, tabulacji lub znaku \n. Wczytuje pojedynczy wyraz.
%ns – ogranicza liczbę wczytanych znaków do n, które jest liczbą. Na przykład %8s wczyta maksymalnie 8 znaków. Za wczytanymi znakami automatycznie dodaje znak NUL o kodzie 0

&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:

  1. Za wynik przyjmij zero
  2. Dopóki są cyfry, pobierz cyfrę, wynik przemnóż przez wartość podstawy i dodaj pobraną cyfrę.

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:

  1. Odczytujemy do c kod ASCII cyfry szesnastkowej
  2. Od c odejmujemy kod cyfry zero.
  3. Jeśli c jest większe od 9, to od c odejmujemy 7 (różnica pomiędzy kodem ASCII cyfry 9 a kodem litery A).
  4. Jeśli c jest większe od 15, to od c odejmujemy 32 (mamy do czynienia z cyfrą a...f).
/*
 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;
}

Na początek:  podrozdziału   strony 

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: i-lo@eduinf.waw.pl

Serwis wykorzystuje pliki cookies. Jeśli nie chcesz ich otrzymywać, zablokuj je w swojej przeglądarce.

Informacje dodatkowe.