C/Napisy

Z Wikibooks, biblioteki wolnych podręczników.
< C
Skocz do: nawigacji, wyszukiwania

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

Uwaga! Uwaga!
Napisy w języku C mogą być przyczyną wielu trudnych do wykrycia błędów w programach. Warto dobrze zrozumieć, jak należy operować na łańcuchach znaków i zachować szczególną ostrożność w tych miejscach, gdzie napisów używamy.

Łańcuchy znaków w języku C[edytuj]

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" (czyli po prostu liczbą zero), 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"[0]); /* wypisze p - znaki w napisach są numerowane od zera */
 printf("%c\n", tekst[2]);      /* 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"[4]);       /* 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?[edytuj]

Napis "Merkkijono" przechowywany w pamięci

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[80] = "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.

Uwaga! Uwaga!
Deklaracja const char *tekst = "cokolwiek"; oraz char tekst[] = "cokolwiek"; pomimo, że wyglądają bardzo podobnie bardzo się od siebie różnią. W przypadku pierwszej deklaracji próba zmodyfikowania napisu (np. tekst[0] = 'C';) może wyświetlać błąd kompilacji. Dzieje się tak dlatego, że const char *tekst = "cokolwiek"; deklaruje wskaźnik na stały obszar pamięci[1].

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) [2].

Znaki specjalne[edytuj]

Jak zapewne zauważyłeś w poprzednim przykładzie, w łańcuchu ostatnim znakiem jest znak o wartości zero ('\0'). Jednak łańcuchy mogą zawierać inne znaki specjalne(sekwencje sterujące), 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 wyglądać dziwnie pod systemem Windows.

Operacje na łańcuchach[edytuj]

Porównywanie łańcuchów[edytuj]

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 <stdio.h>
 #include <string.h>
 
 int main(void) {
   char str1[100] = {'\0'}, str2[100] = {'\0'};
   int cmp;
 
   puts("Podaj dwa ciagi znakow: ");
   fgets(str1, strlen(str1)+1, stdin);
   fgets(str2, strlen(str2)+1, stdin);
 
   cmp = strcmp(str1, str2);
   if (cmp<0) {
     puts("Pierwszy napis jest mniejszy.");
   } else if (cmp>0) {
     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 <stdio.h>
 #include <string.h>
 
 int main(void) {
   char str[100];
   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[edytuj]

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[100];
 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[100] = { 0 };
 strncpy(napis, "Ala ma kota.", sizeof(napis) - 1);

Łączenie napisów[edytuj]

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 <stdio.h>
 #include <string.h>
 
 int main(void) {
   char napis1[80] = "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 <stdio.h>
 #include <string.h>
 
 int main(void) {
   char napis1[80] = "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[10] = "hello ";
   const char *napis2 = "world";
   strncat(napis1, napis2, sizeof(napis1)-strlen(napis1)- 1);
   puts(napis1);
hello wor


Uwaga! Uwaga!
Osoby, które programowały w językach skryptowych muszą bardzo uważać na łączenie i kopiowanie napisów. Kompilator języka C nie wykryje nadpisania pamięci za zmienną łańcuchową i nie przydzieli dodatkowego obszaru pamięci. Może się zdarzyć, że program pomimo nadpisywania pamięci za łańcuchem będzie nadal działał, co bardzo utrudni wykrywanie tego typu błędów!

Bezpieczeństwo kodu a łańcuchy[edytuj]

Przepełnienie bufora[edytuj]

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 <stdio.h>
 #include <string.h>
 #include <stdlib.h>
 int main(int argc , const char **argv) {
   char haslo_poprawne = 0;
   char haslo[16];
 
   if (argc!=2) {
     fprintf(stderr, "uzycie: %s haslo", argv[0]);
     return EXIT_FAILURE;
   }
 
   strcpy(haslo, argv[1]); /* 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.

Jednak okazuje się, że z powodu użycia funkcji strcpy włamywacz nie musi znać hasła, aby program uznał, że zna hasło, np.:

$ ./a.out 11111111111111111111111111111111
Witaj, wprowadziles poprawne haslo.

Co się stało? Podaliśmy ciąg jedynek dłuższy niż miejsce przewidziane na hasło. Funkcja strcpy() kopiując znaki z argv[1] do tablicy (bufora) haslo przekroczyła przewidziane dla niego miejsce i szła dalej - gdzie znajdowała się zmienna haslo_poprawne. strcpy() kopiowała znaki już tam, gdzie znajdowały się inne dane — między innymi wpisała jedynkę do haslo_poprawne.


Podany przykład może się różnie zachowywać w zależności od kompilatora, jakim został skompilowany, i systemu, na jakim działa, ale ogólnie mamy do czynienia z poważnym niebezpieczeństwem.

Uwaga! Uwaga!
Taką sytuację nazywamy przepełnieniem bufora. Może umożliwić dostęp do komputera osobom nieuprzywilejowanym. Należy wystrzegać się tego typu konstrukcji, a w miejsce niebezpiecznej funkcji strcpy stosować bardziej bezpieczną strncpy.

Oto bezpieczna wersja poprzedniego programu:

 #include <stdio.h>
 #include <string.h>
 #include <stdlib.h>
 
 int main(int argc, const char **argv) {
   char haslo_poprawne = 0;
   char haslo[16];
 
   if (argc!=2) {
     fprintf(stderr, "uzycie: %s haslo", argv[0]);
     return EXIT_FAILURE;
   }
 
   strncpy(haslo, argv[1], sizeof(haslo) - strlen(haslo) - 1);
   haslo[sizeof haslo - 1] = '\0';
   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;
 }

Bezpiecznymi alternatywami do strcpy i strcat są też funkcje strlcpy oraz strlcat opracowane przez projekt OpenBSD i dostępne do ściągnięcia na wolnej licencji: strlcpy, strlcat. strlcpy() działa podobnie do strncpy: strlcpy (buf, argv[1], sizeof buf);, jednak jest szybsza (nie wypełnia pustego miejsca zerami) i zawsze kończy napis nullem (czego nie gwarantuje strncpy). strlcat(dst, src, size) działa natomiast jak strncat(dst, src, size-1).

Do innych niebezpiecznych funkcji należy np. gets zamiast której należy używać fgets.

Zawsze możemy też alokować napisy dynamicznie:

 #include <stdio.h>
 #include <string.h>
 #include <stdlib.h>
 
 int main(int argc, const char **argv) {
   char haslo_poprawne = 0;
   const char *haslo;
 
   if (argc!=2) {
     fprintf(stderr, "uzycie: %s haslo", argv[0]);
     return EXIT_FAILURE;
   }
 
   haslo = malloc(strlen(argv[1]) + 1); /* +1 dla znaku null */
   if (!haslo) {
     fputs("Za malo pamieci.\n", stderr);
     return EXIT_FAILURE;
   }
 
   strcpy(haslo, argv[1]);
   if (!strcmp(haslo, "poprawne")) {
     haslo_poprawne = 1;
   }
 
   if (!haslo_poprawne) {
     fputs("Podales bledne haslo.\n", stderr);
     return EXIT_FAILURE;
   }
   puts("Witaj, wprowadziles poprawne haslo.");
   free(haslo);
   return EXIT_SUCCESS;
 }

Zawsze możemy też użyć łańcucha typu linked list:

#include <stdio.h>
 
int main(int argc, const char **argv)
{
    char ok = 0;
    string haslo;
    haslo->wpisz(argv[1]);
    string tmp;
    tmp->wpisz("poprawne");
    if(haslo.porownaj(tmp))
    puts("Witaj");
    else
    puts("Blad");
    putchar('\n');
    free_string(tmp);
    free_string(haslo);
    return 0;
}

Nadużycia z udziałem ciągów formatujących[edytuj]

Jednak to nie koniec kłopotów z napisami. Wielu programistów, nieświadomych zagrożenia często używa tego typu konstrukcji:

 #include <stdio.h>
 int main (int argc, const char **argv)
 {
   printf (argv[1]);
   return 0;
 }

Z punktu widzenia bezpieczeństwa jest to bardzo poważny błąd programu, który może nieść ze sobą katastrofalne skutki! Prawidłowo napisany kod powinien wyglądać następująco:

 #include <stdio.h>
 int main (int argc, const char **argv)
 {
   printf ("%s", argv[1]);
   return 0;
 }

lub:

 #include <stdio.h>
 int main (int argc, const char **argv)
 {
   fputs (argv[1], stdout);
   return 0;
 }

Źródło problemu leży w konstrukcji funkcji printf. Przyjmuje ona bowiem za pierwszy parametr łańcuch, który następnie przetwarza. Jeśli w pierwszym parametrze wstawimy jakąś zmienną, to funkcja printf potraktuje ją jako ciąg znaków razem ze znakami formatującymi. Zatem ważne, aby wcześnie wyrobić sobie nawyk stosowania funkcji printf z co najmniej dwoma parametrami, nawet w przypadku wyświetlenia samego tekstu.

Konwersje[edytuj]

Czasami zdarza się, że łańcuch można interpretować nie tylko jako ciąg znaków, lecz np. jako liczbę. Jednak, aby dało się taką liczbę przetworzyć musimy skopiować ją do pewnej zmiennej. Aby ułatwić programistom tego typu zamiany powstał zestaw funkcji bibliotecznych. Należą do nich:

  • atol, strtol - zamienia łańcuch na liczbę całkowitą typu long
  • atoi - zamienia łańcuch na liczbę całkowitą typu int
  • atoll, strtoll - zamienia łańcuch na liczbę całkowitą typu long long (64 bity); dodatkowo istnieje przestarzała funkcja atoq będąca rozszerzeniem GNU,
  • atof, strtod - przekształca łańcuch na liczbę typu double

Ogólnie rzecz ujmując funkcje z serii ato* nie pozwalają na wykrycie błędów przy konwersji i dlatego, gdy jest to potrzebne, należy stosować funkcje strto*.

Czasami przydaje się też konwersja w drugą stronę, tzn. z liczby na łańcuch. Do tego celu może posłużyć funkcja sprintf lub snprintf. sprintf jest bardzo podobna do printf, tyle, że wyniki jej prac zwracane są do pewnego łańcucha, a nie wyświetlane np. na ekranie monitora. Należy jednak uważać przy jej użyciu (patrz - Bezpieczeństwo kodu a łańcuchy). snprintf (zdefiniowana w nowszym standardzie) dodatkowo przyjmuje jako argument wielkość bufora docelowego.

Operacje na znakach[edytuj]

Warto też powiedzieć w tym miejscu o operacjach na samych znakach. Spójrzmy na poniższy program:

 #include <stdio.h>
 #include <ctype.h>
 #include <string.h>
 
 int main()
 {
   int znak;
   while ((znak = getchar())!=EOF) {
     if( islower(znak) ) {
       znak = toupper(znak);
     } else if( isupper(znak) ) {
       znak = tolower(znak);
     }
     putchar(znak);
   }
   return 0;
 }

Program ten zmienia we wczytywanym tekście wielkie litery na małe i odwrotnie. Wykorzystujemy funkcje operujące na znakach z pliku nagłówkowego ctype.h. isupper sprawdza, czy znak jest wielką literą, natomiast toupper zmienia znak (o ile jest literą) na wielką literę. Analogicznie jest dla funkcji islower i tolower.

Jako ćwiczenie, możesz tak zmodyfikować program, żeby odczytywał dane z pliku podanego jako argument lub wprowadzonego z klawiatury.

Częste błędy[edytuj]

  • pisanie do niezaalokowanego miejsca
 const char *tekst;
 scanf("%s", tekst);
  • zapominanie o kończącym napis nullu
 char test[4] = "test"; /* nie zmieścił się null kończący napis */
  • nieprawidłowe porównywanie łańcuchów
 char tekst1[] = "jakis tekst";
 char tekst2[] = "jakis tekst";
 if( tekst1 == tekst2 ) { /* tu zawsze będzie fałsz bo == porównuje adresy, należy użyć strcmp().  */
    ...
 }

Takich wad nie ma łańcuch typu linked-list.

Unicode[edytuj]

Wikipedia, nasz siostrzany projekt, zawiera artykuł na temat Unicode.

W dzisiejszych czasach brak obsługi wielu języków praktycznie marginalizowałoby język. Dlatego też C99 wprowadza możliwość zapisu znaków wg norm Unicode.

Jaki typ?[edytuj]

Do przechowywania znaków zakodowanych w Unicode powinno się korzystać z typu wchar_t. Jego domyślny rozmiar jest zależny od użytego kompilatora, lecz w większości zaktualizowanych kompilatorów powinny to być 2 bajty. Typ ten jest częścią języka C++, natomiast w C znajduje się w pliku nagłówkowym stddef.h.

Alternatywą jest wykorzystanie gotowych bibliotek dla Unicode (większość jest dostępnych jedynie dla C++, nie współpracuje z C), które często mają zdefiniowane własne typy, jednak zmuszeni jesteśmy wtedy do przejścia ze znanych nam już funkcji jak np. strcpy, strcmp na funkcje dostarczane przez bibliotekę, co jest dość niewygodne. My zajmiemy się pierwszym wyjściem.

Jaki rozmiar i jakie kodowanie?[edytuj]

Unicode określa jedynie jakiej liczbie odpowiada jaki znak, nie mówi zaś nic o sposobie dekodowania (tzn. jaka sekwencja znaków odpowiada jakiemu znaku/znakom). Jako że Unicode obejmuje 918 tys. znaków, zmienna zdolna pomieścić go w całości musi mieć przynajmniej 3 bajty. Niestety procesory nie funkcjonują na zmiennych o tym rozmiarze, pracują jedynie na zmiennych o wielkościach: 1, 2, 4 oraz 8 bajtów (kolejne potęgi liczby 2). Dlatego też jeśli wciąż uparcie chcemy być dokładni i zastosować przejrzyste kodowanie musimy skorzystać ze zmiennej 4-bajtowej (32 bity). Tak do sprawy podeszli twórcy kodowania Unicode nazwanego UTF-32/UCS-4.

Wikipedia, nasz siostrzany projekt, zawiera artykuł na temat UTF-32.

Ten typ kodowania po prostu przydziela każdemu znakowi Unicode kolejne liczby. Jest to najbardziej intuicyjny i wygodny typ kodowania, ale jak widać ciągi znaków zakodowane w nim są bardzo obszerne, co zajmuje dostępną pamięć, spowalnia działanie programu oraz drastycznie pogarsza wydajność podczas transferu przez sieć. Poza UTF-32 istnieje jeszcze wiele innych kodowań. Najpopularniejsze z nich to:

  • UTF-8 - od 1 do 6 bajtów (dla znaków poniżej 65536 do 3 bajtów) na znak przez co jest skrajnie niewygodny, gdy chcemy przeprowadzać jakiekolwiek operacje na tekście bez korzystania z gotowych funkcji
  • UTF-16 - 2 lub 4 bajty na znak; ręczne modyfikacje łańcucha są bardziej skomplikowane niż przy UTF-32
  • UCS-2 - 2 bajty na znak przez co znaki z numerami powyżej 65 535 nie są uwzględnione; równie wygodny w użytkowaniu co UTF-32.

Ręczne operacje na ciągach zakodowanych w UTF-8 i UTF-16 są utrudnione, ponieważ w przeciwieństwie do UTF-32, gdzie można określić, iż powiedzmy 2. znak ciągu zajmuje bajty od 4. do 7. (gdyż z góry wiemy, że 1. znak zajął bajty od 0. do 3.), w tych kodowaniach musimy najpierw określić rozmiar 1. znaku. Ponadto, gdy korzystamy z nich nie działają wtedy funkcje udostępniane przez biblioteki C do operowania na ciągach znaków.

Priorytet Proponowane kodowania
mały rozmiar UTF-8
łatwa i wydajna edycja UTF-32 lub UCS-2
przenośność UTF-8[3]
ogólna szybkość UCS-2 lub UTF-8

Co należy zrobić, by zacząć korzystać z kodowania UCS-2 (domyślne kodowanie dla C):

  • powinniśmy korzystać z typu wchar_t (ang. "wide character"), jednak jeśli chcemy udostępniać kod źródłowy programu do kompilacji na innych platformach, powinniśmy ustawić odpowiednie parametry dla kompilatorów, by rozmiar był identyczny niezależnie od platformy.
  • korzystamy z odpowiedników funkcji operujących na typie char pracujących na wchar_t (z reguły składnia jest identyczna z tą różnicą, że w nazwach funkcji zastępujemy "str" na "wcs" np. strcpy - wcscpy; strcmp - wcscmp)
  • jeśli przyzwyczajeni jesteśmy do korzystania z klasy string (tylko C++), powinniśmy zamiast niej korzystać z wstring, która posiada zbliżoną składnię, ale pracuje na typie wchar_t.

Co należy zrobić, by zacząć korzystać z Unicode:

  • gdy korzystamy z kodowań innych niż UTF-16 i UCS-2, powinniśmy zdefiniować własny typ
  • w wykorzystywanych przez nas bibliotekach podajemy typ wykorzystanego kodowania.
  • gdy chcemy ręcznie modyfikować ciąg musimy przeczytać specyfikację danego kodowania; są one wyczerpująco opisane na siostrzanym projekcie Wikibooks - Wikipedii.

Przykład użycia kodowania UCS-2:

#include <stddef.h> /* jeśli używamy C++, możemy opuścić tę linijkę */
#include <stdio.h>
#include <string.h>
 
int main() {
  wchar_t* wcs1 = L"Ala ma kota.";
  wchar_t* wcs2 = L"Kot ma Ale.";
  wchar_t calosc[25];
 
  wcscpy(calosc, wcs1);
  *(calosc + wcslen(wcs1)) = L' ';
  wcscpy(calosc + wcslen(wcs1) + 1, wcs2);
 
  printf("lancuch wyjsciowy: %ls\n", calosc);
  return 0;
}


Zobacz też[edytuj]


Przypisy

  1. Można się zatem zastanawiać czemu kompilator dopuszcza przypisanie do zwykłego wskaźnika wskazania na stały obszar, skoro kod const int *foo; int *bar = foo; generuje ostrzeżenie lub wręcz się nie kompiluje. Jest to pewna zaszłość historyczna wynikająca, z faktu, że słówko const zostało wprowadzone do języka, gdy już był on w powszechnym użyciu.
  2. Nie należy mylić znaku null (czyli znaku o kodzie zero) ze wskaźnikiem null (czy też NULL).
  3. niektóre popularne biblioteki akceptują jedynie UTF-8 np. GTK 2.0 (które z kolei jest wykorzystywane przez np. popularne wxWidgets przy kompilacji na platformę Linuksową)