mgr inż. Artur Bernat
Rozważania nad istotą języka C.


Wszelkie prawa zastrzeżone


SPIS TRESCI
Wprowadzenie
Pojęcie wyrażenia w języku
Przykłady błędów popełnianych przez początkujących "programistów".
Operowanie na zmiennych wskaźnikowych
Typy łańcuchowe
Pierwszy przykład wykorzystania wskaźników i zmiennych łańcuchowych
Drugi przykład wykorzystania wskaźników i zmiennych łańcuchowych
Trzeci przykład wykorzystania wskaźników i zmiennych łańcuchowych
Czwarty przykład wykorzystania wskaźników i zmiennych łańcuchowych

Wprowadzenie
    Poniżej przedstawione, powiedziałbym, rozważania na temat możliwości leksykalnych i wykonawczych języka C, nie należy traktować jako regularnego, pełno-wymiarowego wykładu, lecz raczej jako pobieżny przegląd co ważniejszych elementów tego języka programowania, z dużym naciskiem kładzionym na wszechstronne możliwości tworzenia wyrażeń w języku C oraz użycie wskaźników. Mam nadzieję, że przytaczając stronę WWW na ten temat, zaoszczędzę dużo trudu tym, którzy zarówno  uczą się języka C jako drugiego języka wysokiego poziomu, po opanowaniu takiego języka jak Pascal lub Fortran, jak i tym, którzy starają się opanować ten język jako ich pierwszy język programowania.

Pojęcie wyrażenia w języku C
Na początek omówimy pojęcie wyrażenia w języku C.
Wyrażeniem w języku C może być zarówno taka linia kodu źródłowego:

i+=2;                /*zwiększ wartość zmiennej liczbowej i o 2*/

jak i linia kodu realizująca np. cztery wzajemnie niezależne operacje:

i++,--j, (*x)++, y--, y--;

/*zwiększ wartość i o jeden, zmniejsz wartość j o jeden, wyłuskaj wartość liczbową spod wskaźnika x i zwiększ ją o jeden, dwukrotnie powtórz deinkrementację wartości zmiennej liczbowej y.*/

Zauważmy, że kolejne instrukcje w wyrażeniu przedzielone są przecinkami, natomiast całość wyrażenia zakończona jest średnikiem. Jak widać z powyższego przykładu wyrażenie może posiadać bardziej rozbudowaną składnię, tzn. zawierać w sobie wiele niezależnych instrukcji. Wyrażenie może poza tym występować w języku C w postaci wolnostojącej, tzn. do napisania dowolnego wyrażenia nie potrzeba stosować koniecznie operatora przypisania „="(wyrażenie wolnostojące - termin własny autora),  wyrażenie takie może wykorzystywać tylko operator unarny (jednoargumentowy) lub nie wykorzystywać żadnych operatorów , tak jak np. w wyrażeniu typu:

i

lub w wyrażeniu:

i++

Z tak szerokim pojęciem wyrażenia w języku C wiąże się odpowiedni mechanizm estymacji wartości wyrażenia. Wszak wartość estymowana takiego wyrażenia może wpływać na przebieg programu w pętlach iteracyjnych for, do while , while oraz w instrukcji warunkowej if.
Ci z czytelników, którzy zadufani są w stylu programowania języka Pascal, doznają pewnie w tym miejscu zdziwienia. Otóż w celu przeprowadzenia dalszych wywodów na temat estymacji wyrażeń w języku C, pomocne będzie wprowadzenie uogólnionego pojęcia wartości zmiennej logicznej. W języku C, w przeciwieństwie do Pascala, nie istnieje tak naprawdę wyspecjalizowane pojęcie zmiennej logicznej i przyjmowanych przez nią wartości TRUE i FALSE. Mianowicie, wartość dowolnego testowanego wyrażenia logicznego lub arytmetycznego umieszczonego w pętli iteracyjnej lub w instrukcji warunkowej , różna od zera interpretowana jest jako spełnienie warunku logicznego, czyli wartość TRUE. Natomiast wartość testowanego wyrażenia logicznego lub arytmetycznego równa zeru oznacza niespełnienie warunku logicznego, czyli wartość FALSE.
Tą właściwość języka C, ( a właściwie jego wielką zaletę, pozwalającą na uproszczenie testowanych wyrażeń) można zobrazować na przykładzie pętli iteracyjnej while, wykorzystanej do obliczania wartości silni S z zadanej liczby całkowitej i. Początkujący w języku C, "wyrażający się" poprawnie „programista", napisałby pewnie:

void main()
{
 int i=7;
 int S=1;
 while(i>0)
 {
  S=S*i;
  i=i-1;
 }
 return;
}

Powyższy program, pomimo, że jest napisany tzw. „poprawnie", jest zwykłą stratą kodu źródłowego i czasu wykonania tychże instrukcji. Bardziej poprawna i zwięzła wersja tego programu może wyglądać następująco:

void main()
{
 int i=7;
 int S=1;
  while(i)        /*praca pętli while jest podtrzymywana, dopóki wartość wyrażenia jest                 niezerowa.*/
      S*=i, i--;

/*wyrażenie to składa się z instrukcji przemnażającej wartość zmiennej   S przez wartość zmiennej i oraz z instrukcji deinkrementacji wartości zmiennej i*/
 return;
 }

Przypomnę, że w języku C istnieje cała gama zwięzłych operatorów:

*=,/=,+=,-=,^=,itp.,

 równoważnych, w pewnych warunkach, w rezultatach wykonania operacji, odpowiednim operatorom:

*, /, +, -, ^, itp.

W przypadkach, gdy wynik jednej z tych operacji dwuargumentowych jest umieszczany pod zmienną będącą jednym z argumentów, można napisać prościej i źwięźlej:

S*=i;

,zamiast:

S=S*i;         /*jest to zwykła nadmiarowa strata kodu źródłowego*/

Z pewnością wartość silni z zadanej liczby całkowitej  można obliczyć w języku C co najmniej kilkanaście sposobów, co zostanie udowodnione, a przy okazji zademonstrowana będzie elastyczność tworzenia wyrażeń języka C
Powyżej przedstawiony program obliczania silni może być jeszcze krótszy:

void main()
{
 int i=7;
 int S=1;
 while(i)
    S*=i--;

/*wyrażenie to wykorzystuje zjawisko deinkrementacji w notacji postfiksowej, tzn. po wykonaniu właściwej operacji przemnażania wartości zmiennej i przez wartość zmiennej S,  wartość zmiennej i jest deinkrementowana.*/
 return;
}

Ciekawe co by się stało, gdyby w wyrażeniu obliczającym silnię, deinkrementację w notacji postfiksowej zamieniono na deinkrementację w notacji prefiksowej, tzn. w przebiegu bieżącej iteracji pętli while najpierw następowała by operacja deinkrementacji wartości zmiennej i, a dopiero w drugiej kolejności tak zmniejszona wartość zmiennej i wykorzystana byłaby w operacji przemnażania:

void main()
{
 int i=8;
 int S=1;
 while(i>1)
   S*=--i;

/*przed wykonaniem właściwej operacji przemnażania wartości zmiennej i przez wartość zmiennej S, wartość zmiennej i jest deinkrementowana.*/
 return;
}

Program nieco się skomplikował. Wartość początkowa zmiennej i wynosi 8, a pomimo tego faktu, program będzie nadal liczył silnię z 7 (5040). Dlaczego? W wyrażeniu podrzędnym pętli while liczącym silnię, występuje unarny(jednoargumentowy) operator deinkrementacji w notacji prefiksowej, pomniejszający o jeden wartość zmiennej i przed każdą operacją przemnażania. „Skomplikowało się" również wyrażenie testowane w warunku pętli while. Musimy w tym programie dbać o to, aby przy następnej wykonywanej iteracji, wartość zmiennej i nie spadła do wartości 1. W przeciwnym wypadku policzona silnia S wynosić będzie wynosić... zero. A propos: testowane wyrażenie w warunku pętli while, z wyrażenia wolnostojącego, przedzierzgnęło się w wyrażenie czysto logiczne o możliwych wartościach zwracanych równych albo 0(FALSE) albo 1(TRUE).
Przeanalizujmy z kolei zupełnie inne podejście do problemu:

void main()
{
 int i=8;
 int S=1;
 while(--i)

/*tutaj został zastosowany unarny(jednoargumentowy) operator deinkrementacji w notacji prefiksowej*/
   S*=i;
 return;
}

W programie tym po raz pierwszy spotykamy się z problemem estymacji testowanego wyrażenia, logicznego lub arytmetycznego, w warunku pętli while. Jeśli, tak jak w tym powyższym programie, występuje operator unarny deinkrementacji w notacji prefiksowej, to wartość wyrażenia jest poddawana estymacji dopiero po wykonaniu operacji deinkrementacji. Tak więc wartość początkowa zmiennej i , w tym opisywanym przykładzie musi wynosić 8, aby została prawidłowo policzona silnia z wartości liczby 7.
Analogicznie, jeśli w wyrażeniu estymowanym warunku pętli while użyjemy deinkrementacji w notacji postfiksowej, to wartość wyrażenia testowego będzie poddawana estymacji przed wykonaniem operacji deinkrementacji. Ilustruje to poniższy program:

void main()
{
 int i=8;
 int S=1;
 while(i -- >1)

/*zastosowany tutaj operator unarny deinkrementacji w notacji postfiksowej, spowodował, że estymacji poddawane jest najpierw wyrażenie i>1, a następnie wykonywana jest operacja deinkrementacji na wartości zmiennej i.*/
   S*=i;
 return;
}

W powyższym przykładzie wartość początkowa zmiennej i musi być również ustawiona na 8, z uwagi na to, że wartość zmiennej i jest pomniejszana o jeden przed operacją mnożenia.
Innym jeszcze sposobem na obliczenie silni przy użyciu deinkrementacji z użyciem pętli iteracyjnej while jest rozbudowa estymowanego wyrażenia warunku pętli while. Oto przykład:

void main()
{
 int i=7;
 int S=1;
 while(S*=i,--i)     /*wyrażenie warunku pętli while złożone jest z dwóch instrukcji*/
 ;             /*tutaj wykonywana jest instrukcja pusta (symbol średnika)*/
 return;
}

Jak widać z powyższego przykładu, właściwa operacja mnożenia i obliczania wartości silni dla zadanej wartości zmiennej i, wykonywana jest jako część wyrażenia estymowanego warunku pętli while. Wskutek takiej kompozycji pętli iteracyjnej while w zasadniczym bloku instrukcji podrzędnych pętli while jest wykonywana tylko instrukcja pusta(symbol średnika)!.
Uwaga: estymowaną wartością takiego złożonego wyrażenia jest wartość zwracana przez ostatnie podwyrażenie(instrukcję) w wyrażeniu, czyli w powyższym przykładzie przez podwyrażenie:

--i.

Z tego też względu, niedopuszczalne byłoby umieszczenie następującego wyrażenia w warunku pętli while:

while(--i, S*=i)

oraz wyrażenia:

while(S*=i--)

W tych dwóch przykładowych, błędnie skonstruowanych, estymowanych wyrażeniach warunku pętli while, wartość zwracana wyrażenia nie tylko nie sugerowałaby zakończenia pracy pętli w odpowiedniej iteracji, lecz prawdopodobnie pętla taka w nieskończonosć liczyłaby błędną wartosć silni..
Uwzględniając możliwość liczenia silni na dwa sposoby tj. przez przemnażanie tymczasowego wyniku przez wzrastającą wartość mnożnika, a drugim razem przez przemnażanie tymczasowego wyniku przez malejącą do jedności wartość mnożnika oraz uwzględniając możliwość zastosowania trzech różnych pętli iteracyjnych tj. while, do while, for, liczbę przedstawionych powyżej sposobów liczenia wartości silni można rozszerzyć do co najmniej 20 przykładów. Nie to jednak jest przedmiotem i celem tego wykładu. Z uwagi na specyficzne odmienności pętli for w stosunku do pętli while powyższe przykłady liczenia silni będą jeszcze zaimplementowane z użyciem tej pętli.

void main()
{
 for(int i=7,S=1; i ; i--)

/*nowsza wersja kompilatora zezwala na deklarację zmiennych w prawie każdym miejscu, np. w wyrażeniu inicjującym warunek początkowy pętli for*/
   S*=i;
 return;
}

Nie zważając na czystość klasycznego języka C, nowsze kompilatory tego języka umożliwiają deklarowanie zmiennych lokalnych dosłownie prawie w każdym miejscu kodu źródłowego programu. Chociaż powyższy program funkcjonuje,. nie zaleca się stosowania takiego stylu programowania.
Jeszcze bardziej zwięzłym kodem źródłowym, tego drobnego przykładu obliczania silni, którego składnię akceptuje kompilator  np. Borland’a w wersji 3.,1 może poszczyć się poniższy program:

void main()
{
 for(int i=7,S=1; i ; S*=i--);

/*wyrażenie trzecie pętli for, odpowiedzialne za zmianę wartości zmiennej iteracyjnej, jednocześnie jest wyrażeniem liczącym wartość silni S*/
 return;
}

W powyższym przykładzie zastosowano deinkrementację w notacji postfiksowej.
Dla urozmaicenia możemy zastosować tutaj również notację prefiksową deinkrementacji, chociaż postać pętli for, obliczącej silnię z wartości zmiennej i, nieco się skomplikuje:

void main()
{
 for(int i=7,S=1; i ; S*=--i+1);

/*wyrażenie trzecie pętli for, odpowiedzialne za zmianę wartości zmiennej iteracyjnej, dokonuje najpierw deinkrementacji wartości zmiennej i, a następnie wykorzystuje tak zmienioną jej wartość w obliczaniu iloczynu S. Z tego względu niezbędne było dodanie jedynki do  wartości zmiennej i w tym wyrażeniu*/
 return;
}

Przykłady błędów popełnianych przez początkujących "programistów".
Na podstawie przytoczonego materiału, celowym w tym miejscu wydaje się przytoczenie typowych błędów popełnianych przez początkujących „programistów" języka C.

Pierwszy przykład. Dosyć często instrukcja warunkowa if, służącą do testowania prawdziwości pewnego warunku, tak jak poniżej:

if(c==5)
 /*tu następuje blok instrukcji podrzędnych wykonywanych warunkowo*/

zostaje nieopatrznie użyta w następujący sposób:

if(c=5)
 /*blok instrukcji podrzędnych wykonywanych rzekomo warunkowo*/

Wówczas umieszczona w warunku instrukcji if wartość wyniku operacji przypisania c=5 zamiast operacji porównania c==5, jest zawsze niezerowa, równa 5, co powoduje bezwarunkowe(!) wykonanie bloku instrukcji podrzednych pod instrukcją if.

Drugi przykład. Często w pętlach iteracyjnych typu while, liczących np. silnię z zadanej liczby całkowitej, tak jak w przykładzie ponizej:

while(i)
 S*=i--;

przez nieuwagę, bezpośrednio po pierwszej linii, zostaje umieszczony symbol średnika, oznaczający instrukcję pustą:

while(i);
 S*=i--;

Powstaje wówczas pętla o nieskończonej liczbie  iteracji, wykonujących instrukcję pustą!
Zwartość i zwięzłość języka C nie pozwala na jakiekolwiek błędne posunięcie, a wadliwe działanie algorytmów związane z niewłaściwym umieszczeniem symbolu instrukcji pustej, okazuje się zwykle trudnym do wykrycia.
Oczywiście, jeśli komuś zależy na stworzeniu niekończącej się pętli w programie napisanym w języku C, to elastyczność związana z manipulowaniem wyrażeniami pozwala użyć np. pętli for o następującej postaci:

for(; ; ;);

Uważam  jednak, że praktyczniejsze wykorzystanie pętli for z trzema pustymi wyrażeniami (tj. realizującymi odpowiednio : inijcjację zmiennej(ych) iteracyjnej(ych), wyrażenie testowe warunku końcowego pętli oraz wyrażenie modyfikacji zmiennej(nnych) iteracyjnej(nych) ) obrazuje poniższy przykład, jeszcze jednego sposobu na obliczanie silni:

int S=1;
int i=7;
void main()
{
 for(;;) //pętla for z trzema wyrażeniami pustymi
  {
   S*=i--; //wyrażenie obliczania wartości silni i deinkrementacji zmiennej i
   if(!i)    //gdy wartość zmiennej osiągnie zero
    break; //bieg programu " wyłamie sie " z pętli for
  }
 return;
}
Mechanizm działania pętli for został "zwolniony" z obowiązku inicjacji i deinkrementacji wartości zmiennej i oraz z testowania warunku końcowego funkcjonowania pętli. Funkcje te przejął blok instrukcji podrzędnych tej pętli for. Jednak nie zaleca się i nie pochwala "wyręczania" pętli for w obowiązku spęłniania tych funkcji. Oczywiście powyższy przykład został tu przytoczony tylko w celach demonstracji granic elastyczności składni języka C.
 

Operowanie na zmiennych wskaźnikowych.
W dobrym opanowaniu języka C, niezbędna jest przynajmniej podstawowa wiedza o typach wskaźnikowych na zmienne wszelkiego typu.

Zmienne wskaźnikowe służą do przechowywania adresu (inaczej mówiąc wskazania) na określony obszar pamięci.Deklaracja zmiennej wskaźnikowej na pewien typ danych zawsze poprzedzona jest symbolem gwiazdki(asterisk symbol), np.

float *wskf;     /*zmienna wskaźnikowa na zmienną typu rzeczywistego*/
char *wskc;    /*zmienna wskaźnikowa na zmienną typu znakowego*/
int *wskn    /*zmienna wskaźnikowa na zmienną typu całkowitego*/

struct el
{
 char nazw[40];
 char imie[40];
}  *wskstr
    /*zmienna wskaźnikowa na typ strukturalny el, zmiennej zajmującej 80 bajtów */

Wartości zmiennych wskaźnikowych mogą ulegać w pewnych sytuacjach przypisaniu oraz innym zmianom np. wywołanych inkrementacją lub deinkrementacją  zmiennej wskaźnikowej.
Przy czym inkrementacja wartości zmiennej wskaźnikowej wskazującej na typ float różni się zawsze od inkrementacji wartości zmiennej wskaźnikowej wskazującej inny np. zdefiniowany powyżej typ strukturalny el. Mianowicie operacja:

wskf++

powoduje zwiększenie adresu wskazującego na zadany obszar pamięci o ilość bajtów zajmowany przez daną typu float, czyli o 4 bajty. Ta sama operacja inkrementacji wykonana  na wartości zmiennej wskaźnikowej wskstr wskazującej na zmienną o typie el zdefiniowanym powyżej, spowoduje zwiększenie adresu wskazywanego o wielkość takiej struktury, czyli w tym rozważanym przypadku, o 80 bajtów. Ten określony „rastr" wartości adresu zmiennych wskaźnikowych, ściśle powiązany z typem danej, na który wskazuje dany wskaźnik, nie ujmuje oczywiście nic z elastyczności języka C. Wykorzystując operator rzutowania ( ) wartości jednego typu danej  na wartość innego typu danych, możemy przypisać wartość wskaźnika 4-bajtowego typu float  wartości wskaźnika 1-bajtowego typu char:

wskc=(char *) wskf;

Mamy wówczas nierzadką okazję „zaglądnięcia" do wewnętrznego formatu liczby typu float, chociażby z użyciem następujących linii kodu:
 

....                /*deklaracje zmiennych wskf, wskc jak powyżej*/
float f=5;
wskf= &f; /*tworzenie referencji, czyli adresu odnoszącego się do zmiennej  f*/
wskc=(char *) wskf; /*operacja rzutowania, wymuszająca w trakcie operacji przypisania wartości wskaźnika wskc wartość wskaźnika wskc, typ wskazania na zmienną typu char.*/
for(int i=0;i<4;i++)
 printf(„%d bajt pamięci : %d\n", i,*wskc++);
........

Rezultat wykonania tego bloku kodu będzie następujący:

0 bajt pamięci: 0
1 bajt pamięci: 1
2 bajt pamięci: -96
3 bajt pamięci: 64

Ale, kto by chciał „zaglądać" do wewnętrznego formatu liczby typu float przechowującej wykładnik i mantysę reprezentowanej liczby rzeczywistej? Fe!
Z typami wskaźnikowymi języka C, w szczególny sposób wiąże się, znakomicie skonstruowana koncepcja typów tablicowych. Nazwa tablicy o przykładowej definicji, umieszczonej poniżej:

char tabznak[20];

czyli tabznak jest jednocześnie wyrażeniem zwracającym adres na początek położenia tej tablicy i jednocześnie jest to adres wskazujący położenie pierwszej komórki tej tablicy tabznak[0]. Z tego też naturalnie wynikającego faktu, indeks pierwszej komórki dowolnej zmiennej, typu tablicowego jest równy zeru, a ostatni element tablicy jest wskazywany przez indeks o jeden mniejszy od całkowitej ilości komórek tablicy.  Dla tablicy tabznak, 20-elementowej, zdefiniowanej jak powyżej, lista dostępnych komórek wygląda następująco:

tabznak[0] tabznak[1] tabznak[2] tabznak[3] ... tabznak[18] tabznak[19].

Do wartości zmiennej przechowywanej pod adresem przechowywanym w zmiennej wskaźnikowej możemy się odwoływać za pomocą operatora wyłuskania  „*"(symbol asterisku). Nie należy mylić tego operatora z tym samym symbolem, używanym w innym miejscu przy deklaracji zmiennych wskaźnikowych. Operatorem dopełniającym do operatora wyłuskania „*", jest operator tworzenia referencji „&" służący do tworzenia adresu odniesienia do zmiennej. Dla przykładu niech będą dane deklaracje zmiennych:

float * wskf1,wskf2;
float f=5;

Wówczas dopuszczalne są operacje:

wskf1=&f;

/*przeprowadzana jest operacja referencji, czyli pobierania adresu odniesienia do zmiennej f*/
wskf2 = wskf1;
/*operacja przypisania wartości wskaźnika wskf1 wartości wskaźnika wskf2*/
printf("Wartość zmiennej f wynosi: %f \n", *wskf2);
/* przy operacji drukowania wartości zmiennej f, użyto operatora wyłuskania „*", czyli operatora dostępu do wartości zmiennej wskazywanej przez wartość wskaźnika wskf2 */

Operator wyłuskania z powodzeniem można używać przy operowaniu na wartościach komórek zmiennych typu tablicowego. Na przykład, dla deklaracji tablicy tabznak jak powyżej, tzn.

char tabznak[20];

poprawny jest następujący blok kodu:

for(int i = 0; i<20; i++)
  printf(„Element nr: %d tablicy wynosi: %d \n", *(tabznak+i));

Inaczej mówiąc wyrażenia *(tabznak+i) oraz tabznak[i] pobierające wartość kolejnych komórek tablicy są sobie równoważne.

Typy łańcuchowe.
Ze typami wskaźnikowymi, niepodzielnie królującymi w języku C, wiąże się sposób operowania na zmiennych typu łańcuchowego. Do zdefiniowania pewnego literału łańcuchowego o postaci:

char *nazw= „Jan Kot";

posłużył wskaźnik typu łańcuchowego  char * nazw. Taki literał łańcuchowy zawsze jest zakończony znakiem pustym 0 (lub ‘/x0’ w heksadecymalnej reprezentacji zmiennej znakowej), oznaczającym koniec odczytywanego łańcucha znakowego. Aby powyżej zdefiniowany literał łańcuchowy był w pewnym funkcjonalnym sensie równoważny wartości  zmiennej tablicowej tabznak, inicjowanej w następujący sposób:

tabznak[0]=’J’;
tabznak[1]=’a’;
tabznak[2]=’n’;
tabznak[3]=’ ‘;
tabznak[4]=’K’;
tabznak[5]=’o’;
tabznak[6]=’t’;

za ostatnią inicjowaną komórką w tablicy, umieszczonego znaku, musi wystąpić znak pusty "/x0":

tabznak[7]=’\x0’;

Zaletą tak konstruowanych literałów i zmiennych łańcuchowych, jest na przykład to, że w parametrach przekazywanych do funkcji, nie zachodzi potrzeba kopiowania całego łańcucha (jak to zwykle miało miejsce w klasycznej wersji języka Pascal), lecz wystarczy tylko przekazać wskazanie na daną zmienną łańcuchową. Wszelkie funkcje biblioteczne, języka C ,operujące wskaźnikach do łańcuchów znakowych, odczytują dany łańcuch znakowy, aż do napotkania znaku pustego ‘\x0’, sygnalizującego jego koniec. Oczywiście wracając do zdefiniowanej powyżej tablicy tabznak, możliwe jest bardziej bezpośrednie zainicjowanie tej tablicy:

tabznak="Jan Kot";

Na ostatniej pozycji, za ostatnim znakiem w łańcuchu znakowym automatycznie zostanie wpisany znak pusty ‘\x0’ kończący łańcuch znakowy. Brak znaku pustego na końcu łańcucha znakowego mógłby spowodować nieprzewidywalne skutki wykonania programu. Taki łańcuch znakowy jest praktycznie odczytywany aż do przypadkowego wystąpienia znaku pustego lub wystąpienia innego zdarzenia w systemie, które zakończy ten odczyt. Z przedstawionych powyżej faktów wynika jeszcze inna możliwość. Mianowicie, wartość każdego wskaźnika dowolnego typu np. char *, może być traktowana jako adres początkowy tablicy o typie danych zgodnym z typem wskaźnika.
Na przykład:

char liczba= 6;
char *wsk;
void main()
{
 wsk=&liczba;
print("Zawartość kolejnych komórek pamięci:\n);

 for(int i =0; i<20; i++)
  printf("Komórka nr: %d : %d \n", i, wsk[i]);
return;
}

Poniższy program wydrukuje poprawnie tylko zawartość pierwszej komórki tablicy tabznak, reszta zawartości komórek przedstawia sobą przypadkową zawartość 19 bajtów pamięci znajdujących się bezpośrednio za bajtem pamięci zarezerwowanym dla zmiennej liczba.
PIERWSZY PRZYKŁAD
Jako pierwszy poważny przykład wprowadzający do technik posługiwania się zmiennymi wskaźnikowymi, może posłużyć krótki program odwracający zadany tekst.

#include<stdio.h>
#include<conio.h>
char *t="To jest prosty tekst do odwrócenia";     /*zadany tekst do odwrócenia*/
char *p;     /*pomocnicza zmienna wskaźnika p. */
void main()
{
 clrscr();
  p=t;

/* przypisanie adresu początkowego łańcucha znakowego do zmiennej wsk. p. */
  while(*p)
/*iteracje pętli while będą kontynuowane dopóki nie napotkany będzie znak pusty, kończący odwracany łańcuch znakowy, wówczas wyrażenie *p. zwróci wartość równą zeru*/
    p++;     /*przewijanie wartości wskaźnika p. */
/*następna pętla while wycofywania się wartości wskaźnika w kierunku początku łańcucha znakowego*/
 while(p!=t)     /*dopóki nie napotkano początku łańcucha znakowego*/
   {
     p--;      /* zmniejszaj wartość wskaźnika p */
     printf(„%c", *p);
/*drukuj kolejno napotykane pod wskazaniem p. znaki łańcucha znakowego*/
    }
 return;
}

Proszę zauważyć, że użycie instrukcji drukowania bieżącego znaku z całości przewijanego od końca łańcucha znakowego w postaci:

printf(„% s\n",p.);

spowodowałoby drukowanie w oddzielnych liniach coraz dłuższych podłańcuchów całkowitego łańcucha  znakowego oi głowach tych podłańcuchów zbiegających się do początku całego tekstu próbnego.

DRUGI PRZYKŁAD
Drugim przykładem wykorzystania operacji na zmiennych wskaźnikowych jest krótki program dokonujący szyfrowania i deszyfrowania zadanego łańcucha znakowego. Program wykorzystuje operację XOR. Operacja ta realizowana za pomocą operatora dwuargumentowego " ^ ", jest operacją odwrotną do samej siebie, tzn. jeden raz zaszyfrowany za pomocą łańcucha znakowego - klucza szyfrowania tekst, może być z powrotem odszyfrowany z użyciem tego samego klucza. Jest to przykład kryptografii z użyciem klucza symetrycznego.

//demonstracja użycia wskaźników w
//szyfrowaniu dowolnego wybranego tekstu jawnego
//za pomocą ciągu liter będących kluczem szyfrowania
#include<stdio.h>
#include<conio.h>
char *t="To jest tekst gotowy do zaszyfrowania";
char *p; /*zmienna pomocnicza wskaźnikowa, przeglądania znaków tekstu jawnego*/
char *h="XOR"; /*literał znakowy będący kluczem szyfrowania*/
char *cp;  /*zmienna pomocnicza wskaźnikowa ,przeglądania znaków klucza */
char szyfr[45]; /*tablica pomocnicza przechowywania tekstu zaszyfrowanego*/
char jawny[45]; /*tablica pomocnicza przechowywania tekstu odszyfrowanego*/
char i;               /* zmienna pomocnicza indeksująca tablice*/
void main()
{
 clrscr();
 p=t;

/* do zmiennej pomocniczej wskaźnikowej przypisz początek łańcucha tekstu jawnego*/
 cp=h;
/* zmiennej pomocniczej wskaźnikowej przypisz początek łańcucha klucza szyfrowania*/
 while(*p)
/*dopóki nie napotkany będzie koniec łańcucha tekstu jawnego*/
 {
   szyfr[i]=*cp^*p;
/*wykonuj operację XOR na bieżącym znaku tekstu jawnego i bieżącym znaku łańcucha klucza szyfrowania. Wynik szyfrowania umieszczany jest w bieżącej komórce tablicy szyfr*/
   i++;
/*inkrementacja indeksu bieżącej komórki tablicy szyfr */
   p++;
/*inkrementacja wskaźnika na bieżący znak tekstu jawnego*/
   cp++;
/*inkrementacja wskaźnika na bieżący znak klucza szyfrowania*/
   if(!*cp) cp=h;
/*jeśli nastąpi koniec łańcucha klucza szyfrowania, to powtórnie przypisz zmiennej wskaźnikowj cp początek łańcucha klucza szyfrowania*/
 }
                            /*koniec pętli szyfrowania*/
 szyfr[i]=0;         /*umieść znak końca łańcucha tekstu zaszyfrowanego*/
 printf("Szyfr:%s",szyfr);     /* wydrukuj tekst zaszyfrowany*/
 i=0;                                         /* ustaw zmienną indeksacji na wartość początkową*/
 p=szyfr;
/*przypisz do wartości zmiennej wskaźnikowej p. początek tekstu zaszyfrowanego*/
 cp=h;
/* przypisz powtórnie początek łańcucha znakowego klucza do zmiennej wsk. cp. */
 while(*p)
/* dopóki nie napotkany będzie koniec łańcucha tekstu zaszyfrowanego*/
 {
  jawny[i]=*p^*cp;
/*wykonuj operację XOR na bieżącym znaku tekstu zaszyfrowanego i bieżącym znaku łańcucha klucza deszyfracji. Wynik deszyfracji umieszczany jest w bieżącej komiórce tablicy jawny */
  i++;                     /*inkrementacja zmiennej indeksu tablicy jawny */
  cp++;                   /*inkrementacja zmiennej wskaźnikowej cp */
  p++;                     /*inkrementacja zmiennej wskaźnikowej p. */
  if(!*cp)  cp=h;
/*warunkowo, przypisz powtórnie zmiennej wskaźnikowej cp., początek łąńcucha klucza deszyfracji*/
 }
 jawny[i]=0;         /*wpisz znak pusty na końcu tablicy tekstu odszyfrowanego*/
 printf("Jawny:%s",jawny);     /* wydrukuj tekst odszyfrowany*/
 return;
}

TRZECI PRZYKŁAD
Pewną odmianą tego przykładu, może być wersja w której zastosowano mechanizm semafora, na zmiennej st, której wartości 1 i 0 ustawiane są naprzemiennie. Co to oznacza? W początkowym przebiegu pętli while, realizującej proces szyfrowania, przy wartości tego semafora ustawionej na 1,  następujące inkrementacja wskaźnika cp, aż do napotkania końca łańcucha klucza. Wówczas następuje zmiana wartości semafora z 1 na 0, co w rezultacie w każdej następnej iteracji pętli while powoduje deinkrementację wartości wskaźnika cp. Z kolei przy napotkaniu początku łańcucha znakowego klucza wartość zmiennej semafora st zmienia powtórnie na 1. Efektem wprowadzonej modyfikacji, program operuje w trakcie szyfrowania, kluczem przewijanym na zasadzie ping-ponga.

//przykład użycia wskaźników podobny do przykładu wsk2.cpp
// z ta różnica, ze
//hasło przeglądane jest na zasadzie ping-pongu
#include<stdio.h>
#include<conio.h>
char *t="To jest tekst do odwrocenia";
char *p;
char *h="IZK";
char *cp;
char szyfr[45];
char jawny[45];
char i;
char st=1;
void main()
{
 clrscr();
 p=t;
 cp=h;
 while(*p)
 {
   szyfr[i]=*cp^*p;
   i++;
   p++;
   if(st)
    cp++;
   else
    cp--;
   if(!*cp)
    st=0;
   if(cp==h)
    st=1;
 }
 szyfr[i]=0;
 printf("Szyfr:%s",szyfr);
 i=0;
 p=szyfr;
 cp=h;
 st=1;
 while(*p)
 {
  jawny[i]=szyfr[i]^*cp;
  i++;
  if(st)
   cp++;
  else
   cp--;
  p++;
  if(!*cp)
   st=0;
  if(cp==h)
   st=1;
 }
 jawny[i]=0;
 printf("Jawny:%s",jawny);
 return;
}

Pierwszy z tych dwóch powyżej przedstawionych przykładów może być nieznacznie zoptymalizowany:

//demonstracja użycia wskaźników przy okazji
//szyfrowania dowolnego wybranego tekstu jawnego
//za pomocą dowolnego ciągu liter hasło za pomocą
//operacji XOR
//wersja optymalizowana
#include<stdio.h>
#include<conio.h>
char *t="To jest tekst do odwrocenia";
char *p;
char *h="IZK";
char *cp;
char szyfr[45];
char jawny[45];
char i;
void main()
{
 clrscr();
 p=t;
 cp=h;
 while(*p)
 {
   szyfr[i++]=*(cp++)^*(p++);
   if(!*cp)
    cp=h;
 }
 szyfr[i]=0;
 printf("Szyfr:%s",szyfr);
 i=0;
 p=szyfr;
 cp=h;
 while(*p)
 {
  jawny[i++]=*(p++)^*(cp++);
  if(!*cp)
   cp=h;
 }
 jawny[i]=0;
 printf("Jawny:%s",jawny);
 return;
}

W powyższym przykładzie, niezależne instrukcje wykonywanej operacji XOR oraz „przewijania" wskaźników:

szyfr[i]=*cp^*p;
   i++;
   p++;
   cp++; ,

zastąpiono jedną instrukcją:

szyfr[i++]=*(cp++)^*(p++);

wykorzystującą zjawisko inkrementacji w notacji
postfiksowej, czyli inkrementowania zmiennych wskaźnikowych po wykonaniu zasadniczej operacji XOR i przypisania wyniku do bieżącej komórki tablicy szyfr.

Rzecz jasna, program realizujący podobne zadanie można rozwijać na znaczną liczbę sposobów, dokonując przy tym podobnych redukcji nadmiarowości kodu źródłowego w języku C. Trzeba jednak pamiętać, że wiąże się z tym nieodłączne, stałe pogarszanie się dla „twórcy programu" czytelności kodu źródłowego. Dla celów dydaktycznych dalsza ewolucja tego programu w tym kierunku zdaje się być bezcelową. Po za tym program ten, przy całej swojej prostocie nie jest pozbawiony banalnych błędów. Mianowicie w ciągu znaków dowolnie wybranego klucza szyfrowania, mogą się pojawić bieżące znaki identyczne z bieżącymi znakami tekstu szyfrowanego. Wynikiem operacji XOR będzie oczywiście wartość zero, co spowoduje trudności w wyświetlaniu w całości łańcucha tekstu zaszyfrowanego( jest to znak pusty określający koniec łańcucha znakowego).
CZWARTY PRZYKŁAD
Jako ostatnią już propozycję rozbudowy tego programu szyfrującego podany zostanie wydruk programu szyfrującego z użyciem trzech niezależnych kluczy-haseł:

//demonstracja użycia wskaźników przy okazji
//szyfrowania dowolnego wybranego tekstu jawnego
//za pomocą dowolnego ciągu liter hasło za pomocą
//operacji XOR
//jest to modyfikacja programu wsk5.cpp
//program ten uzywa az trzech po kolei haseł do szyfrowania tekstu jawnego
#include<stdio.h>
#include<conio.h>
char *t="To jest tekst do zaszyfrowania";
char *p;
char *h1="ALA";
char *h2="ELKA";
char *h3="MONICA";
char *cp;
char szyfr[45];
char jawny[45];
char i;
char haslo=1;
void main()
{
 clrscr();
 p=t;
 cp=h1;
 while(*p)
 {
   szyfr[i]=*(cp++)^*(p.++);
   i++;
   if(!*cp)
    switch(haslo)
    {
     case 1: {cp=h2;haslo++;};break;
     case 2: {cp=h3;haslo++;};break;
     case 3: {cp=h1;haslo=1;};break;
    }
 }
 szyfr[i]=0;
 printf("Szyfr:%s",szyfr);
 i=0;
 p=szyfr;
 cp=h;
 haslo=1;
 while(*p)
 {
  jawny[i]=*(p.++)^*(cp++);
  i++;
  if(!*cp)
   switch(haslo)
   {
     case 1: {cp=h2;haslo++;};break;
     case 2: {cp=h3;haslo++;};break;
     case 3: {cp=h1;haslo=1;};break;

   }
 }
 jawny[i]=0;
 printf("Jawny:%s",jawny);
 return;
}

Mam nadzieję, że te powyższe skromne uwagi na temat programowania, dotyczące konstruowania i funkcjonowania wyrażeń oraz dotyczące operacji na zmiennych wskaźnikowych staną się pomocne w zrozumieniu wyjątkowości języka C.

POWRÓT DO SPISU TRESCI
POWRÓT DO STRONY GŁÓWNEJ

dnia 10.10.1998r
Data ostatniej modyfikacji 15.04.1999r.
 
 PS: do poprzedniej wersji tychże rozwazań na temat istoty języka C zakradły się pewne błędy w przykładach programowych... Jest to o tyle dziwne, że wszystkie moje przykłady zostały do tej strony  WWW wklejone ze wcześniej sprawdzonych pod kompilatorem Borlanda 3.1 plików kodów źródłowych tych programików. Jednakże zaznaczam, że w przyszłości będę z uwagą śledził losy moich stron WWW, co do wystąpienia dziwnych, nadzwyczaj "inteligentych" chochlików drukarskich. Oczywiście, moje "ciche" podejrzenie pada na serwer "lwa", z którego po uprzednim, finalnym zredagowaniu,  nowwsze wersje moich stron są zwykle przekopiowywane na inne mirrory.
Ponadto, staram się w miarę możliwości, dostosowywać terminologię stosowaną w moich wykładach do terminów technicznych stosowanych powszechnie w polskiej  literaturze technicznej.
mgr inż. Artur Bernat