W tradycyjnym C typ char i char* są generycznymi typami służącymi do bezpośrednich odniesień do pamięci. Niektóre operacje na napisach są więc bardzo podobne do podanych wcześniej operacji działających bezpośrednio na pamięci (patrz rozdział o zarządzaniu pamięcią ).
Napis jest traktowany jak tablica znaków zawierająca znak NUL na końcu. Nazwa NUL (przez jedno 'L') jest nazwą znaku, ale nie odpowiada żadnej nazwie zdefiniowanej w języku. Literałem tego znaku jest ' \0' — nie jest to w żadnym przypadku NULL (taki symbol, definiowany za pomocą dyrektywy preprocesora, oznacza wskaźnik pusty).
Należy pamiętać, że
Aby napis był modyfikowalny, należy go zadeklarować jawnie jako tablicę:
1. char* s1 = "Ula"; // Niemodyfikowalny, wymiar 4 2. char s2[] = "Ula"; // Modyfikowalny, wymiar 4 3. char s3[] = {'U', 'l', 'a', '\0'} // Jak s2Definicje C-napisów s2 i s3 są w zasadzie równoważne. Forma z linii 2 jest oczywiście wygodniejsza, bo wymaga mniej pisania. Zauważmy, że w obu pierwszych formach kompilator sam zadba o wstawienie jako ostatniego znaku tablicy znaku NUL (czyli ' \0'). Używając formy z linii 3 musimy sami o tym pamiętać. We wszystkich przypadkach tablica zawierać będzie cztery znaki: trzy litery słowa Ula i znak NUL. W dwóch ostatnich przypadkach jest to zwykła tablica, utworzona na stosie. Natomiast tablica utworzona w sposób przedstawiony w linii 1 jest alokowana gdzie indziej i jest niemodyfikowalna. Należy o tym pamiętać przy przekazywaniu tego rodzaju napisów do funkcji. Typem s1 jest char*, a więc kompilator zgodzi się na wysłanie s1 do funkcji, których odpowiedni parametr nie był wcale zadeklarowany jako const char*. Tym niemniej, próba modyfikacji takiego napisu przez funkcję skończy się załamaniem programu. Używając zatem formy z linii 1 lepiej, mimo że nie jest to przez język wymagane, samemu zadeklarować typ wskaźnika jako const char*. Kompilator będzie wówczas sprawdzał, czy funkcje, do których napis ten wysyłamy, „obiecują”, poprzez deklarację typu parametru jako const char*, że napisu tego nie zmodyfikują.
Podobnie jest z literałami napisowymi użytymi jako argument funkcji; jeśli wywołamy funkcję fun o parametrze typu char* z literałem jako argumentem, na przykład func("Jan"), to chociaż parametr nie jest zadeklarowany jako niemodyfikowalny, próba zmiany zawartości napisu wewnątrz funkcji nie uda się!
Do funkcji działających na C-napisach mamy dostęp poprzez dołączenie pliku nagłówkowego cstring. Funkcje operujące na pojedynczych znakach zawarte są w cctype, przydatne funkcje konwertujące natomiast — w cstdlib.
Wymieńmy najważniejsze z funkcji dostarczanych w pliku nagłówkowym cstring(string.h w C). Należą one do standardu C i zawsze są dostępne.
size_t strlen(const char* s) — podaje długość napisu s w znakach nie licząc znaku NUL na jego końcu. Typ size_t jest pewnym typem całościowym.
char* strcat(char* dest, const char* src) — dodaje zawartość napisu src do napisu dest (konkatenacja); zwraca dest. Użytkownik musi zadbać o to, by dest i src kończyły się znakiem NUL i żeby obszar pamięci zarezerwowany pod adresem wskazywanym przez dest był wystarczający do pomieszczenia obu napisów (i kończącego znaku NUL). Pierwszy znak NUL w napisie dest jest nadpisywany przez pierwszy znak scr i poczynając od tej pozycji przekopiowywane są znaki z src aż do kończącego go znaku NUL włącznie. Częsty błąd polega na dodawaniu do niezainicjowanego napisu
char s[100]; strcat(s,"jakis napis");co jest błędem, bo program będzie szukał znaku NUL w napisie s, ale go nie znajdzie!
char* strncat(char* dest, const char* src, size_t n) — jest podobne do funkcji poprzedniej, tylko kopiuje co najwyżej n znaków, kończąc kopiowanie, jeśli napotkany i przekopiowany został znak NUL. Jeśli znak NUL nie został napotkany po przekopiowaniu n znaków, to go dostawia jako n+1-szy znak. W takim przypadku zapisanych więc zostanie w sumie n+1 znaków! Na przykład
char t1[20] = {'\0'}; // konieczne ! strncat(t1,"123456789",5); cout << t1 << " ma " << strlen(t1) << " znakow" << endl;wydrukuje '12345 ma 5 znakow', co świadczy o tym, że w tablicy t1 zapisanych zostało 6 znaków: pięć cyfr '12345' i znak NUL.
char* strcpy(char* dest, const char* src) — kopiuje znaki od wskazywanego przez src do obszaru wskazywanego przez dest. Dotychczasowa zawartość obszaru pamięci od adresu dest jest nadpisywana. Kopiowanie kończy się, gdy przekopiowanym znakiem jest znak NUL. Użytkownik musi zatem zadbać o to, żeby pod adresem dest była zarezerwowana odpowiednia ilość pamięci i żeby napis src kończył się znakiem NUL! Funkcja zwraca dest. Na przykład funkcja strcat mogłaby być zaimplementowana następująco:
char* strcat(char* dest, const char* src) { char* s = dest + strlen(dest); strcpy(s, src); return dest; }za pomocą strcpy i strlen.
char* strncpy(char* dest, const char* src, size_t n) — kopiuje dokładnie n znaków od wskazywanego przez src do obszaru wskazywanego przez dest. Dotychczasowa zawartość obszaru pamięci od adresu dest jest nadpisywana. Gdy napotkany zostanie znak NUL, jest kopiowany i dalej wpisywane są do obszaru docelowego same znaki NUL, aż całkowita liczba zapisanych znaków będzie wynosić dokładnie n. Jeśli wśród n pierwszych znaków w napisie źródłowym src znaku NUL nie będzie, to przekopiowanych zostanie n znaków a NUL dostawiony nie będzie.
int strcmp(const char* s1, const char* s2)
—
porównuje leksykograficznie („słownikowo”) dwa napisy,
zwracając -1, jeśli napis
s1
jest wcześniejszy
niż
s2, +1, jeśli
s1
jest
późniejszy, i 0, jeśli napisy są identyczne.
Napis
s1
jest wcześniejszy, jeśli zachodzi któryś
z poniższych warunków:
a) napisy są takie same aż do pewnej pozycji, a na pierwszej
pozycji, na której występuje różnica, znak w
s1
jest numerycznie mniejszy od odpowiadającego
znaku z
s2;
b) napis
s1
jest krótszy niż
s2
i wszystkie jego znaki (nie licząc kończącego znaku
NUL) są takie same jak znaki w
s2
na
odpowiadających pozycjach.
Oba napisy muszą oczywiście być zakończone znakiem
NUL. Na przykład
if ( ! strcmp(s1,s2) ) cout << "Takie same\n"; else cout << "Rozne\n";zadziała, bo jakakolwiek wartość niezerowa zwrócona przez strcmp zostanie potraktowana jak true, a po zanegowaniu operatorem '!' jak false.
1. #include <iostream> 2. #include <cstring> 3. using namespace std; 4. 5. void first_last(char**,char*&,char*&); 6. 7. int main() { 8. 9. char *nam[] = { "Katarzyna", "Magdalena", 10. "Alicja", "Wanda", 11. "Izabela", "Aldona", "" }, 12. *p,*q; 13. 14. first_last(nam,p,q); 15. 16. cout << "Pierwsza: " << p << endl 17. << "Ostatnia: " << q << endl; 18. } 19. 20. void first_last(char** s, char*& p, char*& q) { 21. p = q = *s; 22. while ( **++s ) { 23. if ( strcmp(*s, p) < 0 ) p = *s; 24. if ( strcmp(*s, q) > 0 ) q = *s; 25. } 26. }
Zauważmy, że nie przesłaliśmy do funkcji wymiaru tablicy napisów (liczby imion); zamiast tego jako ostatni napis w tablicy umieściliśmy (linia 11) napis pusty — złożony wobec tego tylko ze znaku NUL, który zostanie automatycznie dodany przez kompilator. Jest to technika bardzo często stosowana w wielu funkcjach bibliotecznych C (dla tablic napisów). Kod funkcji first_last jest tu bardzo zwięzły; mógłby być dłuższy, ale bardziej czytelny. Jednak zrozumienie go w podanej formie jest dobrym ćwiczeniem na posługiwanie się wskaźnikami. Program drukuje oczywiście 'Pierwsza: Aldona Ostatnia: Wanda'.
int strcoll(const char* s1, const char* s2) — działa jak funkcja strcmp, tylko uwzględnia lokalizm. Na przykład, jeśli wybrane jest polskie locale, to litera 'ą' jest za 'a', natomiast w wersji francuskiej 'â' jest równoważne zwykłemu 'a' przy porównywaniu leksykalnym.
int strncmp(const char* s1, const char* s2, size_t n) — jak funkcja strcmp, tylko porównuje co najwyżej n znaków.
char* strchr(const char* s, int c) — zwraca wskaźnik do pierwszego wystąpienia znaku (char)c w napisie s lub wskaźnik pusty nullptr (NULL), jeśli tego znaku w tym napisie nie ma. Na przykład
cout << strchr("Daniel Defoe",'f') << endl;wypisze 'foe'. Funkcja zliczająca liczbę wystąpień znaku (char)c w napisie s mogłaby mieć postać
int count(const char* s, int c) { int n = 0; while (s = strchr(s,c)) ++s,++n; return n; }W szczególności można za pomocą strchr szukać znaku NUL.
char* strrchr(const char* s, int c) — zwraca wskaźnik do ostatniego wystąpienia znaku (char)c w napisie s lub wskaźnik pusty nullptr (NULL), jeśli tego znaku w tym napisie nie ma.
size_t strspn(const char* s, const char* set) — zwraca długość najdłuższego początkowego podciągu w s który składa się wyłącznie ze znaków zawartych w set (w szczególności może to być zero lub długość całego napisu s). Na przykład
cout << strspn("sound and fury","os nudda") << endl;wypisze 10.
size_t strcspn(const char* s, const char* set) — zwraca długość najdłuższego początkowego podciągu w s, który składa się wyłącznie ze znaków nie zawartych w napisie set. Na przykład
cout << strcspn("sound and fury","xyztdv") << endl;wypisze 4.
char* strpbrk(const char* s, const char* set) — zwraca wskaźnik do pierwszego znaku w s, który jest zawarty w set, lub nullptr, jeśli w set nie ma znaków występujących w s. Na przykład
cout << strpbrk("Daniel Defoe","wKlor") << endl;wypisze 'l Defoe'.
char* strstr(const char* s, const char* sub) — zwraca wskaźnik do pierwszego znaku w s, od którego rozpoczyna się w nim podciąg identyczny z sub (pierwsze jego wystąpienie). Funkcja zwraca wskaźnik zerowy, jeśli podciąg sub nie występuje w s.
cout << strstr("Daniel Defoe","De") << endl;wypisze 'Defoe'.
char* strtok(char* str, const char* set) — funkcja ta jest „tokenizerem”, czyli funkcją pozwalającą rozłożyć napis na leksemy („słowa”) oddzielone znakami ze zbioru znaków w napisie set, które wobec tego traktowane są jako separatory (np. odstęp, przecinek, dwukropek, itd.). Działanie funkcji oparte jest na istnieniu wewnętrznego wskaźnika typu char*. Kolejne wywołania strtok zwracają kolejne leksemy. Przy pierwszym wywołaniu str jest napisem, który chcemy rozłożyć; w kolejnych wywołaniach pierwszym argumentem powinien być wskaźnik zerowy nullptr, co informuje funkcję, że należy kontynuować przetwarzanie od końca ostatnio zwróconego leksemu. Pomiędzy wywołaniami nie wolno zmieniać napisu str — sama funkcja go jednak zmienia, co oznacza, że nie można przekazać napisu niemodyfikowalnego, np. zadanego jako literał napisowy. Wolno natomiast przy kolejnych wywołaniach zmieniać zbiór separatorów w set. Funkcja działa następująco:
Jeśli str nie jest nullptr (czyli kiedy rozpoczynamy analizę pewnego napisu), ignorowane są wszystkie wiodące znaki z str, które należą do zbioru separatorów set. Jeśli wszystkie znaki w str należą do set, czyli w napisie są same separatory, to funkcja zwraca nullptr i wskaźnik wewnętrzny ustawiany jest też na nullptr, co kończy przetwarzanie napisu. Jeśli natomiast tak nie było, to wewnętrzny wskaźnik ustawiany jest na pierwszy napotkany znak nie będący separatorem i dalej postępowanie jest takie jak w przypadku, gdy str jest nullptr.
Jeśli str jest nullptr i wskaźnik wewnętrzny też jest nullptr, to zwracany jest nullptr i stan wewnętrznego wskaźnika jest niezmieniany. Kończy to analizę jednego napisu.
Jeśli str jest nullptr, ale wskaźnik wewnętrzny nie jest nullptr, to funkcja szuka, poczynając od miejsca wskazywanego przez wewnętrzny wskaźnik, pierwszego wystąpienia separatora. Jeśli zostanie znaleziony, to jest nadpisywany znakiem pustym ' \0', funkcja zwraca adres zawarty we wskaźniku wewnętrznym, a sam wewnętrzny wskaźnik jest przesuwany na pierwszy znak za wpisanym znakiem ' \0'. Jeśli natomiast nie zostanie znaleziony, to funkcja zwraca adres zawarty we wskaźniku wewnętrznym, a sam wewnętrzny wskaźnik jest ustawiany na nullptr.
Brzmi to bardzo skomplikowanie, ale użycie nie jest takie
trudne. Na przykład poniższy program
1. #include <iostream> 2. #include <cstring> 3. using namespace std; 4. 5. int main() { 6. char strin[] = "int* fun(char& c,double** wtab);"; 7. char separ[] = ")(,;"; 8. char* token; 9. 10. token = strtok(strin,separ); 11. while (token != 0) { 12. cout << token << endl; 13. token = strtok(0,separ); 14. } 15. }
drukuje w kolejnych liniach
int* fun
char& c
double** wtab
Funkcje operujące na napisach są dobrym ćwiczeniem programistycznym.
Na przykład niektóre ze standardowych funkcji udostępnianych przez
nagłówek
cstring
mogą być zaimplementowane w pokazany
poniżej sposób (i na wiele innych sposobów):
1. char* Strcpy(char* target, const char* source) { 2. char* t = target; 3. while ( *t++ = *source++ ); 4. return target; 5. } 6. 7. char* Strcat(char* target, const char* source) { 8. char* t = target-1; 9. while ( *++t ); 10. while ( *t++ = *source++ ); 11. return target; 12. } 13. 14. char* Strncat(char* target, const char* source, int n) { 15. char* t = target-1; 16. while ( *++t ); 17. while ( (*t++ = *source++) && n--); 18. *(t-1) = '\0'; 19. return target; 20. } 21. 22. int Strlen(const char* source) { 23. const char* s = source; 24. while ( *s++ ); 25. return s-source-1; 26. } 27. 28. char* Strchr(const char* target, int c) { 29. while ( *target && *target++ != c ); 30. return (char*)(*--target ? target : 0); 31. }
Po dołączeniu pliku nagłówkowego cctype mamy do dyspozycji szereg funkcji operujących na pojedynczych znakach.
Duża grupa funkcji, wszystkie o nazwach rozpoczynających się od is, to funkcje sprawdzające, czy znak przesłany jako argument spełnia określone kryteria. Argument jest typu int, ale brany pod uwagę jest tylko jego najmłodszy bajt. Wartością zwracaną jest zawsze wartość typu int: niezerowa oznacza true, zerowa false. Tak więc wszystkie te funkcje mają nagłówek typu
int isJakasWlasnosc(int c);Nie ma gwarancji (i zwykle tak nie jest), że true odpowiada wartości 1; może to być dowolna wartość niezerowa.
Wymieńmy funkcje z tej grupy:
Prócz tego biblioteka dołączana przez nagłówek cctype zawiera dwie przydatne standardowe funkcje, również typu int → int:
Na przykład funkcja
uplow
w programie
1. #include <iostream> 2. #include <cctype> 3. using namespace std; 4. 5. int uplow(char* s) { 6. int cnt = 0; 7. do { 8. if (isalpha(*s)) 9. if ( cnt == 0 || !isalpha(*(s-1))) { 10. *s = (char)toupper(*s); 11. cnt++; 12. } else 13. *s = (char)tolower(*s); 14. } while (*s++); 15. return cnt; 16. } 17. 18. int main() { 19. char napis[] = "to jEST DlUgI,dluGI nAPIs!"; 20. 21. int ile = uplow(napis); 22. cout << ile << " slow, napis = \'" << napis << "\'\n"; 23. }
Plik nagłówkowy cstdlib dostarcza kilku przydatnych funkcji konwertujących. Stosowanie ich wymaga starannego sprawdzania możliwych błędów. Służy do tego globalna zmienna errno (może to być makro preprocesora, ze względu na programy wielowątkowe, ale z punktu widzenia programisty zachowuje się jak zmienna globalna). Aby móc z niej korzystać, należy dołączyć plik nagłówkowy cerrno.
double strtod(const char* str, char** ptr) — (string to double) zwraca liczbę typu double zapisaną w początkowej części napisu str. Wiodące białe znaki są ignorowane. Wczytywanie znaków kończy się po napotkaniu pierwszego „złego” znaku, to znaczy znaku, który nie może być traktowany jako kontynuacja zapisu wczytywanej liczby. Jest rozpoznawany zapis liczb w postaci liczb całkowitych dziesiętnych (jak 127), liczb w zapisie z kropką dziesiętną (jak 123,34) i liczb w formacie naukowym (z literą 'e', dużą lub małą, przed wykładnikiem potęgi dziesięciu hyfn 1.2E-11).
Wskaźnik str wskazuje napis zawierający na początku liczbę przeznaczoną do „wyczytania”. Za tą liczbą mogą występować w napisie inne znaki: pozostaną one w strumieniu wejściowym i będą mogły być wczytane przez następną operację czytania. Drugim argumentem funkcji powinien być adres wskaźnika typu char* — dlatego typem parametru jest char**. Może to być adres pusty (czyli nullptr albo po prostu zero), ale nie jest to wskazane: lepiej przesłać do funkcji adres istniejącej zmiennej wskaźnikowej typu char*.
Po prawidłowej konwersji zwracana jest „wyczytana” liczba w postaci wartości typu double. Jeśli ptr nie był pusty, do wskaźnika wskazywanego przez ptr wpisywany jest adres pierwszego znaku w napisie str za wczytaną reprezentacją liczby (w szczególności może to być adres kończącego napis str znaku ' \0'. Jeśli posłaliśmy do funkcji nullptr jako drugi argument, to informacji tej nie dostaniemy. Wartość errno jest zero.
Jeśli napis nie zawiera legalnego zapisu liczby, to zwracane jest zero, a do wskaźnika wskazywanego przez ptr, jeśli nie był to wskaźnik pusty, wpisywany jest adres początku napisu str, czyli wartość samego str. Tę równość wskaźników można zatem sprawdzić w programie, gdy zwróconą wartością jest zero — jeśli wartości tych wskaźników są takie same, to znaczy, że zwrócone zero nie jest „prawdziwe”.
Jeśli napis zawiera legalny zapis liczby, ale liczba ta przekracza dopuszczalny zakres (nadmiar, ang. overflow), zwracana jest wartość ± HUGE_VAL z właściwym znakiem (ta stała oznacza inf — nieskończoność). Zmienna errno jest wtedy ustawiana na ERANGE. Do wskaźnika wskazywanego przez ptr wpisywany jest adres pierwszego znaku za reprezentacją liczby, tak jak w przypadku udanej konwersji.
Jeśli napis zawiera legalny zapis liczby, ale liczba ta ma za małą wartość bezwzględną, aby była odróżnialna od zera (niedomiar, ang. underflow), zwracane jest zero. Zmienna errno jest ustawiana na ERANGE, a do wskaźnika wskazywanego przez ptr wpisywany jest adres pierwszego znaku za reprezentacją liczby, tak jak w przypadku udanej konwersji.
Następujący programik ilustruje te definicje:
1. #include <iostream> 2. #include <iomanip> // setw 3. #include <cstdlib> // strtod 4. #include <cerrno> // errno 5. using namespace std; 6. 7. int main() { 8. char* ptr; 9. double x; 10. char* str; 11. 12. cout << "ERANGE = " << ERANGE << endl; 13. 14. // = 1 = OK 15. str = "-1.2e+2xxx"; 16. x = strtod(str,&ptr); 17. cout << "=1= str = " << str << "; x = " 18. << setw(4) << x << "; errno = " << setw(2) 19. << errno << "; ptr = " << ptr << endl; 20. 21. // = 2 = Not a Number 22. str = "abcdefghij"; 23. x = strtod(str,&ptr); 24. cout << "=2= str = " << str << "; x = " 25. << setw(4) << x << "; errno = " << setw(2) 26. << errno << "; ptr = " << ptr << endl; 27. 28. // = 3 = Overflow 29. str = "-9e+9999xx"; 30. x = strtod(str,&ptr); 31. cout << "=3= str = " << str << "; x = " 32. << setw(4) << x << "; errno = " << setw(2) 33. << errno << "; ptr = " << ptr << endl; 34. 35. // = 4 = Underflow 36. str = "-9e-9999xx"; 37. x = strtod(str,&ptr); 38. cout << "=4= str = " << str << "; x = " 39. << setw(4) << x << "; errno = " << setw(2) 40. << errno << "; ptr = " << ptr << endl; 41. }
ERANGE = 34 =1= str = -1.2e+2xxx; x = -120; errno = 0; ptr = xxx =2= str = abcdefghij; x = 0; errno = 0; ptr = abcdefghij =3= str = -9e+9999xx; x = -inf; errno = 34; ptr = xx =4= str = -9e-9999xx; x = 0; errno = 34; ptr = xx
Na podobnej zasadzie działają funkcje:
long strtol(const char* str, char** ptr) — (string to long) zwraca liczbę typu long zapisaną w początkowej części napisu str. Działa tak jak opisana wcześniej funkcja strtod, tyle że w przypadku nadmiaru zwraca nie ± HUGE_VAL, ale stałe LONG_MAX lub LONG_MIN. Niedomiar oczywiście wystąpić nie może.
long strtol(const char* str, char** ptr, int base) — jak funkcja poprzednia, ale zwraca liczbę typu long odczytując ją z napisu str w układzie o podstawie base. Podstawa może być liczbą od 2 do 36. Dla podstaw większych od dziesięciu jako cyfry interpretowane są kolejne litery (duże lub małe) poczynając od litery 'a', która oznacza cyfrę 10. Jeśli podstawa wynosi 16, reprezentacja napisowa liczby może się zaczynać od '0x' (lub '0X') — te znaki są wtedy ignorowane. Na przykład strtol("J23",&ptr,20) zwraca 7643 = 19⋅400 + 2⋅20 + 3.
unsigned long strtoul(const char* str, char** ptr) — (string to unsigned long) zwraca liczbę typu unsigned long zapisaną w początkowej części napisu str. Działa tak jak dwuargumentowa funkcja strtol, tyle że w przypadku nadmiaru zwraca ULONG_MAX.
unsigned long strtoul(const char* str, char** ptr, int base) — jak trzyargumentowa funkcja strtol, ale zwraca wartość typu unsigned long.
Prócz wymienionych istnieją, ze względu na wsteczną zgodność, tradycyjne choć niezalecane funkcje konwertujące:
double atof(const char* str) — (ascii to floating) działa podobnie jak funkcja strtod, ale nie ma drugiego argumentu. W przypadku błędu zwraca zero, lub jeśli nastąpił nadmiar, ± HUGE_VAL. Ustawia, niezależnie od przyczyny niepowodzenia, wartość errno na ERANGE.
int atoi(const char* str) — (ascii to integer) działa podobnie jak funkcja strtol, ale zwraca wartość typu int (a nie long) i nie ma drugiego argumentu. Obsługa błędów podobnie jak dla funkcji atof.
long atol(const char* str) — (ascii to long) działa podobnie jak funkcja strtol, ale nie ma drugiego argumentu. Obsługa błędów podobnie jak dla funkcji atof.
T.R. Werner, 21 lutego 2016; 20:17