Większość języków daje typ string, który zna własną długość. C tego nie robi. W C napis to po prostu tablica char z jedną dodatkową zasadą: po tekście następuje bajt o wartości zero, zapisywany jako '\0' i nazywany znakiem końca napisu (null terminator). Wszystko inne o napisach w C wynika z tej jednej konwencji.
Napis to tablica char plus '\0'
Napis deklarujesz, inicjalizując tablicę char literałem:
Tablica ma cztery bajty, a nie trzy:
name: +-----+-----+-----+------+
| 'A' | 'd' | 'a' | '\0' |
+-----+-----+-----+------+
indeks: 0 1 2 3
sizeof(name) wynosi 4. Kompilator policzył znaki, dodał jeden na terminator i sam ustalił rozmiar tablicy. Przez ten dodatkowy bajt char name[3] = "Ada"; to błąd, który czeka na swoją chwilę: tekst się mieści, ale terminator już nie, więc nic dalej nie będzie wiedziało, gdzie kończy się napis.
Gdy chcesz mieć miejsce na późniejszą zmianę zawartości, podaj rozmiar sam i zostaw zapas:
char name[32] = "Ada"; /* 3 znaki, terminator i 28 bajtow zapasu */
Nieużywane bajty inicjalizator wypełnia zerami, co jest nieszkodliwe.
Dlaczego terminator ma znaczenie
Żadna funkcja w C nie dostaje długości. printf("%s", s) otrzymuje jeden adres i idzie do przodu, wypisując bajty, aż trafi na '\0'. Tablica char bez terminatora nie jest więc napisem, a traktowanie jej jak napisu czyta pamięć, która akurat leży za nią.
Zasada do zapamiętania: sizeof podaje rozmiar pamięci, a strlen długość tekstu. Dla char buf[32] = "Ada" sizeof buf wynosi 32, a strlen(buf) wynosi 3. Odpowiadają na różne pytania, a ich mylenie to jeden z najczęstszych błędów w C.
Literały napisowe a tablice char
Te dwie linie wyglądają podobnie, a zachowują się zupełnie inaczej:
char a[] = "hello"; /* twoja wlasna tablica, zainicjalizowana literalem */
char *p = "hello"; /* wskaznik wycelowany w sam literal */
a to sześciobajtowa tablica z prywatną kopią tekstu. Możesz ją modyfikować. p wskazuje na dane napisowe programu przeznaczone tylko do odczytu: literał jest współdzielony, a zapis przez p to niezdefiniowane zachowanie, które zwykle wysypuje program w czasie działania, a nie w czasie kompilacji.
Pisz const char * zawsze, gdy wskaźnik celuje w literał. Kompilator odrzuci wtedy zapis w czasie kompilacji, zamiast pozwolić mu zamienić się w awarię. Wybór między tymi dwiema formami to tak naprawdę pytanie o wskaźniki i tablice, omówione na stronie o wskaźnikach i tablicach.
Jeszcze jedna różnica warta uwagi: sizeof a wynosi 6, ale sizeof p to rozmiar wskaźnika (8 na większości systemów), niezależnie od długości tekstu.
Wypisywanie i wczytywanie napisów
%s wypisuje napis, a %c pojedynczy znak.
Przy wczytywaniu całkowicie unikaj gets: usunięto ją z języka, bo nie da się jej używać bezpiecznie. Używaj fgets, które przyjmuje rozmiar bufora i go nie przepełni:
fgets zostawia w napisie znak nowej linii z naciśniętego Entera, a tego prawie nigdy nie chcesz. Linia ze strcspn znajduje pierwsze '\n' i nadpisuje je terminatorem: to zwięzły, standardowy sposób na jego usunięcie.
Przechodzenie po napisie znak po znaku
Koniec jest oznaczony, a nie policzony, więc idiomatyczna pętla sprawdza sam znak:
Warunek text[i] != '\0' często skraca się do samego text[i], bo '\0' to zero, czyli fałsz. Obie formy są poprawne, ale jawna jest czytelniejsza, gdy dopiero się uczysz.
Po napisie można też iść wskaźnikiem zamiast indeksem, co jest równie idiomatycznym C:
Rzutowanie na unsigned char przed wywołaniem toupper to nie ozdobnik: funkcje z <ctype.h> są niezdefiniowane dla wartości ujemnych, a zwykły char może być ujemny dla bajtów powyżej 127.
Napisu nie skopiujesz przez =
To ściana, na którą trafia każdy początkujący:
char a[10];
a = "hello"; /* error: assignment to expression with array type */
char b[10] = "hi";
char c[10];
c = b; /* blad z tego samego powodu */
Nazwa tablicy nie jest modyfikowalną wartością, więc = nie ma tu nic do zrobienia. Skrót działa tylko przy inicjalizacji w deklaracji, a to inna operacja, wykonywana przez kompilator.
Aby skopiować napis w czasie działania, skopiuj bajty przez strcpy z <string.h>:
To samo rozumowanie wyjaśnia, dlaczego == nie porównuje napisów. if (a == b) porównuje dwa adresy, więc dla dwóch różnych tablic z identycznym tekstem daje fałsz. Zamiast tego użyj strcmp(a, b) == 0. Obie funkcje i ich bezpieczniejszych krewnych omawia strona o funkcjach napisowych.
Tablice napisów
Lista napisów to albo tablica wskaźników (dla stałego tekstu, który tylko czytasz), albo dwuwymiarowa tablica char (gdy każdy element ma być modyfikowalny):
Tablica wskaźników przechowuje trzy adresy literałów tylko do odczytu: zwięźle, ale bez możliwości zmiany. Forma char[3][16] rezerwuje 48 bajtów twojej własnej pamięci, więc każdy wiersz można nadpisać.
Częste błędy
- Zapominanie o bajcie terminatora.
char s[5] = "hello"nie ma miejsca na'\0'. Rezerwujstrlen + 1. - Używanie
sizeofzamiaststrlen.sizeofna wskaźniku podaje rozmiar wskaźnika, a nie długość tekstu. - Zapis przez
char *do literału. Deklaruj takie wskaźniki jakoconst char *. - Porównywanie przez
==. To porównuje adresy. Użyjstrcmp. - Wczytywanie przez
scanf("%s", buf). Nie ma limitu rozmiaru i chętnie przepełnia bufor, lepiej użyćfgets.
Najczęściej zadawane pytania
Jak zadeklarować string w C?
Jako tablicę char: char name[] = "Ada";. C nie ma wbudowanego typu string: ta tablica przechowuje 'A', 'd', 'a' i czwarty, ukryty bajt '\0', który oznacza koniec. Jeśli chcesz mieć miejsce na późniejszą zmianę tekstu, podaj rozmiar tablicy jawnie: char name[32] = "Ada";.
Czym jest znak końca napisu w C?
To bajt '\0' (o wartości zero), który oznacza, gdzie kończy się napis. Każda funkcja standardowa, czyli printf("%s"), strlen, strcpy, czyta dalej, dopóki nie trafi na ten bajt. Tablica char bez niego nie jest napisem, a przekazanie jej do tych funkcji czyta pamięć za jej końcem.
Dlaczego w C nie można przypisać napisu przez =?
Bo tablica nie jest wartością, którą da się skopiować przez =. char a[10]; a = "hi"; się nie skompiluje. Zamiast tego skopiuj znaki przez strcpy(a, "hi") z <string.h> albo zainicjalizuj tablicę przy deklaracji, char a[10] = "hi";, bo to jedyne miejsce, gdzie ten skrót działa.
Czym różni się char * od char [] w C?
char s[] = "hi" tworzy modyfikowalną tablicę z prywatną kopią tekstu. char *p = "hi" tworzy wskaźnik na literał napisowy, który leży w pamięci tylko do odczytu: czytanie jest w porządku, a zapis przez ten wskaźnik wysypuje program. Używaj tablicy, gdy zamierzasz modyfikować tekst, a wskaźnika, gdy tylko go czytasz.