1 Przetwarzanie tablic znakowych Ćwiczenie to poświęcone jest poznaniu i przetrenowaniu metod przetwarzania tablic znakowych. Tablice znakowe w języku C i C++ umożliwiają przetwarzanie napisów. Umiejętność sprawnego operowania napisach jest ważna, szczególnie aktualnie, kiedy powszechnym jest przetwarzanie tekstów w formacie HTML czy XML. W ramach tego ćwiczenia przewidziane jest wykonanie zbioru funkcji służących do przetwarzania tablic znakowych. 1.1 Przetwarzanie tablic znakowych w pigułce Ponieważ ćwiczenia często wyprzedzają wykład, poniżej przedstawiam streszczenie materiału wykładowego poświęconego tablicom znakowym. Zawiera ono podstawowe informacje związane z deklarowaniem takich tablic i wykonywaniem na nich podstawowych operacji. 1.1.1 Tablice znakowe deklaracja, reprezentacja wewnętrzna Tablice znakowe nie różnią się od innych tablic w języku C. Tablica jest zmienną złożoną z elementów tego samego typu, w tym przypadku są to elementy typu char. Istotnym problemem związanym z reprezentowaniem napisów jest ich zmienna długość. Załóżmy, że zdefiniowano następującą zmienną tablicową: char imie[ 80 ]; Następnie do tablicy imie wczytujemy ze strumienia wejściowego (klawiatury) napis. Załóżmy, że to jest trzyznakowy napis "Aga". printf( "Podaj imi ę:" ) gets( imie ); W tym momencie 80-cio znakowa tablica imie zawiera trzyznakowy napis. Jak można określić, że z owych 80-ciu znaków interesują nas tylko pierwsze trzy? Zmienna imie : A g a \0... 0 1 2 3 4 5 78 Rysunek 1: Reprezentacja wewnętrzna napisu 79 W języku C i C++ przyjęto koncepcję łańcuchów ze znacznikiem końca (ang. null terminated strings). Oznacza to, że na końcu właściwego napisu dopisany jest znacznik, informujący, że w tym miejscu napis się kończy. Ilustruje to Rysunek 1. Znacznikiem tym jest znak o kodzie ASCII równym zero, reprezentowanym symbolicznie w języku C w postaci literału '\0'. 1
Literał łańcuchowy "Aga" w istocie jest reprezentowany nie na trzech znakach, a na czterech. Kompilator dla takiego napisu rezerwuje o jeden bajt w pamięci więcej, tak by zmieścić jeszcze znacznik końca napisu. Wszystkie funkcje biblioteczne języka C zakładają, że na końcu napisu jest umieszczony znacznik '\0'. Co się stanie, gdy go nie będzie? Większość funkcji zgłupieje i będzie przetwarzała feralny napis tak długo, aż napotka w pamięci operacyjnej bajt o wartości 0. Tak bowiem w pamięci operacyjnej reprezentowany jest znacznik '\0'. Należy zwrócić uwagę, że zainicjowanie tablicy na etapie jej deklaracji w sposób następujący: char imie[ 80 ] = A, g, a ; nie dopisuje jawnie znacznika końca. Jednak w przypadku inicjowania tablic (dowolnych, nie tylko znakowych), gdy wyrażeń inicjalizujących jest mniej niż elementów tablicy, w miejsce brakujących inicjatorów wpisywane są wartości zerowe. I tak, przypadkiem bajt o wartości 0 znajdzie się tuż za znakiem '\a'. Można oczywiście zadeklarować i zainicjować tablicę imię w następujący sposób: char imie[ 80 ] = A, g, a, \0 ; co zapewni niezawodne dopisanie znacznika końca we właściwym miejscu. Zwróćmy jednak uwagę, że oba powyższe sposoby inicjowania tablic nie są wygodne. Dlatego dla tablic znakowych dozwolone jest inicjowanie zawartością odpowiedniego literału łańcuchowego: char imie[ 80 ] = "Aga"; Jak wiemy, literał taki zawiera znacznik końca napisu, i zostanie on wstawiony do zmiennej imie. Ilustruje to Rysunek 2. Literał łańcuchowy : A g a \0 0 1 2 3 Zmienna imie : A g a \0... 0 1 2 3 4 5 78 79 Rysunek 2: Literał łańcuchowy inicjalizujący zmienną imie Jak reprezentowany jest napis pusty? To proste znacznik końca napisu występuje na samym jego początku zobacz Rysunek 3. Zmienna imie: \0... 0 1 2 3 4 5 78 79 Rysunek 3: Reprezentacja napisu pustego Na etapie definiowania tablic można je uczynić pustymi wykorzystując jeden z podanych dalej sposobów. char imie[ 80 ] = \0 ; 2
lub w sposób bardzie popularny: char imie[ 80 ] = ""; Uczynienie łańcucha pustym po jego zadeklarowaniu zrealizować można następująco: imie[ 0 ] = \0 ; 1.1.2 Podstawowe operacja na tablicach znakowych Przetwarzanie tablic polega zwykle na przemaszerowaniu zmienna indeksową po tablicy, dopóki nie ma końca napisu oznaczanego znakiem \0 : int i; char s[ 80 ];... for( i = 0; s[ i ]!= \0 ; i++ ) < tu jakie ś operacje na każdym znaku s[ i ] > Wyprowadzanie napisu do stdout znak po znaku: void put_string( char s[] ) int i; for( i = 0; s[ i ]!= \0 ; i++ ) putchar( s[ i ] ); choć wielu programistów napisze to tak: void put_string( char s[] ) int i; for( i = 0; s[ i ]!= \0 ; putchar( s[ i++ ] ) ) ; Do manipulowania tablicami znakowymi opracowano szereg funkcji bibliotecznych (plik nagłówkowy string.h)... większość z nich można łatwo napisać samemu! Gdy trzeba znać liczbę znaków w łańcuchu, używamy funkcji strlen. Jej rezultatem jest liczba znaków napisu, przekazanego tej funkcji parametrem. #define N 80 char imie[ N ] = "Aga";... printf( "\nliczba znaków w %s wynosi:%d", imie, strlen( imie ) ); Dwie możliwe implementacje funkcji strlen: int strlen( char s[] ) /* Wersja 1-sza */ int len = 0; while( s[ len ]!= '\0' ) len++; return len; 3
int strlen( char s[] ) /* Wersja 2-ga */ int len; for( len = 0; s[ len ]!= '\0'; len++ ) ; return len; Przykładowe funkcje operujące na tablicach znakowych: char a[ N ] = Ala ; char b[ N ]; char C[ N ]; /*Przepisanie zawartości a do b, teraz w b to samo co w a: Ala */ strcpy( b, a ); /*Tak samo należy przepisywać zawartość literałów łańcuchowych */ strcpy( c, ma kota ); /* Można równie ż tak zerować tablic ę znakow ą */ strcpy( b, ); /* Ale szybciej jest tak: b[ 0 ] = \0 */ /* Łączenie (sklejanie) napisów. Zawartość c zostaje doklejona do a : Ala ma kota */ strcat( a, c ); /* Porównywanie łańcuchów znakowych realizuje funkcja strcmp */ printf( \nłańcuchy znaków %s i % s, a, b ) if( strcmp( a, c )== 0 ) printf( s ą jednakowe ); else printf( s ą różne ); /* Porównywanie łańcuchów znakowych bez uwzględnienia wielkości liter, funkcja stricmp */ strcpy( a, ALA ); strcpy( b, ala ); printf( \nłańcuchy znaków %s i % s, a, b ) if( stricmp( a, b ) == 0 ) printf( s ą jednakowe ); else printf( s ą różne ); Uwaga mimo iż wolno inicjalizować tablicę literałem łańcuchowym na etapie deklaracji: char imie[ 80 ] = "Aga"; nie wolno przypisywać literałów łańcuchowych do tablic: imie = "Aga";<<= Błąd oraz tablic między sobą: char imie[ 80 ] = "Aga"; char drugie[ 80 ]; drugie = imie; <<= Błąd 4
Jak zatem przypisać do tablicy znakowej literał lub zawartość innej tablicy? Należy do tego użyć funkcji kopiowania łańcuchów znakowych strcpy. strcpy( imie, "Aga" ); strcpy( drugie, imie ); Jak działa funkcja kopiowania łańcuchów? Funkcja maszeruje po kolejnych znakach łańcucha źródłowego (aż do napotkania znacznika końca) i kopiuje zawartość każdego odwiedzonego elementu tablicy źródłowej do odpowiedniego elementu tablicy docelowej. Ilustruje to poglądowo Rysunek 4. char s1[ 80 ] = "Język C"; char s2[ 20 ];... strcpy( s2, s1 ); s i++ s1... J ê z y k C \0... 0 1 2 3 4 5 6 79 d s2 J ê z y k C \0... 0 1 2 3 4 5 void strcpy( char d[], char s[] ) int i; for( i = 0; s[ i ]!= '\0'; i++ ) d[ i ] = s[ i ]; d[ i ] = '\0'; Rysunek 4: Ilustracja działania funkcji strcpy Inne operacja na napisach konwersja wielkości liter. 6 19 strcpy( a, ALA ); strcpy( b, ala ); /* Konwersja liter - małe na duże: strupr, duże na małe: strlwr */ strlwr( a ); /* Po wywołaniu strlwr a zawiera napis ala */ strupr( b ); /* Po wywołaniu strupr a zawiera napis ALA */ Przykładowe implementacje funkcji strlwr i strupr mogą mieć następującą postać: void strupr( char s[] ) int i; for( i = 0; s[ i ]!= '\0'; i++ ) s[ i ] = toupper( s[ i ] ); void strlwr( char s[] ) int i; 5
for( i = 0; s[ i ]!= '\0'; i++ ) s[ i ] = tolower( s[ i ] ); Funkcje konwersji wielkości liter wykorzystują do zamiany wielkości pojedynczych znaków odpowiednio funkcji tolower i toupper. Mogą one mieć następującą postać (dla kodowania znaków zgodnego z ASCII): char tolower( char c ) return ( c >= 'A' && c <= 'Z' )? c + 32 : c; char toupper( char c ) return ( c >= 'a' && c <= 'z' )? c - 32 : c; Oczywiście powyższe funkcje występują w bibliotekach jeżyka C, znaleźć je można w pliku nagłówkowym ctype.h. 1.2 Zestaw funkcji do własnoręcznej implementacji W ramach ćwiczeń należy zaimplementować funkcje opisane w przedstawionym dalej programie-szablonie. 1.2.1 Funkcja str_len Własna implementacja funkcji wyznaczającej długość napisu liczoną w znakach, prototyp: int str_len( char s[] ); char s[] tablica zawierająca napis, którego długość jest wyznaczana. Długość napisu liczona w znakach, czli liczba znaków przed znacznikiem końca napisu '\0'. char imie[ 80 ] = "Kunegunda"; int l = str_len( imie ); printf( "Dlugosc napisu %s to %d", imie, l ); 1.2.2 Funkcja lwr_str_cnt Wyznacza liczbę małych liter w napisie przekazanym parametrem, prototyp: int lwr_str_cnt( char s[] ); 6
char s[] tablica zawierająca napis, w którym należy wyznaczyć liczbę małych liter. Liczba małych liter w napisie s. char napis[ 80 ] = "ABCabc"; int l = lwr_str_cnt( napis ); printf( "Liczba malych liter w napisie %s to %d", napis, l ); 1.2.3 Funkcja upr_str_cnt Wyznacza liczbę dużych liter w napisie przekazanym parametrem, prototyp: int upr_str_cnt( char s[] ); char s[] tablica zawierająca napis, w którym należy wyznaczyć liczbę dużych liter. Liczba dużych liter w napisie s. char napis[ 80 ] = "ABCabc"; int l = upr_str_cnt( napis ); printf( "Liczba duzych liter w napisie %s to %d", napis, l ); 1.2.4 Funkcja dgt_str_cnt Wyznacza liczbę cyfr w napisie przekazanym parametrem, prototyp: int dgt_str_cnt( char s[] ); char s[] tablica zawierająca napis, w którym należy wyznaczyć cyfr. Liczba cyfr w napisie s. char napis[ 80 ] = "ABC123"; int l = dgt_str_cnt( napis ); printf( "Liczba cyfr w napisie %s to %d", napis, l ); 1.2.5 Funkcja nalpha_str_cnt Wyznacza liczbę liter i cyfr w napisie przekazanym parametrem, prototyp: 7
int nalpha_str_cnt( char s[] ); char s[] tablica zawierająca napis, w którym należy wyznaczyć liczbę liter i cyfr. Liczba liter i cyfr w napisie s. char napis[ 80 ] = "ABC123"; int l = nalpha_str_cnt( napis ); printf( "Liczba liter i cyfr w napisie %s to %d", napis, l ); 1.2.6 Funkcja chr_str_cnt Wyznacza liczbę wystąpień znaku (pierwszy parametr) w napisie przekazanym drugim parametrem, prototyp: int chr_str_cnt( char c, char s[] ); char c znak poszukiwany w napisie, char s[] tablica zawierająca napis do przeszukania. Liczba wystąpień znaku c w napisie s. char napis[ 80 ] = "Jezyk c i C++"; int l = chr_str_cnt( 'C', napis ); printf( "Liczba wystapien znaku %c w napisie %s to %d", 'C', napis, l ); 1.2.7 Funkcja chr_str_cnt Wyznacza pozycję (indeks) pierwszego wystąpienia znaku (pierwszy parametr) w napisie przekazanym drugim parametrem, prototyp: int chr_str_pos( char c, char s[] ); char c znak poszukiwany w napisie, char s[] tablica zawierająca napis do przeszukania. Pozycja (indeks) pierwszego wystąpienia znaku c w napisie s. 8
char napis[ 80 ] = "Jezyk c i C++"; int l = chr_str_pos( 'C', napis ); printf( "Znak %c wystepuje w napisie %s na pozycji %d", 'C', napis, l ); 1.2.8 Funkcja chr_str_pos Wyznacza pozycję (indeks) pierwszego wystąpienia znaku (pierwszy parametr) w napisie przekazanym drugim parametrem, prototyp: int chr_str_pos( char c, char s[] ); char c znak poszukiwany w napisie, char s[] tablica zawierająca napis do przeszukania. Pozycja (indeks) pierwszego wystąpienia znaku c w napisie s lub 1, gdy znak nie został znaleziony. char napis[ 80 ] = "Jezyk c i C++"; int l = chr_str_pos( 'C', napis ); if( l!= -1 ) printf( "Znak %c wystepuje w napisie %s na pozycji %d", 'C', napis, l ); else puts( "Znak nie został znaleziony" ); 1.2.9 Funkcja chr_str_rpos Wyznacza pozycję (indeks) ostatniego wystąpienia znaku (pierwszy parametr) w napisie przekazanym drugim parametrem, prototyp: int chr_str_rpos( char c, char s[] ); char c znak poszukiwany w napisie, char s[] tablica zawierająca napis do przeszukania. Pozycja (indeks) ostatniego wystąpienia znaku c w napisie s lub 1 gdy znak nie został znaleziony. char napis[ 80 ] = "Jezyk c i C++"; int l = chr_str_pos( 'C', napis ); 9
if( l!= -1 ) printf( "Znak %c w %s wystepuje na pozycji %d, liczac od konca", 'C', napis, l ); else puts( "Znak nie został znaleziony" ); 1.2.10 Funkcja str_rev Odwraca kolejność znaków w napisie przekazanym parametrem, prototyp: void str_rev( char s[] ); char s[] tablica zawierająca napis do odwrócenia. brak. char napis[ 80 ] = "Jezyk C i C++"; printf( "Napis przed odwroceniem %s: ", napis ); str_rev( napis ); printf( "Napis po odwroceniu %s: ", napis ); 1.3 Szablon programu Przedstawiony niże kod stanowi szablon programu, pozwalającego na realizację opisanych wyżej funkcji. Oczywiście ciała funkcji znajdujące sie w szablonie są puste, ich napisanie jest przedmiotem tego ćwiczenia. napis_s.c Szablon do cwiczenia wpisaniu funkcji operujacych na lancuchach znakowych #include <stdio.h> #include <stdlib.h> #define MAX_LEN 128 int str_len( char s[] ) Wlasna implementacja funkcji wyznaczajacej dlugosc napisu liczona w znakach ktorego dlugosc jest wyznaczana 10
dlugosc napisu liczona w znakach int str_len( char s[] ) int lwr_str_cnt( char s[] ) Wyznacza liczbe malych liter w napisie s Liczba malych liter w napisie s int lwr_str_cnt( char s[] ) int upr_str_cnt( char s[] ) Wyznacza liczbe duzych liter w napisie s Liczba duzych liter w napisie s int upr_str_cnt( char s[] ) int dgt_str_cnt( char s[] ) Wyznacza liczbe cyfr w napisie s Liczba cyfr w napisie s int dgt_str_cnt( char s[] ) int nalpha_str_cnt( char s[] ) Wyznacza liczbe liter i cyfr w napisie s 11
Liczba liter i cyfr w napisie s int nalpha_str_cnt( char s[] ) int chr_str_cnt( char c, char s[] ) Wyznacza liczbe wystapien znaku c w s char c -- znak poszukiwany w napisie Liczba wystapien znaku c w napisie s int chr_str_cnt( char c, char s[] ) int chr_str_pos( char c, char s[] ) Wyznacza indeks (pozycje w napisie) pierwszego wystapienia znaku c w s char c -- znak poszukiwany w napisie Pozycja znaku w tablicy liczona od 0 lub -1 gdy znak nie znaleziony int chr_str_pos( char c, char s[] ) int chr_str_rpos( char c, char s[] ) Wyznacza indeks (pozycje w napisie) ostatniego wystapienia znaku c w s char c -- znak poszukiwany w napisie 12
Pozycja znaku w tablicy liczona od 0 lub -1 gdy znak nie znaleziony int chr_str_rpos( char c, char s[] ) void str_rev( char s[] ) Odwraca kolejnosc znakow w tablicy s Brak void str_rev( char s[] ) /* Zadaniem fukcji main jest przetestowanie zaimplementowanych wyzej funkcji */ int main() char line[ MAX_LEN ]; char c; int pos; printf( "\n\nwpisz linie tekstu: " ); gets( line ); printf( "\nliczba znakow : %d", str_len( line ) ); printf( "\nliczba malych liter : %d", lwr_str_cnt( line ) ); printf( "\nliczba duzych liter : %d", upr_str_cnt( line ) ); printf( "\nliczba cyfr : %d", dgt_str_cnt( line ) ); printf( "\nliczba innych znakow : %d", nalpha_str_cnt( line ) ); printf( "\n\npodaj pojedynczy znak: " ); c = getchar(); fflush( stdin ); printf( "\nliczba wystapien znaku %c : %d", c, chr_str_cnt( c, line ) ); if( ( pos = chr_str_pos( c, line ) )!= -1 ) printf( "\npierwsze wystapienie znaku %c od poczatku : %d", c, pos + 1 ); else printf( "\nznak %c nie zostal znaleziony", c ); if( ( pos = chr_str_rpos( c, line ) )!= -1 ) printf( "\npierwsze wystapienie znaku %c od konca : %d", c, pos + 1 ); else printf( "\nznak %c nie zostal znaleziony", c ); printf( "\n\nnapis oryginalny : %s", line ); str_rev( line ); 13
printf( "\nnapis odwrocony : %s", line ); ( void )getchar(); return EXIT_SUCCESS; 14