C++ bez cholesterolu

Podstawowe jednostki leksykalne

Koniec linii

Poznaliśmy już dyrektywy preprocesora, oraz instrukcje. Definicje te należy jeszcze uściślić. Ustalmy jednak napierw kilka rzeczy.

Niektóre jednostki leksykalne kończą się wraz z końcem linii. Koniec linii jednak nie oznacza zawsze najbliższego znaku końca linii w tekście (czyli widzialnego w edytorze końca linii). Linia może być zawsze przedłużona, jeśli jej ostatnim znakiem (czyli poprzedzającym znak końca linii) jest znak `\'. Zatem "koniec linii" w tym wypadku oznacza najbliższy koniec linii nie poprzedzony znakiem `\'.

Większość jednostek leksykalnych jednak jest niewrażliwa na znak końca linii: jest on traktowany identycznie jak tabulator lub spacja.

DYREKTYWA PREPROCESORA zatem, zaczyna się znakiem `#' i kończy wraz z końcem linii (w programach w C niejednokrotnie da się zauważyć przedłużanie linii z dyrektywą #define).

Słowa kluczowe

Są to jednostki zapisywane ciągiem znaków literowych (do nich należy też znak `_'!), które stanowią wbudowane elementy języka. Nie można zatem ich zmieniać (używać jako identyfikatorów), one już mają ściśle określone znaczenie. Niektóre stanowią instrukcje, inne są typami danych, modyfikatorami, operatorami lub wartościami.

Oto one: and const_cast float operator static_cast using and_eq continue for or struct virtual asm default friend or_eq switch void auto delete goto private template volatile bitand do if protected this wchar_t bitor double inline public throw while bool dynamic_cast int register true xor break else long reinterpret_cast try xor_eq case enum mutable return typedef catch explicit namespace short typeid char export new signed typename class extern not sizeof union const false not_eq static unsigned.

Parę drobnych komentarzy. Jeśli komuś wydaje się, że tych słów kluczowych jest dużo, to nadmienię tylko, że C++ stosunku do swoich możliwości ma ich jeszcze bardzo mało (np. w Javie te proporcje wychodza o wiele gorzej; dla przykładu w C++ słowo throw służy do tych samych rzeczy, do których w Javie służą słowa throw i throws, takoż np. właściwość sygnowaną w Javie przez implements i extends w C++ oznacza się dwukropkiem). Wyjaśnię tylko parę drobniejszych słów, którymi nie będziemy się zajmować:

Identyfikatory

Identyfikator jest jednostką leksykalną, która ma odpowiednie odniesienie. Może się odnosić do typu, obiektu (stałego lub zmiennego) etykiety lub też funkcji (oraz makrodefinicji). Identyfikatory funkcji są obarczone w C++ innymi jednak regułami, niż pozostałe; na razie się więc zajmiemy tymi pozostałymi.

Identyfikatory mogą się składać się z małych i dużych liter (są rozróżniane!), znaku podkreślenia (nie zaleca się stosować jako pierwszy znak, ze względu na użycie go w nazwach zastrzeżonych przez kompilator i biblioteki) oraz cyfr (nie wolno stosować jako pierwszy znak). Identyfikator musi być unikalny, stanowi on bowiem jednoznaczną identyfikację tego, do czego się odnosi. Identyfikatory jednak posiadają swoje zasięgi. Będzie o nich mowa przy omawianiu bloków.

Identyfikatory mogą być wbudowane (ang. builtin), a oznaczające je słowa są słowami kluczowymi języka. Pozostałe identyfikatory są tworzone w pliku nagłówkowym lub przez użytkownika (z punktu widzenia kompilatora to jest jedno i to samo).

Identyfikatory funkcji są objęte bardziej luźnymi, ale też bardziej skomplikowanymi regułami. Identyfikatorem funkcji zatem może być ciąg wyżej wymienionych znaków lub nadający się do przeciążenia operator. Identyfikatory funkcji nie są zatem unikalne, nie są więc jednoznaczne (tzn. jeden identyfikator może oznaczać kilka różnych funkcji). Będzie to szczegółowo wyjaśnione przy właściwościach dodatkowych.

W C oczywiście identyfikatory funkcji nie posiadały takiego wyróżnienia i były takimi samymi identyfikatorami, jak pozostałe. Konsekwencją tego jest brak bezpieczeństwa podczas procesu ŁĄCZENIA (inaczej wiązania, ang. link - dotyczy to nie tylko języka C, ale również Pascala, Fortrana i asemblera). Tam bowiem zewnętrzny identyfikator funkcji składa się wyłącznie z jej nazwy (czasem jest poprzedzony podkreśleniem). Mamy np. zapowiedź funkcji:

  int Hood( const char* );

którą program łączący (ang. linker) połączy ze znalezionym symbolem `Hood'. Niestety, przez przypadek deasygnat symbolu `Hood' może być funkcją o zupełnie innym nagłówku, bądź nawet w ogóle nie być funkcją. C++ jest pod tym względem bezpieczniejszy, gdyż na podstawie deklaracji nagłówka wytwarza odpowiednio zakodowaną (ang. mangled) nazwę funkcji i taka nazwa jest potem poszukiwana przez program łączący. Powoduje to oczywiście pewną niezgodność pomiędzy C a C++ w dziedzinie łączenia. Dlatego właśnie wszelkie deklaracje funkcji z języka C poprzedzona są `extern "C"'. Oznacza to, że nazwa funkcji ma pozostać taka jaka jest i nie być zakodowana; w C++ bowiem zewnętrzna nazwa przedstawionej funkcji brzmiałaby nie `Hood', ale miałaby jakiś przyrostek, np. w gcc z serii 2 byłoby to `Hood__FPCc' (typ zwracany nie jest ujęty w nazwie). O kodowaniu nazw w gcc 2 wspomniałem w dodatku.

Kodowanie (manglowanie) nazw ma znaczenie nie tylko dla bezpieczeństwa typów. Ciąg znaków dodawany do nazwy funkcji stanowi też dodatkową identyfikację tej funkcji (z kolei operatory też muszą być zakodowane przy pomocy odpowiednich identyfikatorów literowych - i to jest właśnie jeden z powodów niezalecania używania znaku `_' jako pierwszego znaku identyfikatora).

Literały

Literały są to oznaczenia obiektów stałych określonego typu. Ich obsługą zajmuje się kompilator. Mogą to być:

Stałe liczbowe mogą być różnie typowane (często też w zależności od kontekstu). Pierwsza z nich, 0, może przybierać najróżniejsze typy, na które może być to 0 konwertowane. Kolejne dwie to stałe całkowite, przy czym pierwsza może (nie musi!) być liczbą całkowitą bez znaku. Kolejna liczba jest zmiennoprzecinkowa (nazwa maszynowa), czy jak kto woli - rzeczywista (nazwa matematyczna). Nie są to synonimy, gdyż liczba zmiennoprzecinkowa nie jest jedyną możliwą maszynową reprezentacją liczby rzeczywistej (ale jedyną standardową w C++).

Kolejna liczba zaczyna się od `0'. Wbrew pozorom, nie jest ona wyróżniona bez powodu i nie jest to to samo, co 15. Literał ten ma wartość całkowitą odpowiadającą 13, jest to bowiem liczba zapisana nie w systemie dziesiętnym, lecz ósemkowym (ang. octal).

Ostatnia liczba jest również liczbą całkowitą, ale w systemie szesnastkowym (ang. hexadecimal). Jeśli ktoś nie wie nic o systemie ósemkowym i szesnastkowym, to - przykro mi - tutaj wyjaśnienia nie znajdzie (może spytać swojego nauczyciela/wykładowcę od matematyki ;*).

Opiszę jeszcze dokładniej reguły dotyczące literałów napisowych i znakowych. Literał znakowy może zawierać jeden znak (tzn. w niektórych szczególnych przypadkach może ich zawierać do czterech, ale o tym za chwilę). Może być to znak pisany lub znak specjalny.

Literał napisowy może się składać z (teoretycznie) dowolnej ilości znaków i symbolizuje tablicę składającą się z przedstawionych znaków z dodatkiem na końcu znaku o kodzie 0. Znaki specjalne używane w tych literałach rozpoczynają się znakiem `\' i mogą to być:

\n
LF - przejście do następnej linii ("wysunięcie linii")
\r
CR - skok na początek linii (skrót pochodzi od angielskiego Carriage Return [przyp.AP.])
\t
HT - tabulacja pozioma
\v
VT - tabulacja pionowa
\a
BL - sygnalizacja (tzw. BELL, kiedyś wpisanie tego znaku wywoływało sygnał dźwiękowy z zainstalowanego w komputerze małego głośniczka)
\f
FF - wysunięcie strony w drukarce (na dobrych terminalach również skasowanie ekranu)
\xNN
znak o kodzie szesnastkowym 0xNN
\DDD
znak o kodzie dziesiętnym DDD
\\
znak \
\'
znak '
\"
znak "

Znaki ' i " muszą być umieszczone po \, gdyż w przeciwnym razie byłyby potraktowane jako terminator literału. Jeśli jednak znak ten nie rozpoczyna literału, nie musi (ale może) być poprzedzony `\', tzn. wewnątrz literału napisowego można stosować znak ' i ".

Ze znakiem specjalnym \DDD jest parę dziwnych rzeczy, którymi się specjalnie nie będę zajmował, w każdym razie nie wszystkie trzy cyfry muszą być napisane, tzn. mogą być pominięte wiodące zera (np. można napisać \1). Nie wiem tylko jak są wtedy traktowane znaki, które mają być cyframi za nim, ale szczerze mówiąc nie widzę konieczności skupiania się na tym. Uzupełnienie AP: samo 0 jest przez język C++ traktowane jak stała ósemkowa.

Literały mogą być oznaczane jeszcze literami przyrostkowymi (liczby) lub przedrostkowymi (znaki). Są to: `l' i `u' dla liczb oraz `l' dla znaków. Przykładowo może to być -1u, 12l, L"Janek" lub L'FORM'. Dokładniej typy literałów zostaną przedstawione w następnym rozdziale.

Mała ciekawostka co do znaków \n i \f: proszę nie zapominać, że pierwsze komputery, które potrafiły się posługiwać znakami, nie posiadały monitorów, ale drukarki znakowe. Nazewnictwo zatem niektórych znaków sterujących jeszcze pamięta te czasy. Znak \n oznaczał mianowicie wysunięcie papieru o wielkość jednej linii tekstu, a \f wysunięcie strony papieru do końca. Proszę też zwrócić uwagę, że \n to sygnał dla wałka, a nie dla głowicy; dla głowicy przeznaczony był znak \r; z tego też powodu całe "przejście do następnej linii" należało zrealizować przez \r\n (tak samo na maszynie do pisania machnięcie wajchą składało się z dwóch faz: pociągnięcia do oporu przekręcenia wałka o jedną linię [\n] i przesunięcie całego wałka w lewo [\r]). W systemach uniksowych od samego początku zatwierdzono, że znak \n ma wykonywać obie te czynności, więc teksty uniksowe są kończone tylko tym znakiem. Inne systemy jednak w tej kwestii również pamiętają tamte czasy, co najwyżej niektóre edytory (jak np. edit.com, czy edytory Nortona Commandera lub Dosa Navigatora) są jeszcze na tyle inteligentne, żeby honorować znak \n jako znak końca linii (nie należy do nich windowsowy notatnik; efekt wczytania do niego tekstu uniksowego jest efektem może śmiesznym, ale niezbyt zachwycającym).

Komentarze

W C++ są dwa rodzaje komentarzy: B i C. Pierwszy z nich istniał w języku B, ale nie zaakceptował się w języku C (mówię o ANSI C; w ISO C 99 już to dodali). Rozpoczyna się on `//' i kończy wraz z końcem linii (pamiętaj o `\'!). Przykładu chyba podawać nie muszę (zresztą w przykładach niejednokrotnie on wystąpi).

Drugi rodzaj komentarzy, który jako jedyny istnieje w C (przynajmniej w standardzie ANSI C, bo gcc to ograniczenie olewa ;*) to rozpoczynający się sekwencją `/*' i kończący `*/'. Koniec linii go nie zraża, ale proszę pamiętać, że takich komentarzy nie można zagnieżdżać! Osobiście całkowicie odradzam ich używanie zgodne z przeznaczeniem (czyli do komentowania ;*). Jedyny wyjątek mógłbym przyznać takiej oto postaci komentarza (bo ładnie wygląda, a poza tym umożliwia "wygrepowanie" takich komentarzy przez szukanie gwiazdki na drugiej pozycji: grep '^.\*' plik.cc):

/*
 * Tak profesjonalni programiści
 * komentują swoje wspaniałe
 * twórcze wypociny ;*)
 */

Staraj się jednak nie stosować tego komentarza wewnątrz bloku, bo jak dobrze wiesz, programiści często lubują się w blokowaniu kodu za pomocą komentarzy. Z drugiej jednak strony, to akurat również odradzam - nie blokuj nigdy dużych fragmentów kodu za pomocą komentarzy (kompilator powinien, ale nie musi ostrzec, że wewnątrz komentarza znajduje się sekwencja `/*'). Stosuj preprocesor:

tu_skompilujemy();
#if 0
A tu nie ;*)
#endif
jedziemy_dalej = 0;

Daje nam to dodatkowo możliwość odblokowywania kodu w zależności od potrzeby, zmieniając tylko 0 na 1. Osobiście jednak przyznam, że blokowanie fragmentów kodu nie stanowiło dla mnie nigdy czegoś tak istotnego, żeby myśleć o takich rzeczach ;*)