ANSI C, Edukacja, C C++


0x01 graphic

WSKAŹNIKI I TABLICE


0x08 graphic
Wskaźnik jest zmienną, która zawiera adres innej zmiennej. W języku C chętnie ko­rzysta się ze wskaźników częściowo dlatego, że czasami jest to jedyny sposób przed­stawienia algorytmu obliczenia, częściowo zaś dlatego, że ich użycie zwykle prowa­dzi do bardziej zwartego i efektywnego kodu niż otrzymywany innymi sposobami. Wskaźniki i tablice są blisko spokrewnione; w tym rozdziale badamy to pokrewień­stwo i pokazujemy, jak można z niego korzystać.

Wskaźniki wraz z instrukcją goto „cudownie" nadają się do tworzenia zupełnie nie­zrozumiałych programów. Dzieje się tak bez wątpienia wówczas, gdy wskaźniki są stosowane nieostrożnie. Łatwo jest bowiem utworzyć wskaźnik, który wskazuje nie wiadomo na co. Jednak przy przestrzeganiu odpowiedniej dyscypliny dzięki wskaź­nikom można osiągnąć znaczną przejrzystość i prostotę algorytmów. Właśnie ten aspekt spróbujemy zilustrować.

Główna zmiana w ANSI C polega na jawnym określeniu reguł, według których moż­na manipulować wskaźnikami, a więc w efekcie jest uprawomocnieniem tego, co do­brzy programiści od dawna stosują, a czego dobre kompilatory od dawna przestrzega­ją. Dodatkowo zamiast typu char * (wskaźnik do typu char) wprowadzono typ void * (wskaźnik do typu void) jako jedyny poprawny typ dla wskaźnika ogólnego.


0x08 graphic

0x01 graphic

Wskaźniki i adresy


0x08 graphic
Rozpoczniemy od pokazania prościutkiego obrazka, na którym widać, jak jest zorga­nizowana pamięć. W typowej maszynie jest to tablica kolejno numerowanych lub ad­resowanych komórek pamięci; można nimi manipulować indywidualnie lub posługi­wać się całymi grupami sąsiednich komórek. Ogólny stan rzeczy jest taki, że dowolny bajt można traktować jako obiekt typu char, parę jednobajtowych komórek - jako obiekt całkowity typu short, a cztery przylegające do siebie bajty - jako obiekt cał­kowity typu long. Wskaźnik jest grupą komórek (często dwie lub cztery), które mogą pomieścić adres. A więc jeśli c jest obiektem typu char, a p jest wskaźnikiem, który wskazuje na c, to taką sytuację możemy zilustrować następująco:

130


5.4 ARYTMETYKA NA ADRESACH

mentem manipulować. Można nawet jednocześnie stosować oba rodzaje odwołań, o ile wydaje się to stosowne i przejrzyste.

Funkcji wolno przekazać część tablicy, podając wskaźnik do początku podtablicy. Na przykład, jeżeli a jest tablicą, to w obu przypadkach:

f(&a[2])

f(a+2)

funkcji f zostanie przekazany adres podtablicy, która zaczyna się od elementu a[2]. Wewnątrz funkcji f deklaracja parametru formalnego może mieć postać

f(intarr[]) {...} lub

f{int *arr) {...}

Z punktu widzenia funkcji f to, że jej argument wskazuje w rzeczywistości na frag-ment większej tablicy, nie ma żadnego znaczenia.

Można także odwoływać się do elementów tablicy wstecz, tj. p[-1], p[-2] itd., jeśli jest rzeczą pewną, że te elementy istnieją. Taki zapis jest składniowo poprawny i od-nosi się do obiektów, które bezpośrednio poprzedzają obiekt p[0]. Oczywiście odwo-ływanie się do obiektów, które leżą poza granicami tablicy, jest nielegalne.


0x01 graphic

Arytmetyka na adresach


Jeżeli p jest wskaźnikiem do pewnego elementu tablicy, to po zwiększeniu p++ wska-zuje on na następny element, a po zwiększeniu p+=i - na element oddalony o i pozycji od aktualnie wskazywanego. Te i podobne konstrukcje są najprostszymi oraz najczęś-ciej stosowanymi formami arytmetyki na wskaźnikach lub adresach.

Stosowana w języku C arytmetyka na adresach jest spójna i regularna. Związek mię-dzy wskaźnikami, tablicami i arytmetyką na adresach stanowi jedną z najważniej-szych cech świadczących o mocy języka. Zilustrujemy to na przykładzie prymityw-nego programu dystrybutora pamięci. Tworzą go dwa podprogramy. Pierwszy, o na-zwie alloc(n), udostępnia wskaźnik p do obszaru pamięci zajmującego n kolejnych pozycji znakowych; obszar ten można w funkcji wywołującej alloc wykorzystać do przechowywania znaków. Drugi podprogram, o nazwie afree(p), zwalnia uprzednio przydzieloną pamięć tak, aby mogła być ponownie wykorzystana. Ten mechanizm przydziału pamięci jest „prymitywny", ponieważ wywołania afree muszą następować w odwrotnej kolejności do wywołań alloc. Oznacza to, że pamięć obsługiwana przez

139


0x01 graphic

5.1 WSKAŹNIKI I ADRESY

Jednoargumentowy operator & podaje adres obiektu, zatem instrukcja p = &c;

przypisuje zmiennej p adres zmiennej c; teraz zmienna p „wskazuje na" zmienną C. Operator adresu & może być stosowany tylko do obiektów zajmujących pamięć: zmiennych i elementów tablic. Nie można go stosować do wyrażeń, stałych i zmien­nych register.

Jednoargumentowy operator * oznacza adresowanie pośrednie lub odwołanie pośred­nie; zastosowany do wskaźnika daje zawartość obiektu wskazywanego przez ten wskaźnik. Przypuśćmy, że x i y są obiektami całkowitymi, a ip jest wskaźnikiem do obiektu typu int. Następujący sztuczny ciąg instrukcji pokazuje, jak zadeklarować wskaźnik oraz jak stosować operatory & i *:

int x = 1, y = 2,zL10];

int *ip; /* ip jest wskaźnikiem do obiektów typu int */

ip = &x; /* teraz ip wskazuje na x */

y = *ip; /* y ma teraz wartość 1 */

*ip = 0; /* x ma teraz wartość 0 */

ip = &z[O]; /* teraz ip wskazuje na element z[0] */

Deklaracje obiektów x, y i Z znamy już od dawna. Deklaracja wskaźnika ip: int *ip;

z założenia jest mnemotechniczna; mówi ona, że wyrażenie *ip ma wartość typu int. Składnia deklaracji tej zmiennej naśladuje składnię wyrażenia, w którym zmienna może wystąpić. Takie rozumowanie stosuje się również do deklaracji funkcji. Na przykład deklaracja

double *dp, atof(char *);

mówi, że w wyrażeniu obie konstrukcje *dp i atof(s) mają wartości typu double oraz że argument funkcji atof jest wskaźnikiem do typu char.

Zapamiętaj także płynący stąd wniosek: od wskaźnika wymaga się wskazywania obiek­tów określonego rodzaju. (Z jednym wyjątkiem — wskaźnik do void, czyli ,,wskaźnik do niczego", może przechowywać wskaźnik do obiektów dowolnego typu, ale nie można go stosować do adresowania pośredniego. Wrócimy do tego w p. 5.11.)

131


5 WSKAŹNIKI I TABLICE

Jeżeli wskaźnik ip wskazuje na zmienną całkowitą X, to *ip może wystąpić wszędzie tam, gdzie może wystąpić x, a więc na przykład

*ip = *ip + 10;

zwiększa obiekt wskazywany przez ip o 10.

Jednoargumentowe operatory & i * wiążą silniej niż operatory arytmetyczne, zatem w przypisaniu

y = *ip + 1

najpierw bierze się wartość obiektu, na który wskazuje ip, dodaje do niej 1 i wynik przypisuje zmiennej y. Natomiast zwiększenie tego obiektu, na który wskazuje ip, można zapisać tak:

*ip += 1 lub tak:

++*ip lub jeszcze inaczej:

(*ip)++

W tym ostatnim przypadku nawiasy są niezbędne: bez nich efektem obliczenia byłoby zwiększenie wskaźnika ip, a nie obiektu wskazywanego przez ip, gdyż operacje okreś­lone przez jednoargumentowe operatory * i ++ są wykonywane od prawej strony do lewej.

Na koniec, wskaźniki są zwykłymi zmiennymi, mogą więc być używane bez adre­sowania pośredniego. Dla przykładu, jeśli iq jest innym wskaźnikiem do obiektów typu int, to

iq = ip

kopiuje zawartość ip do iq, sprawiając, że iq będzie wskazywać na to samo, na co wskazuje ip.


0x01 graphic

Wskaźniki i argumenty funkcji


0x08 graphic
W języku C argumenty są przekazywane przez wartość. Nie ma więc bezpośredniego sposobu na to, aby wywołana funkcja mogła zmienić wartość zmiennej należącej

132


5.2 WSKAŹNIKI I ARGUMENTY FUNKCJI

do funkcji wywołującej. Na przykład podprogram sortujący mógłby zamieniać miejs­cami dwa nieuporządkowane elementy za pomocą funkcji swap. Nie wystarczy jed­nak napisać

swap(a, b); jeśli funkcja swap jest zdefiniowana następująco:

void swap(int x, int y) /*ŹLE */

{ int temp; temp = x;

y = temp; }

Na skutek przekazywania argumentów przez wartość funkcja ta nie ma dostępu do argumentów a i b należących do wywołującego ją podprogramu. Powyższa funkcja swap jedynie zamienia miejscami wartości kopii argumentów a i b.

Jedyny sposób na osiągnięcie zamierzonego celu polega na przekazaniu przez pro­gram wywołujący wskaźników do obiektów, których wartości należy zamienić miejs­cami:

swap(&a, &b);

Operator & daje adres zmiennej, zatem &a jest wskaźnikiem do a. Deklarując w sa­mej funkcji swap parametry jako wskaźniki, otrzymujemy - za ich pomocą - pośre­dni dostęp do rzeczywistych argumentów operatora adresu.

void swap(int *px, int *py) /* zamień miejscami *px i *py */

{

int temp;

temp = *px; *px = *py;

*py = temp; }

133


5 WSKAŹNIKI I TABLICE


Obrazowo wygląda to tak:

0x01 graphic

w funkcji swap:

w miejscu wywołania:


Argumenty wskaźnikowe pozwalają funkcji mieć dostęp do (a więc i zmieniać war­tość) obiektów należących do funkcji wywołującej. Jako przykład rozważmy funkcję getint, która wczytując nie sformatowane dane wejściowe, przekształca strumień zna­ków na wartości całkowite (jedną na każde wywołanie). Funkcja ta musi przekazać znalezioną wartość oraz sygnał końca pliku, gdy nie ma już więcej znaków z wejścia. Obie wartości muszą być przesłane dwiema oddzielnymi ścieżkami, gdyż niezależnie od tego, jaką wartość ma EOF, może ona być również wartością wczytanej liczby.

Jedno z rozwiązań polega na tym, że funkcja getint zwraca sygnał o napotkaniu końca pliku jako swoją wartość funkcyjną, podczas gdy znalezioną liczbę całkowitą przeka­zuje do funkcji wywołującej za pomocą argumentu wskaźnikowego. Taki sam sche­mat jest podstawą działania standardowej funkcji scanf; zajrzyj do p. 7.4.

W następującej pętli do wypełnienia tablicy wartościami całkowitymi użyto funkcji getint:

int n, array[SIZE], getint(int *);

for (n = 0; n < S1ZE && getint(&array[n]) != EOF; n++)

W każdym kroku pętli wywołanie funkcji getint umieszcza w array[n] wartość kolej­nej wejściowej liczby; zwiększa się też licznik n. Zauważ, że przekazanie do funkcji getint adresu elementu array[n] jest konieczne, inaczej bowiem nie ma sposobu na to, by mogła ona obliczoną wartość przekazać do miejsca wywołania.

134


5.2 WSKAŹNIKI I ARGUMENTY FUNKCJI

Nasza wersja funkcji getint zwraca: EOF - gdy napotka koniec pliku, zero - jeśli kolejne znaki z wejścia nie tworzą liczby, oraz wartość dodatnią - jeśli na wejściu podano poprawną liczbę*.

#include <ctype.h>

int getch(void); void ungetch(int);

/* getint: wczytaj następną liczbę całkowitą */ int getint(int *pn)

{

int c, sign;

while (isspace(c = getch())) /* pomiń białe znaki */

if (! isdigit(c) && c != EOF && c != '+' && c != '-') { ungetch(c); /* to nie jest liczba */ return 0;

}

sign = (c = = '-') ? -1 : 1;

if(c=='+' ||c =='-')

c = getch(); for(*pn = 0; isdigit(c); c = getch())

*pn = 10 * *pn + (c - '0'); *pn *= sign; if (c != EOF)

ungetch(c); return c; }

W całej funkcji getint konstrukcja *pn jest używana jak zwykła zmienna całkowi­ta. Ponadto skorzystaliśmy z funkcji getch i ungetch (opisanych w p. 4.3), aby dodatkowy znak, który trzeba było przeczytać, mógł być z powrotem odesłany na wejście.

0x08 graphic
*Przy takiej definicji funkcji getint warunek w powyższej pętli trzeba uzupełnić sprawdzeniem, czy funk­cja zwraca zero. Przy podanym warunku funkcja getint zatnie się na pierwszym znaku nic należącym do liczby (zawsze sygnalizując zero), mimo że pętla będzie się kręcić dalej. - Przyp. tłum.

135


5 WSKAŹNIKI I TABLICE

Ćwiczenie 5.1. Tak jak została napisana, funkcja getint traktuje znaki + i -, po których nie następuje cyfra, jako poprawne reprezentacje zera. Zmień tę funkcję tak. aby oddawała je z powrotem na wejście.

Ćwiczenie 5.2. Napisz funkcję getfloat - zmiennopozycyjny odpowiednik funkcji getint. Jakiego typu wartość powinna zwracać getfloat jako swoją wartość

funkcyjną?


0x01 graphic

Wskaźniki i tablice


0x08 graphic
W języku C występuje ścisła zależność między wskaźnikami i tablicami, ścisła do tego stopnia, że wskaźniki i tablice powinny być rozpatrywane jednocześnie. Każdą operację, którą można przeprowadzić za pomocą indeksowania tablicy, można rów­nież wykonać za pomocą wskaźników. Wersja wskaźnikowa będzie na ogół szybsza, ale - zwłaszcza dla początkujących - trochę trudniejsza do zrozumienia.

Deklaracja

int a[10];

definiuje tablicę a o rozmiarze 10, a więc blok dziesięciu kolejnych obiektów nazwa­
nych a[0], a[1J a[9J.


0x01 graphic

a:

a[0] a[1] a [9]

Zapis a[i] oznacza i-ty element tablicy a. Niech pa będzie wskaźnikiem do obiektów całkowitych, zadeklarowanym jako

int *pa;

wówczas przypisanie pa- &a[0];

ustawi pa tak, aby wskazywał na zerowy element tablicy a; wskaźnik pa zawiera więc adres elementu a[0].

0x01 graphic

a:

a[0]

pa:

136


5.3 WSKAŹNIKI I TABLICE

Teraz przypisanie

x = *pa; skopiuje zawartość a[0] do x.

Jeśli pa wskazuje na pewien element tablicy, to - z definicji - pa+1 wskazuje na element następny, pa+i odnosi się do i-tego elementu po pa, a pa-i do i-tego elementu przed pa. Jeżeli więc pa wskazuje na a[0], to

*(pa+1) odnosi się do zawartości a[1 ]; pa+i jest adresem a[i], a *(pa+i) jest zawartością a[i].


0x01 graphic

pa:

pa + 1:

pa + 2:

a:

a[0]


Te spostrzeżenia są prawdziwe niezależnie od typu lub rozmiaru elementów tablicy a. Znaczenie operacji „dodawanie 1 do wskaźnika", a szerzej - całej arytmetyki na wskaźnikach, jest oparte na tym, że pa+1 wskazuje następny obiekt, a pa+i odnosi się do obiektu oddalonego od pa o i takich obiektów.

Ścisła odpowiedniość między indeksowaniem i arytmetyką na wskaźnikach jest oczy­wista. Wartością zmiennej lub wyrażenia typu tablica jest z definicji adres zerowego elementu tej tablicy. Zatem po przypisaniu

pa = &a[0];

pa oraz a mają identyczne wartości. Ponieważ nazwa tablicy reprezentuje położenie jej elementu początkowego, przypisanie pa=&a[0] można także napisać w postaci

pa = a;

Większą niespodzianką, przynajmniej na pierwszy rzut oka, będzie to, że odwołanie do a[i] można zapisać jako *(a+i). W języku C wyrażenie a[i] przy obliczaniu jest przekształcane bezpośrednio na *(a+i) - obie te formy są równoważne. Po zastosowa­niu operatora adresu & do obu form widać, że &a[i] oraz a+i także są identyczne: a+i jest adresem elementu oddalonego o i elementów od a. A z drugiej strony, jeśli pa jest wskaźnikiem, to w wyrażeniach może wystąpić z indeksem: pa[i] jest równoważne z *(pa+i). Podsumowując, wyrażenie w postaci „tablica i indeks" jest równoważne z wyrażeniem w postaci „wskaźnik i przesunięcie" (ang. offset).

137


5 WSKAŹNIKI I TABLICE

Między nazwą tablicy a wskaźnikiem jest jednak istotna różnica, o której należy pa­miętać. Wskaźnik jest zmienną, wiec operacje pa=a czy pa++ są dozwolone. Nato­miast nazwa tablicy nie jest zmienną, zatem konstrukcje w rodzaju a=pa i a++ są niedozwolone.

Gdy argumentem funkcji jest nazwa tablicy, wówczas funkcja otrzymuje położenie początkowego elementu tej tablicy. Wewnątrz wywołanej funkcji temu argumentowi odpowiada zwykła zmienna lokalna. Zatem parametr będący nazwą tablicy jest w is­tocie wskaźnikiem, czyli zmienną zawierającą adres. Korzystając z tego, możemy na­pisać inną wersję funkcji strlen obliczającej długość tekstu.

/* strlen: podaj długość tekstu s */ int strlen(char *s)

{

int n;

for (n = 0; *s != '\0'; s++)

n++; return n;

}

Zwiększanie S jest całkowicie poprawne, S jest bowiem zmienną wskaźnikową. Ope­racja s++ nie ma żadnego wpływu na ciąg znaków w miejscu wywołania funkcji Strlen, zwiększa jedynie jej prywatną kopię wskaźnika. Z tego wynika, że wywołania mające postać

strlen("ahoj, przygodo"); /* stała napisowa */
strlen(array); /* char array[100]; */

strlen(ptr); /* char *ptr; */

działają poprawnie.

Następujące definicje parametrów formalnych funkcji:

char s[]; i

char *s;

są równoważne; my dajemy pierwszeństwo tej ostatniej, gdyż wyraźniej mówi o tym, że parametr jest wskaźnikiem. Jeśli do funkcji jest przekazywana nazwa tablicy, to - w zależności od tego, co jest wygodniejsze - w funkcji tej można przyjąć, że jako argument otrzymuje ona albo nazwę tablicy, albo wskaźnik i odpowiednio tym argu-

138



Wyszukiwarka

Podobne podstrony:
edukacja Słowacja
Edukacja a stratyfikacja
Psychologiczne Podstawy Edukacji 1
Psychologiczne podstawy edukacji 11
Dzieci niewidome i ich edukacja w systemie integracyjnym
WYKŁAD EDUKACYJNY M Jędraszczyk ppt
Edukacja przedszkolna we Francji
Edukacja i reedukacja posturalna
ukryty orogram edukacyjny
Specjalne potrzeby edukacyjne
Inteligencje wielorakie Howarda Gardnera w polskiej edukacji przedszkolnej
CELE I ZADANIA EDUKACJI MEDIALNEJ(1)
prezentacja edukacja muzyczna gr 3
Edukacja międzykulturowa wprowadzenie (1)

więcej podobnych podstron