5.4 Tablice znaków (C-napisy)

Nieco specjalne są tablice i wskaźniki znakowe. Związane jest to z faktem, że w klasycznym C nie ma typu napisowego, a rolę zmiennych napisowych pełnią tablice znaków, przy czym koniec napisu oznaczany jest znakiem ' \0'. Znak ten nazywany jest NUL; nie należy go mylić ze wskaźnikiem pustym NULL (podwójne L). Znak NUL to znak o kodzie ASCII równym zeru, który nie odpowiada żadnemu znakowi graficznemu. Zauważmy, że NULL (przez dwa 'L') jest zdefiniowaną nazwą preprocesora, której możemy używać w tekście programów (choć zaleca się stosować w jej miejsce liczbowego zera lub, lepiej, nullptr); NUL natomiast jest tradycyjną nazwą znaku zerowego ale nie jest zdefiniowaną nazwą preprocesora (makrem). Znak ten możemy wprowadzić do kodu programu używając literału ' \0' (z apostrofami).


Różne możliwości definiowania tablic znakowych (C-napisów) znajdujemy w następującym programie. W linii 9 ' char tab1[] = "Kasia";' tworzy tablicę sześciu (sic!) znaków na podstawie literału napisowego: pięć znaków imienia i jako szósty znak ' \0', który musi tam być, aby oznaczyć koniec napisu — kompilator doda ten znak automatycznie.


P23: tabchar.cpp     Napisy: tablice znaków

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  void napisz (const char* tab) {
      5.      cout << "Napis: " << tab << endl;
      6.  }
      7.  
      8.  int main() {
      9.      char  tab1[] = "Kasia";
     10.      char  tab2[] = {'B', 'a', 's', 'i', 'a', '\0'};
     11.      const char *tab3 = "Wisia";
     12.      cout << "Wymiar    tab1: "  << sizeof(tab1)   << endl;
     13.      cout << "Wymiar    tab2: "  << sizeof(tab2)   << endl;
     14.      cout << "Wymiar    tab3: "  << sizeof(tab3)   << endl;
     15.      cout << "Wymiar \'Wisia\': "<< sizeof("Wisia")<< endl;
     16.  
     17.      tab1[0] = 'B';
     18.      tab2[0] = 'K';
     19.      //tab3[0] = 'C'; // ŹLE
     20.  
     21.      napisz(tab1);
     22.      napisz(tab2);
     23.      napisz(tab3);
     24.  }

Dlatego, jak widać z poniższego wydruku, wymiar tablicy tab1 jest 6:

    Wymiar    tab1: 6
    Wymiar    tab2: 6
    Wymiar    tab3: 8
    Wymiar 'Wisia': 6
    Napis: Basia
    Napis: Kasia
    Napis: Wisia
Taki sam jest wymiar tablicy tab2, gdzie inicjalizacji dokonaliśmy sposobem analogicznym do tego, jaki znamy dla tablic innych typów: poprzez podanie wartości kolejnych elementów tablicy w nawiasie klamrowym. Tu znak ' \0' musieliśmy dodać „ręcznie”. W obu przypadkach powstały sześcioelementowe statyczne tablice znakowe, które, jak widać dalej w programie, można modyfikować.

Szczególna jest definicja zmiennej tab3. Inicjujemy tu zmienną typu const char* adresem literału napisowego. Jest to zmienna wskaźnikowa, więc jej wymiar wynosi 8 (lub 4 na maszynie 32-bitowej). Wydawałoby się, że odpowiada to przypadkowi pierwszemu (linia 9). Jest to jednak co innego. W linii 9 utworzyliśmy literał napisowy, następnie na stosie utworzona została tablica sześcioelementowa, do której przekopiowany został, znak po znaku, ten literał (wraz z kończącym znakiem ' \0'). Po przekopiowaniu tab1 jest normalną, modyfikowalną tablicą znaków o wymiarze 6.

Natomiast definiując tab3 utworzyliśmy trwały literał napisowy, nie na stosie, i przypisaliśmy adres początku tego napisu do zmiennej tab3. Sama tablica będzie utworzona w niemodyfikowalnym obszarze pamięci. Dlatego typ wskaźnika powinien być const char* a nie char* — o modyfikatorze const powiemy w następnym rozdziale. Zauważmy, że kompilator pozwoliłby nam zadeklarować typ tab3 jako char* (bez const), ale program i tak załamałby się przy próbie modyfikacji elementu wskazywanej tablicy (ta niekonsekwencja jest zaszłością historyczną).

Zauważmy też, że przekazując do funkcji napis w postaci tablicy znakowej nie musimy przekazywać jej wymiaru: obecność znaku ' \0' pozwala bowiem określić koniec napisu, a więc i jego długość.

Zwróćmy też uwagę na fakt, że wyjątkowe jest traktowanie zmiennych typu char* (lub const char*) przez operator wstawiania do strumienia. W zasadzie po 'cout « nap', gdzie nap jest typu char*, powinna zostać wypisana wartość zmiennej nap, czyli pewien adres (tak by było, gdyby zmienna nap była np. typu int*). Wskaźniki do zmiennych typu char są jednak traktowane odmiennie: zakłada się, że wskaźnik wskazuje na pierwszy znak napisu i wypisywane są wszystkie znaki od tego pierwszego poczynając aż do napotkania znaku ' \0'. Lepiej więc, żeby ten znak ' \0' tam rzeczywiście był!

Więcej na temat napisów w rozdziale im poświęconym .

T.R. Werner, 23 lutego 2019; 23:59