W dzisiejszych czasach komputer przestał być narzędziem tylko i wyłącznie do przetwarzania danych. Od programów komputerowych zaczęto wymagać czegoś nowego - program w wyniku swojego działania nie ma zwracać danych, rozumianych tylko przez autora programu, lecz powinien być na tyle komunikatywny, aby przeciętny użytkownik komputera mógł bez problemu tenże komputer obsłużyć. Do przechowywania tychże komunikatów służą tzw. "łańcuchy" (ang. string) czyli ciągi znaków.

Język C nie jest wygodnym narzędziem do manipulacji napisami. Jak się wkrótce przekonamy, zestaw funkcji umożliwiających operacje na napisach w bibliotece standardowej C jest raczej skromny. Dodatkowo, problemem jest sposób, w jaki łańcuchy przechowywane są w pamięci. Zobaczymy także, jak stworzyć łańcuch typu lista połączona

Łańcuchy znaków w języku C Napis jest zapisywany w kodzie programu jako ciąg znaków zawarty pomiędzy dwoma cudzysłowami.

printf ("Napis w języku C");

W pamięci taki łańcuch jest następującym po sobie ciągiem znaków (char), który kończy się znakiem "null" zapisywanym jako '\0'.

Jeśli mamy napis, do poszczególnych znaków odwołujemy się jak w tablicy:

const char *tekst = "Jakiś tam tekst"; printf("%c\n", "przykład"); /* wypisze p - znaki w napisach są numerowane od zera */ printf("%c\n", tekst); /* wypisze k */

Ponieważ napis w pamięci kończy się zerem umieszczonym tuż za jego zawartością, odwołanie się do znaku o indeksie równym długości napisu zwróci zero:

printf("%d", "test"); /* wypisze 0 */

Napisy możemy wczytywać z klawiatury i wypisywać na ekran przy pomocy dobrze znanych funkcji scanf, printf i pokrewnych. Formatem używanym dla napisów jest %s.

printf("%s", tekst);

Większość funkcji działających na napisach znajduje się w pliku nagłówkowym string.h.

Jeśli łańcuch jest zbyt długi, można zapisać go w kilku linijkach, ale wtedy przechodząc do następnej linii musimy na końcu postawić znak "\".

printf("Ten napis zajmuje \ więcej niż jedną linię");

Instrukcja taka wydrukuje: Ten napis zajmuje więcej niż jedną linię

Możemy zauważyć, że napis, który w programie zajął więcej niż jedną linię, na ekranie zajął tylko jedną. Jest tak, ponieważ "\" informuje kompilator, że łańcuch będzie kontynuowany w następnej linii kodu - nie ma wpływu na prezentację łańcucha. Aby wydrukować napis w kilku liniach należy wstawić do niego \n ("n" pochodzi tu od "new line", czyli "nowa linia").

printf("Ten napis\nna ekranie\nzajmie więcej niż jedną linię.");

W wyniku otrzymamy:

Ten napis na ekranie zajmie więcej niż jedną linię.

Jak komputer przechowuje w pamięci łańcuch?

Zmienna, która przechowuje łańcuch znaków, jest tak naprawdę wskaźnikiem do ciągu znaków (bajtów) w pamięci. Możemy też myśleć o napisie jako o tablicy znaków (jak wyjaśnialiśmy wcześniej, tablice to też wskaźniki).

Możemy wygodnie zadeklarować napis:

const char *tekst = "Jakiś tam tekst"; /* Umieszcza napis w obszarze danych programu i przypisuje adres */ char tekst[] = "Jakiś tam tekst"; /* Umieszcza napis w tablicy */ char tekst[] = {'J','a','k','i','s',' ','t','a','m',' ','t','e','k','s','t','\0'}; /* Tekst to taka tablica jak każda inna */

Kompilator automatycznie przydziela wtedy odpowiednią ilość pamięci (tyle bajtów, ile jest liter plus jeden dla kończącego nulla). Jeśli natomiast wiemy, że dany łańcuch powinien przechowywać określoną ilość znaków (nawet, jeśli w deklaracji tego łańcucha podajemy mniej znaków) deklarujemy go w taki sam sposób, jak tablicę jednowymiarową:

char tekst = "Ten tekst musi być krótszy niż 80 znaków";

Należy cały czas pamiętać, że napis jest tak naprawdę tablicą. Jeśli zarezerwowaliśmy dla napisu 80 znaków, to przypisanie do niego dłuższego napisu spowoduje pisanie po pamięci.

Pisanie po pamięci może czasami skończyć się błędem dostępu do pamięci ("segmentation fault" w systemach UNIX) i zamknięciem programu, jednak może zdarzyć się jeszcze gorsza ewentualność - możemy zmienić w ten sposób przypadkowo wartość innych zmiennych. Program zacznie wtedy zachowywać się nieprzewidywalnie - zmienne a nawet stałe, co do których zakładaliśmy, że ich wartość będzie ściśle ustalona, mogą przyjąć taką wartość, jaka absolutnie nie powinna mieć miejsca.

Kluczowy jest też kończący napis znak null. W zasadzie wszystkie funkcje operujące na napisach opierają właśnie na nim. Na przykład, strlen szuka rozmiaru napisu idąc od początku i zliczając znaki, aż nie natrafi na znak o kodzie zero. Jeśli nasz napis nie kończy się znakiem null, funkcja będzie szła dalej po pamięci. Na szczęście, wszystkie operacje podstawienia typu tekst = "Tekst" powodują zakończenie napisu nullem (o ile jest na niego miejsce) .

Znaki specjalne Jak zapewne zauważyłeś w poprzednim przykładzie, w łańcuchu ostatnim znakiem jest znak o wartości zero ('\0')wikipedia: Null_(znak). Jednak łańcuchy mogą zawierać inne znaki specjalne(sekwencje sterujące)wikipedia: Kod sterujący, np.: '\a' - alarm (sygnał akustyczny terminala) '\b' - backspace (usuwa poprzedzający znak) '\f' - wysuniecie strony (np. w drukarce) '\r' - powrót kursora (karetki) do początku wiersza '\n' - znak nowego wiersza '\"' - cudzysłów '\'' - apostrof '\\' - ukośnik wsteczny (backslash) '\t' - tabulacja pozioma '\v' - tabulacja pionowa '\?' - znak zapytania (pytajnik) '\ooo' - liczba zapisana w systemie oktalnym (ósemkowym), gdzie 'ooo' należy zastąpić trzycyfrową liczbą w tym systemie '\xhh' - liczba zapisana w systemie heksadecymalnym (szesnastkowym), gdzie 'hh' należy zastąpić dwucyfrową liczbą w tym systemie '\unnnn' - uniwersalna nazwa znaku, gdzie 'nnnn' należy zastąpić czterocyfrowym identyfikatorem znaku w systemie szesnatkowym. 'nnnn' odpowiada dłuższej formie w postaci '0000nnnn' '\unnnnnnnn' - uniwersalna nazwa znaku, gdzie 'nnnnnnnn' należy zastąpić ośmiocyfrowym identyfikatorem znaku w systemie szesnatkowym.

Warto zaznaczyć, że znak nowej linii ('\n') jest w różny sposób przechowywany w różnych systemach operacyjnych. Wiąże się to z pewnymi historycznymi uwarunkowaniami. W niektórych systemach używa się do tego jednego znaku o kodzie 0x0A (Line Feed - nowa linia). Do tej rodziny zaliczamy systemy z rodziny Unix: Linux, *BSD, Mac OS X inne. Drugą konwencją jest zapisywanie '\n' za pomocą dwóch znaków: LF (Line Feed) + CR (Carriage return - powrót karetki). Znak CR reprezentowany jest przez wartość 0x0D. Kombinacji tych dwóch znaków używają m.in.: CP/M, DOS, OS/2, Microsoft Windows. Trzecia grupa systemów używa do tego celu samego znaku CR. Są to systemy działające na komputerach Commodore, Apple II oraz Mac OS do wersji 9. W związku z tym plik utworzony w systemie Linux może prezentować się inaczej pod systemem Windows.

Operacje na łańcuchach

nagłówki Większość funkcji operujących na ciągach C jest zadeklarowana w nagłówku string.h (cstring w C++), natomiast funkcje operujące na ciągach C są deklarowane w nagłówku wchar.h

deklaracja

// http://www.crasseux.com/books/ctutorial/Initializing-strings.html

#include #include

int main() {

/* Example 1 */ char string1[] = "A string declared as an array.\n";

/* Example 2 */ const char *string2 = "A string declared as a pointer.\n";

/* Example 3 */ char string3; strcpy(string3, "A string constant copied in.\n");

printf ("%s\n", string1); printf ("%s\n",string2); printf ("%s\n",string3);

return 0; }

Porównywanie łańcuchów Napisy to tak naprawdę wskaźniki. Tak więc używając zwykłego operatora porównania ==, otrzymamy wynik porównania adresów a nie tekstów.

Do porównywania dwóch ciągów znaków należy użyć funkcji strcmp zadeklarowanej w pliku nagłówkowym string.h. Jako argument przyjmuje ona dwa napisy i zwraca wartość ujemną jeżeli napis pierwszy jest mniejszy od drugiego, 0 jeżeli napisy są równe lub wartość dodatnią jeżeli napis pierwszy jest większy od drugiego. Ciągi znaków porównywalne są leksykalnie kody znaków, czyli np. (przyjmując kodowanie ASCII) "a" jest mniejsze od "b", ale jest większe od "B". Np.:

#include #include

int main(void) { char str1 = {'\0'}, str2 = {'\0'}; int cmp;

puts("Podaj dwa ciagi znakow: "); fgets(str1, sizeof str1, stdin); fgets(str2, sizeof str2, stdin);

cmp = strcmp(str1, str2); if (cmp0) { puts("Pierwszy napis jest wiekszy."); } else { puts("Napisy sa takie same.");

return 0;

Czasami możemy chcieć porównać tylko fragment napisu, np. sprawdzić czy zaczyna się od jakiegoś ciągu. W takich sytuacjach pomocna jest funkcja strncmp. W porównaniu do strcmp() przyjmuje ona jeszcze jeden argument oznaczający maksymalną liczbę znaków do porównania:

#include #include

int main(void) { char str; int cmp;

fputs("Podaj ciag znakow: ", stdout); fgets(str, sizeof str, stdin);

if (!strncmp(str, "foo", 3)) { puts("Podany ciag zaczyna sie od 'foo'.");

return 0;

Kopiowanie napisów

Do kopiowania ciągów znaków służy funkcja strcpy, która kopiuje drugi napis w miejsce pierwszego. Musimy pamiętać, by w pierwszym łańcuchu było wystarczająco dużo miejsca.

char napis; strcpy(napis, "Ala ma kota.");

Znacznie bezpieczniej jest używać funkcji strncpy, która kopiuje co najwyżej tyle bajtów ile podano jako trzeci parametr. Uwaga! Jeżeli drugi napis jest za długi funkcja nie kopiuje znaku null na koniec pierwszego napisu, dlatego zawsze trzeba to robić ręcznie:

char napis = { 0 }; strncpy(napis, "Ala ma kota.", sizeof(napis) - 1);

Łączenie napisów Do łączenia napisów służy funkcja strcat, która kopiuje drugi napis do pierwszego. Ponownie jak w przypadku strcpy musimy zagwarantować, by w pierwszym łańcuchu było wystarczająco dużo miejsca.

#include #include

int main(void) { char napis1 = "hello "; const const char *napis2 = "world"; strcat(napis1, napis2); puts(napis1); return 0;

I ponownie jak w przypadku strcpy istnieje funkcja strncat, która skopiuje co najwyżej tyle bajtów ile podano jako trzeci argument i dodatkowo dopisze znak null. #include #include

int main(void) { char napis1 = "hello "; const char *napis2 = "world"; strncat(napis1, napis2, 2); puts(napis1); return 0;

hello wo

Możemy też wykorzystać trzeci argument do zapewnienia bezpiecznego wywołania funkcji kopiującej. W przypadku zbyt małej tablicy skopiowany zostanie fragment tylko takie długości, na jaki starczy miejsca (uwzględniając, że na końcu trzeba dodać znak '\0'). Przy podawaniu ilości znaków należy także pamiętać, że łańcuch, do którego kopiujemy nie musi być pusty, a więc część pamięci przeznaczona na niego jest już zajęta, jak w poniższym przykładzie. Dlatego od rozmiaru całego łańcucha do którego kopiujemy należy odjąć długość napisu, który już się w nim znajduje. char napis1 = "hello "; const char *napis2 = "world"; strncat(napis1, napis2, sizeof(napis1)-strlen(napis1)- 1); puts(napis1);

hello wor

Bezpieczeństwo kodu a łańcuchy Przepełnienie bufora O co właściwie chodzi z tymi funkcjami strncpy i strncat? Otóż, niewinnie wyglądające łańcuchy mogą okazać się zabójcze dla bezpieczeństwa programu, a przez to nawet dla systemu, w którym ten program działa. Może brzmi to strasznie, lecz jest to prawda. Może pojawić się tutaj pytanie: "w jaki sposób łańcuch może zaszkodzić programowi?". Otóż może i to całkiem łatwo. Przeanalizujmy następujący kod:

#include #include #include int main(int argc , const char **argv) { char haslo_poprawne = 0; char haslo;

if (argc!=2) { fprintf(stderr, "uzycie: %s haslo", argv); return EXIT_FAILURE;

strcpy(haslo, argv); /* tutaj następuje przepełnienie bufora */ if (!strcmp(haslo, "poprawne")) { haslo_poprawne = 1;

if (!haslo_poprawne) { fputs("Podales bledne haslo.\n", stderr); return EXIT_FAILURE;

puts("Witaj, wprowadziles poprawne haslo."); return EXIT_SUCCESS;

Jest to bardzo prosty program, który wykonuje jakąś akcję, jeżeli podane jako pierwszy argument hasło jest poprawne. Sprawdźmy czy działa:

$ ./a.out niepoprawne Podales bledne haslo. $ ./a.out poprawne Witaj, wprowadziles poprawne haslo.