W tej części rozwiniemy pojęcie wskaźników oraz odniesiemy je do tablic, wprowadzimy pojęcie łańcuchów tekstowych oraz... odniesiemy je do wskaźników, poza tym przykłady, przykłady, przykłady...
Tablice i wskaźniki
Z poprzedniej części znacie Państwo pojęcie wskaźników jako zmiennych zawierających adresy innych zmiennych w pamięci. Znacie także pojęcie tablic. Skoro więc tablica zawiera uporządkowany zbiór elementów tego samego typu, to możemy określić wskaźniki na poszczególne elementy np.:
int tab[10]; // tablica 10 elementów (1)
int *wskaznik; //jakiś wskaźnik na int
No i logicznie możemy napisać: wskaźnik = &tab[0]; (2)
Zapis jest oczywiście poprawny. Okazuje się jednak, że nazwa tablicy także reprezentuje adres. Jest to adres obszaru, od którego rozpoczyna się zbiór elementów tablicy. Skoro adres jest „czymś” co może być przypisane wskaźnikowi, możemy więc napisać: wskaznik = tab;
Teraz zmienne wskaznik pokazuje na początek tablicy tab. A wiec jeśli napiszemy *wskaznik to co otrzymamy? Pierwszy element tablicy. A więc zapisy tab[0] = 5; oraz *wskaznik = 5; są równoważne.
To nie wszystko. Jak Państwo zauważyli, wskaźniki, które umiemy deklarować mają określony typ. Tak więc zapis *wskaznik powoduje, że odwołanie dotyczy obszaru wskazywanego przez zmienną o wielkości odpowiadającej zmiennej int (najczęściej 2 bajty). Gdyby wskaźnik miał typ float, odwołanie dotyczyło by oczywiście odpowiednio większego obszaru.
Co wynika z tego, że program wie, jaki tym jest wskazywanej zmiennej? Po pierwsze możemy wykorzystać to w wyrażeniach, ale także możemy dokonywać na wskaźnikach działań arytmetycznych. Możemy na przykład napisać: wskaznik = wkaznik +1; Co to oznacza? Oznacza to, ze zmieni się adres zapisany w zmiennej wskaznik. O ile bajtów? O tyle, na ile skazuje typ zmiennej. W naszym przypadku jest to int, a wiec o 2 bajty.
Gdy dokonamy więc takich przypisań jak (1) i (2) możemy za pomocą zmiennej wskaznik mieć dostęp do całej tablicy tab. Na przykład:
Jeśli wskaźniki dotyczą tego samego typu oraz spójnego obszaru pamięci (na przykład w ramach tablicy), to możemy na wskaźnikach dokonywać operacji dodawania, odejmowania, mnożenia oraz porównywania.
Przykład1. Dla tablicy int tab[MAX] posumować wszystkie elementy korzystając ze wskaźników.
int i, suma = 0, *wsk = tab;
for(i = 0; i<MAX; i++) suma += *wsk++;
Przykład 2. Napisz fragment programu, który w tablicy z poprzedniego zadania wyszuka wartość wskazaną przez użytkownika.
int szukam, i, *wsk = tab;
scanf(”%d”, &szukam);
for(i=0; i<MAX; i++) if (*wsk++ == szukam) printf(”Jest na poz. %d”, i);
Zadania
1)Przerobić przykładowe zadania z tablicami z poprzedniej części na wersje operujące na wskaźnikach.
2)Korzystając ze wskaźników zaproponować realizację działań na stosie: dodawania i pobierania wartości ze stosu.
Łańcuchy znakowe
W standardzie języka C nie został określony specjalny typ do operowania na łańcuchach znakowych (jak STRING w Pascalu). Deklaracje i obsługa łańcuchów znakowych w dużym stopniu spoczywa na programiście. Łańcuch znakowe w języku C reprezentowane są przez tablice znaków ASCII, a więc tablice typu char.
Charakterystyczne jest, że łańcuch znakowy kończy się znakiem ‘’ o numerze 0 w tablicy ASCII. Często przyjmuje się też oznaczenie tego znaku stałą NULL, predefiniowaną w stdio.h.,
Jeśli więc chcemy zapisać łańcuch znakowy to możemy to zrobić tak:
Pamiętajmy, że jeśli nie określimy wielkości tablicy (j.w.) to zostanie ona dobrana automatycznie dla zbioru wartości początkowych.
Oczywiście sposób taki wprowadzania łańcuchów znakowych jest czasochłonny i niestosowany w praktyce. Jak już pewnie zauważyliście wcześniej, łańcuchy znakowe możemy oznaczać tekstem ograniczonym cudzysłowami. Możemy więc powyższą deklarację zastąpić przez:
char napis[] = ”Ala ma kota”;
Efekt będzie ten sam, a znak ‘’zostanie dodany automatycznie na końcu łańcucha.
Oczywiście możemy deklarować „dłuższe” tablice, aby przechować dodatkowe znaki, mając wtedy zapas na dodatkowe znaki. Np.:
char napis_duzy[100] = ”Ala ma kota”;
W tym wypadku możemy wprowadzić do łańcucha dodatkowe znaki (zawsze pamiętając o dodaniu dodatkowego znaku ‘’ na końcu).
A teraz skoro pamiętamy, że nazwa tablicy określa adres obszaru w pamięci, to możemy wszystkie deklaracje tablic char zastąpić przez char*, pisząc np.:
char *tekst = ”Ala ma kota”;
Jest zasadnicza różnica pomiędzy tymi deklaracjami. W pierwszym przypadku zadeklarowana zostanie tablica znaków w określonym obszarze pamięci. W drugim wskaźnik oraz obszar ze znakami, na który wskazywać będzie wskaźnik. Można też definiować tablice określonych nazw, na przykład miesięcy:
Wprowadzanie łańcuchów tekstowych możliwe jest w łatwy sposób za pomocą funkcji scanf oraz formatu %s (string). Na przykład scanf(”%s”,&lancuch); Analogicznie wyprowadzanie zmiennych tekstowych można realizować za pomocą funkcji printf np. printf(”Napis: %s”, lancuch);
Przykład 3: Mając do dyspozycji tablice char i wskaźniki, napisz program do zamiany liczby typu int na jej postać binarną i wyświetl ją na ekranie.
Rozwiązanie problemu obliczania postaci binarnej jest analogiczne jak w części poprzedniej: należy zapisywać resztę z dzielenia liczby przez 2 a dokonywać dzielenia całkowitego tej liczby przez 2. Tym razem jednak cyfry 0 i 1 możemy zapisać w łańcuchu tekstowym, a na końcu dokonać odwrócenia tego łańcucha. A więc:
main() {
char bin[32], *nast=bin; //rezerwujemy miejsca na zera i jedynki
int liczba; //tą liczbę będziemy zamieniać
scanf (”%d”,&liczba);
while (liczba){
if (liczba % 2) *nast++=’1’;
else *nast++=0;
licza/=2;
}
*nast=’/0’;
while(nast-- >= bin) printf(”%c”, *nast);
}
Funkcje biblioteczne operujące na znakach
Dodatkowe operacje na tekstach umożliwiają funkcje predefiniowane w bibliotece string.h. Oczywiste jest więc, że aby z nich skorzystać należy taką bibliotekę dołączyć. Możemy więc na przykład przypisać łańcuch tekstowy, za pomocą predefiniowanej w funkcji: strcpy()do zmiennej s.
strcpy(s, "Ala");
Inne funkcje operujące na tekstach, zawarte w tej bibliotece:
strcpy(dest, scr) kopiująca łańcuch src do dest, zwracająca wskaźnik
do pozycji za ostatnim skopiowanym znakiem;
strcat(dest, src) dołącza łańcuch src na koniec łańcucha dest, zwraca wskaźnik
do łańcucha powstałego w wyniku połączenia;
strchr(lancuch, int c) znajduje pierwsze wystąpienie znaku c w łańcuchu lancuch
i zwraca wskaźnik do pierwszego wystąpienia znaku lub NULL jeśli nie
znalazła szukanego znaku
strrchr(lancuch, int c) podobnie jak powyżej, tylko znajduje ostatnie
wystąpienie znaku c w łańcuchu lancuch
strstr(lancuch1, lancuch2) przeszukuje lancuch1 w poszukiwaniu podłańcucha
lancuch2. Zwraca wskaźnik na początek pierwszego odnalezionego
znaku podłańcucha lub NULL jeśli nie znalazł żadnego wystąpienia.
strtok(lancuch, znaki) pozwala podzielić lancuch na wyrazy oddzielone
odpowiednimi separatorami zawartymi w łańcuchu znaki (np. spacje, przecinki itp) Zwraca w kolejnych wywołaniach wskaźnik na kolejny wyraz (przykład poniżej). Pierwsze wywołanie zwraca adres pierwszego znaku pierwszego słowa w lancuch i umieszcza znak ‘’ za tym słowem. Kolejne
wywołania z parametrem zawierającym adres pusty będą przetwarzać lancuch dalej aż odnalezione
zostaną wszystkie słowa, wtedy funkcja zwróci adres pusty NULL
strcmp(lancuch1, lancuch2) porównuje łańcuch, zwracając: 0 jeśli
lancuch1=lancuch2, <0 jeśli lancuch1<lancuch2, >0 jeśli lancuch1>lancuch2
strlen(lancuch) zwraca długość łańcucha
strlwr(lancuch) zmienia dusze litery A..Z na małe a..z
strup(lancuch) jak wyżej, tylko małe na duże
strrev(lancuch) odwraca kolejność znaków w łańcuchu
Jest tez wiele innych funkcji, zawartych także w innych bibliotekach, np. konwersji na liczby i odwrotnie. Zainteresowanych odsyłam do HELPa lub dokumentacji C.
Inna sprawa, że wszystkie te funkcje można łatwo zrealizować wykorzystując właściwości łańcuchów tekstowych, operacje na wskaźnikach i pętle.
Przykład 4 Korzystając z funkcji tok() podziel wprowadzony od użytkownika
tekst w zmiennej text na słowa, wyświetlając je kolejno na ekranie i zliczając
krotność ich występowania */
char text[200], *sep=" ,;-", *tok;
int cnt=0;
gets(text);
do
{
tok=strtok(cnt++ ? NULL:text,sep);
if (tok) printf("nSlowo nr %d - %s", cnt, tok);
} while (tok);
Dokonujemy operacji na łańcuchach
Ogólna zasada w operacjach na łańcucha znaków za pomocą wskaźników jest prosta: każdy łańcuch tekstowy kończy się (powinien!!!) znakiem ‘/0’, inaczej 0 a jeszcze inaczej stałą NULL. Wykonujemy więc różnego rodzaju pętle, które w warunku mają odwołanie do znaku wskazywanego przez wskaźnik np. while(*lancuch). Jednocześnie powinniśmy pamiętać o zwiększaniu wskaźnika.
Przykład 5. Spróbujmy wiec wyświetlić zawartość zmiennej lancuch „znak po znaku”
char *q=lancuch;
while (*q) putchar(*q++);// putchar(znak) wyświetla jeden znak char na ekranie
Przykład 6. Napisać fragment obliczający długość łańcucha tekstowego lancuch.
Pamiętając, że zmienna lancuch reprezentuje tablicę znaków, możemy wykonać zadanie tradycyjnie, z wykorzystaniem indeksu tablicy:
int k=0;
while (lancuch[k++])
/* od poczatku lancucha tekstowego przesuwamy sie
az napotkamy 0 (koniec lancucha), zwiekszajac licznik k */
;
printf("n s ma %d znakow n",k);
Nieco inaczej wygląda realizacja za pomocą wskaźników:
int k=0;
char *p=lancuch;
do
k++;
while(*p++);
printf("n s ma %d znakow n",k);
Przykład 7. Napisać fragment, który w łańcuchu lancuch znajduje pierwsze wystąpienie znaku zawartego w zmiennej a typu char i podaje jego pozycję.
char *ptr=lancuch;
int poz=0;
while(a!=*ptr && *ptr++)
poz++;
if (*ptr) printf(”Znak %c jest na poz. %d”,a,poz);
else printf(”Nie wystąpił znak %c”,a);
Proszę jednocześnie zauważyć, że pod adresem ptr znajduje się szukany znak, lub znak ‘/0’ jeśli nie znaleziono.
Przykład 8. Skopiować do zmiennej tekstowej fragment n znaków z łańcucha tekstowego tekst.
char *f=fragment, *t=tekst;
while (n--)
*f++=*t++;
*f=’/0’; //pamiętajmy, aby o tym nie zapominać!
Przykład 9. Skopiować do zmiennej fragment n znaków od końca tekstu.
Aby ułatwić sobie zadanie możemy skorzystać z funkcji strlen() zwracającej długość łańcucha. Pozwoli to na ustawienie wskaźnika we właściwym miejscu. To jedyna różnica w stosunku do poprzedniego zadania.
char *f, *t=tekst;
f=fragment + strlen(fragment)-n;
while(*t)
*t++=*f++;
*f=’/0’;
Przykład 10. Napisać fragment programu, który łączy dwa teksty ze zmiennych tekstowych tekst_a i tekst_b, a połączony tekst umieszcza w tekst_a.
char *a=tekst_a, *b=tekts_b;
a=a+strlen(a); //ustawiamy się na koncu
while(*b) *a++=*b++;
*a=’/0’;
Przykład 11. Korzystając z funkcji getchar(), która z bufora klawiatury pobiera jeden znak, napisz program, który pobiera do bufora znaki, tak długo aż napotka na znak końca wiersza (ENTER czyli ASCII 13) lub aż do końca bufora.
Wykorzystamy w tym przykładzie rozkazy preprocesora aby zdefiniować parametry programu. Jako bufor wykorzystamy tablicę znaków ASCII, czyli... oczywiście łańcuch tekstowy.
#define ROZMBUF 40
#define KONIEC 13
main()
{
char bufor[ROZMBUF];
char *wskbuf;
for (wskbuf=bufor; wskbuf<bufor+ROZMBUF; wskbuf++){
*wskbuf=getchar();
if (*wskbuf == KONIEC) break;
}
// teraz dalsza czesc programu, w ktorej mozna wykorzystac zapelniony bufor
}
Jak zmodyfikować to zadanie, aby znak końca wiersza był usuwany w bufora?
Zadania do wykonania
1) Napisz program, który wylosuje 50 różnych wartości i określi wartość środkową (medianę) wartości. Na tablicy należy dokonywać operacji z wykorzystaniem wskaźników.
2) Napisz program kalendarz, który wydrukuje kalendarz na dany miesiąc. Zmienna n zawiera liczbę dni w miesiącu a zmienna numer informuje, od którego dnia tygodnia zaczyna się miesiąc (od 1 do 7, gdzie 1 to poniedziałek). Np. dla n=30 i numer=3 tabelka powinna wyglądać:
P W S C P S N
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
3) Wykonaj kilka testów na swoim kompilatorze, czy Twój kompilator generuje kod równoważny do programów operujących na wskaźnikach zamiast tablic oraz tablicach i indeksach.
4) Napisz fragment programu, który dzieli wprowadzony tekst na wyrazy i wyświetla je w osobnych liniach. Jako wyraz przyjmuje się ciąg znaków oddzielony spacją.
5) Napisz program, który wprowadzony łańcuch tekstowy zamienia na zmienną typu int w ten sposób, że bierze pod uwagę tylko cyfry i tak na przykład „alala2d3dk45” -> 2345
6) Napisać program, który w łańcuchu tekstowym, wprowadzonym przez użytkownika, zlicza liczbę wystąpień cyfr od 0 do 9 i wypisuje te informacje na ekranie. Program ma działać z wykorzystaniem wskaźników.
7) Wykorzystując przykład 7 zamień wszystkie wystąpienia znaku znak (typu char) na znak następny w tabeli znaków ASCII.
8) Napisz program który w tekście wprowadzonym przez użytkownika obliczy najdłuższy ciąg tych samych znaków, na przykład spacji.
9) Napisz program ECHO, który korzystając z getchar() będzie wyprowadzał na ekran wszystkie znaki wprowadzane z klawiatury, aż do napotkania ustalonego znaku specjalnego (np. ‘!’).
10) Napisz program, który we wprowadzony przez użytkownika łańcuchu tekstowym łancuch znajdzie pierwsze wystąpienie ustalonego podłańcucha podłańcuch. Po wykonaniu tego fragmentu wskaźnik wsk ma pokazywać na początek tego podłańcucha lub na znak ‘/0’.