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--;
return;/*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*/
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;
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;
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;
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 S i deinkrementacji zmiennej i
if(!i) //gdy
wartość zmiennej i 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*/
/*drukuj kolejno napotykane pod wskazaniem p. znaki łańcucha znakowego*/}
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;
cp=h;/* do zmiennej pomocniczej wskaźnikowej przypisz początek łańcucha tekstu jawnego*/
while(*p)/* zmiennej pomocniczej wskaźnikowej przypisz początek łańcucha klucza szyfrowania*/
{/*dopóki nie napotkany będzie koniec łańcucha tekstu jawnego*/
i++;/*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*/
p++;/*inkrementacja indeksu bieżącej komórki tablicy szyfr */
cp++;/*inkrementacja wskaźnika na bieżący znak tekstu jawnego*/
if(!*cp) cp=h;/*inkrementacja wskaźnika na bieżący znak klucza szyfrowania*/
}/*jeśli nastąpi koniec łańcucha klucza szyfrowania, to powtórnie przypisz zmiennej wskaźnikowj cp początek łańcucha klucza szyfrowania*/
cp=h;/*przypisz do wartości zmiennej wskaźnikowej p. początek tekstu zaszyfrowanego*/
while(*p)/* przypisz powtórnie początek łańcucha znakowego klucza do zmiennej wsk. cp. */
{/* dopóki nie napotkany będzie koniec łańcucha tekstu zaszyfrowanego*/
i++; /*inkrementacja zmiennej indeksu tablicy jawny *//*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 */
}/*warunkowo, przypisz powtórnie zmiennej wskaźnikowej cp., początek łąńcucha klucza deszyfracji*/
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