Jak pisać kod programu,
aby był czytelny

Każdy pisać może ... (cz.1)

Przedstawiony poniżej artykuł autorstwa Marka Kotowskiego został opublikowany w jednym z numerów czasopisma PC Kurier i traktuje o stylu kodowania w języku C. Ponieważ obecnie obserwuje się wśród studentów brak jakiegokolwiek stylu programowania, zalecane jest przeczytanie niniejszego artykułu.

Wstęp

Program jest zapisem myśli ludzkiej, i tak jak myśl ludzka, może być zapisany różnie: jasno i zrozumiale, ale też zawile i nieczytelnie. Dotyczy to także - a może zwłaszcza - programów w języku C, który dzięki bogatemu zestawowi operatorów umożliwia dużą elastyczność w kodowaniu operacji.

Ten sam algorytm w języku C można zakodować na zupełnie różne sposoby: tak, że widać od razu, jakie operacje realizowane są w danym fragmencie programu, albo jako swoistą łamigłówkę, wymagającą długiego czasu i bardzo wnikliwej analizy, by zrozumieć intencje programisty (takie programy określa się czasami jako "write-only" - napisane, nie dają się odczytać).

Na to nakładają się naturalne różnice stylu programistów, jako że i w programowaniu funkcjonuje - i to w stopniu większym, niż można by się spodziewać - zasada, że "człowiek to styl". Programista, jeśli nie narzucono mu żadnych standardów kodowania, pisze programy wedle zasad stylistycznych nabytych w trakcie nauki języka, wpojonych mu na kursach programowania, podpatrzonych w programach innych autorów, czy wreszcie wypracowanych w oparciu o własne doświadczenia. Nie ma zbyt wielkiej przesady w stwierdzeniu, że stylów kodowania jest tyle, ilu jest programistów.

Określenie zasad poprawnego stylu kodowania nie jest sprawą prostą i w wielu aspektach sprowadza się bardziej do formułowania zaleceń, często dyskusyjnych, niż jednoznacznych reguł, co do których panowałaby powszechna zgoda. Łatwiej jest wskazać, co jest zakodowane w złym stylu.

Ludzie programy piszą ...

Oto cztery różne przykłady.

Widziałem program, w którym - po to, by kod zajmował możliwie mało miejsca - definicje kolejnych funkcji, notabene pozbawionych zupełnie nagłówków komentarzowych, rozpoczynały się w tych samych liniach, w których znajdowały się nawiasy klamrowe zamykające poprzednie funkcje:

f1){
....
} f2(){
....
} f3(){
....
}

Oczywiście zapis ten, jakkolwiek dla wielu programistów zaskakujący, jest najzupełniej uprawniony (bezargumentowe funkcje f1, f2 i f3 są domyślnie typu int). Prototypy funkcji - jeśli można to prototypami nazwać - również były zapisane w programie tak, by zajmowały jak najmniej miejsca - w jednej linii, z domyślnymi typem int i pustym argumentem:

f1();f2();f3();

Innym przykładem, będącym doprowadzeniem do krańcowości stylu z poprzedniego przykładu, był program napisany "jak leci" (podobnie pisane programy bywają często przytaczane w podręcznikach - jako przykłady wyjątkowo złego stylu kodowania):

main(){int k, suma=0; for(k=1; k<=10; k++) suma+=k;
printf("\nSuma=%d",suma);}

Autor programu, notabene bardzo młody człowiek, stwierdził, że ten sposób pisania "został mu" po konkursie dla młodych programistów, w którym brał udział i którego zasady wymagały, by rozmiar programu nie przekroczył 30 linii, przy czym pojęcie linii celowo nie było sprecyzowane (prawdę rzekłszy, konkursów wymuszających taki styl kodowania lepiej byłoby nie robić).

Niezależnie od tego można oczywiście napisać program, który jakkolwiek na poziomie zdań w miarę jasno zakodowany, nie będzie czytelny ani łatwy do modyfikowania. Widziałem kiedyś program, w którym znajdowała się jedna "super-funkcja" (tak ją nazwał jej autor) rozciągająca się na trzy strony wydruku (!) i zawierająca siedem (!) zagnieżdżonych bloków:

for(...)
{
  ...
  while(...)
    {
      if(...)
      {
        for(...)
        {
          ...
          switch(...)
          {
            ...itd.

Powodem stworzenia takiej konstrukcji miało być wydatne zmniejszenie czasu wykonywania programu. W super-funkcji przeglądanych było równocześnie kilka dużych tablic, co oczywiście zabierało czas. Autor programu uznał, że wskazane jest zminimalizować wywoływanie innych funkcji, by uniknąć obciążenia czasowego związanego z kładzeniem na stos argumentów, adresów powrotu itd. Stąd opisana konstrukcja super-funkcji.

Niestety, okazała się ona nader kłopotliwa. Ponieważ bloki w super-funkcji rozciągały się na trzy strony - zatem w środku drugiej strony wydruku nie sposób było stwierdzić, do jakiego bloku należy dane zdanie. Autor programu połączył więc na wydruku różnobarwnymi liniami pionowymi nawiasy klamrowe, rozpoczynające i zamykające poszczególne bloki: czerwoną linią - bloki pętli for, zieloną - bloki pętli while itd. Pomogło to niewiele. Program rychło stał się całkowicie niemodyfikowalny przez samego autora, który w końcu napisał go na nowo, rozbijając ową super-funkcję na szereg funkcji mniejszych. I żeby morał był pełny: program po tych modyfikacjach działał szybciej niż w wersji pierwotnej.

I przykład ostatni:

...
init_zamowie(code)
int code;{
int nstr; extern int yyprevious;
while((nstr = yylook()) >= 0)
yyfussy: switch(nstr){
case 0:
if(yywrap())return(0);break;
case 1:
(strncpy(arg1, yytext, 2);
...

Jest to początkowy fragment funkcji o nazwie "init_zamowie" ("initowanie zamówienia"?!), definiowanej w starym stylu, i - jak widać - w języku polsko-angielskim. Do etykiety "yyfussy " następuje skok (w tył) z głębi funkcji, i to za pomocą makroinstrukcji(!).

#define REJECT {nstr = yyreject(); goto yyfussy;}

Trudno byłoby samemu wymyślić przykład gorszego stylu i prawdopodobnie wielu Czytelników będzie wątpić w autentyczność tego kodu. Funkcja ta pochodzi z jednego z kilku programów - niestety, kodowanych w podobnym stylu - pisanych w ubiegłym roku przez programistów znanej warszawskiej firmy i sprzedawanych jako dobre i sprawdzone produkty.

Styl: czego, po co i jaki?

Pytanie pierwsze: styl czego? Otóż istnieją co najmniej dwa pojęcia stylu: styl kodowania, który odnosi się bezpośrednio do procesu tworzenia kodu, i styl programowania, który jest pojęciem szerszym, mniej precyzyjnym i często różnie rozumianym. Interpretowany szeroko, styl programowania obejmuje cały proces tworzenia programu, łącznie z definiowaniem struktur danych i projektowaniem algorytmów, i wreszcie z samym kodowaniem. Używając dalej słowa "styl", będziemy rozumieli go w tym pierwszym sensie, jako styl kodowania, jakkolwiek w kilku miejscach nawiążemy do jego szerszego znaczenia.

Pytanie drugie: po co mówić o stylu kodowania? O co kruszyć kopie, skoro np. ostatni z opisanych wyżej programów przykładowych działa poprawnie? Czyż nie lepiej zostawić wolną rękę programiście, który stosując własny i najbardziej mu odpowiadający styl kodowania, będzie tworzył programy najoptymalniejsze i w dodatku szybko?

Styl kodowania był zawsze ważny, przy programowaniu w jakimkolwiek języku symbolicznym, także w języku asemblera, z przyczyn, o których niżej. Owszem, prawdą jest, że tym, co decydowało o jakości programu jeszcze kilkanaście lat temu - a przynajmniej bardzo często tak rzecz traktowano - był jego rozmiar i czas działania. Im krótszy kod i krócej działający, tym lepszym programistą był jego autor. Duża część wysiłku programisty była kierowana na skrócenie czasu działania programu. Ale wraz ze wzrostem pojemności pamięci i szybkości działania komputerów, kryteria efektywności przestały być tak istotne. Rozwijający się rynek i silna konkurencja, a także czynniki ekonomiczne: tanienie sprzętu i wzrost ceny pracy programisty spowodowały przesunięcie akcentów. Duży złożony program zazwyczaj nie jest skończonym i zamkniętym tworem - żyje, jest wzbogacany o nowe opcje, implementowany na nowym sprzęcie lub w nowym środowisku systemowym, zmieniają się pracujący nad nim programiści. Dlatego też dzisiaj ważniejsze są inne sprawy: czytelność programu, łatwość jego modyfikowania i rozwijania.

Oczywiście nie znaczy to, że efektywność programu przestała być ważna. Przeciwnie - i nie ma tu sprzeczności. Po pierwsze: gdy nie ma innych kryteriów (patrz niżej), o wyborze takiej czy innej konstrukcji językowej powinny decydować względy efektywności. Po drugie, i ważniejsze: właściwy styl kodowania w istocie zwiększa efektywność programu (patrz przykład super-funkcji wyżej), nawet jeśli na pierwszy rzut oka wydaje się, że jest przeciwnie. Wreszcie, prawdę rzekłszy, użyteczność programu, który będąc wysoce efektywnym, jest skonstruowany tak zawile i zakodowany tak niejasno, że nie sposób go skutecznie rozwijać, jest dyskusyjna.

Z powyższych uwag wyłania się pierwsza odpowiedź na pytanie: co to jest dobry styl kodowania? Dobrze napisany program powinien być zatem czytelny i łatwo modyfikowalny. Jak to ujął zwięźle Stephen Meadows, program musi być "new programmer friendly" - przyjazny dla programisty, który przejmuje pracę nad programem. Czytelność programu jest jednym z czynników, na które Brian Kernighan i Dennis Ritchie (określani będą dalej stosowanym powszechnie skrótem K&R) nieustannie kładą nacisk w swojej klasycznej już książce "The C Programming Language" (w indeksie tej książki drugim z haseł o największej liczbie odwołań do tekstu jest "readability", czyli czytelność programu.) Wiele z podanych niżej uwag na temat czytelności programu było już sformułowanych przez K&R w ich książce.

Czytelność programu nie jest oczywiście jedynym kryterium poprawności stylu kodowania. Dobrze napisany program powinien być również łatwy w testowaniu i stylistycznie spójny (o cechach tych będzie jeszcze mowa). Wreszcie - last but not least - program powinien być przenośny. Przenośność programu jakkolwiek związana z czytelnością programu, jest jednak zagadnieniem oddzielnym, i chociaż będziemy o niej wspominać, w istocie problem wykracza poza zakres niniejszych rozważań (nie będziemy również - poza jednym wyjątkiem - mówić o elementach stylu związanych z obsługą błędów, czyli o szeroko rozumianym programowaniu defensywnym).

Każde z wymienionych słów-haseł (notabene nie wyczerpują one wszystkich cech, jakie musi posiadać poprawny stylistycznie program) jest na tyle wieloznaczne, że może być różnie rozumiane. Samo słowo "czytelność" w odniesieniu do programu wydaje się na pierwszy rzut oka dość jasne. A przecież liczba czynników, które wchodzą w grę - tj. mogą tę czytelność zwiększyć lub zmniejszyć - jest duża. Każdy programista może rzecz widzieć inaczej. To, co dla jednego programisty jest czytelne, drugiemu może wydać się zbyt zawiłe, niejasne i mylące. Co więcej: każdy może przytoczyć zupełnie racjonalne i czasami trudne do zbicia argumenty na obronę swojego punktu widzenia.

Trzeba również pamiętać - i jest to w istocie truizm - że poprawny styl kodowania nie gwarantuje, że program będzie łatwo modyfikowalny. Nawet najbardziej czytelnie napisany kod może okazać się w sumie trudny do rozwijania z powodu niewłaściwej struktury całego programu, źle wydzielonych modułów i błędnie zaprojektowanych interfejsów między nimi, nieprzemyślanych i niespójnych struktur danych itd. Problemy tego typu wykraczają jednak poza zakres tematyczny tego tekstu i nie będziemy ich omawiać. Opiszemy natomiast niektóre wybrane elementy stylu kodowania w języku C, poczynając od spraw nie tyle mniej ważnych, co nie wpływających bezpośrednio na kod wynikowy, aż do konstrukcji językowych i używania danych.

Uwagi poniższe w żadnym wypadku nie pretendują do rangi systematycznej analizy. Mają raczej charakter przykładowy, pokazując wybrane zasady kodowania programów w języku C oraz kryteria, jakimi należy posługiwać się przy wybieraniu takiej czy innej konstrukcji językowej czy wręcz tworzeniu swojego własnego stylu kodowania (tam, gdzie sprawa jest dyskusyjna, jest to wyraźnie zaznaczone). Prawdę rzekłszy, każdy programista w miarę zyskiwania doświadczenia, sam dochodzi do pewnych reguł stylistycznych, które zazwyczaj zmierzają w tym samym kierunku. Takim Czytelnikom uwagi te, a przynajmniej niektóre z nich, mogą wydać się oczywistościami.

Zasady ogólne

Istnieje kilka zasad ogólnych dotyczących stylu kodowania. Nie tworzą one systematycznego zestawu reguł, ale są przydatne jako ogólne wskazówki (odnoszą się zresztą nie tylko do języka C). Nie są też rozłączne, a raczej w dużym stopniu się dopełniają. Niektóre są wręcz innymi sformułowaniami tego samego.

Naczelną i w istocie najogólniejszą zasadą jest, by pisząc kod, myśleć o jego potencjalnym przyszłym czytelniku, łącznie z sobą samym. Powinien to być truizm, ale niestety, nie zawsze jest. Bywa, że napisany niedbale kod przestaje być zrozumiały dla samego jego autora już miesiąc po napisaniu.

Prostą konsekwencją powyższej zasady jest zalecenie, by w programie ujawniać intencje, np. podawać explicite typy funkcji, rzuty przy zmianach typów itd. (będzie o tym mowa niżej).

Bardzo zbliżoną do obydwu opisanych wyżej zasad jest trzecia, znana pod skrótem SWYM - "Say What You Mean" ("Mów, co masz na myśli") i zalecająca kodowanie operacji w sposób odzwierciedlający jej sens, bez używania niejawnych cech języka (patrz niżej).

Zasada czwarta jest nieco odmienna i kładzie akcent na prostotę programu. Została ona sformułowana przez R.B. Smitha i określana jest skrótem KISS: "Keep It Simple, Stupid", co w wolnym tłumaczeniu oznacza "Dbaj o prostotę (programu - M.K.), głupcze'" (nieco szerzej o niej: w PCkurierze 25/93, M. Kotowski "Dekoracje w języku C", s. 157).

W istocie większość omówionych niżej zaleceń i elementów poprawnego stylu kodowania jest rozwinięciem tych zasad i przełożeniem ich na konkrety. Zanim jednak do nich przejdziemy, trzeba powiedzieć o pewnej sprawie dyskusyjnej.

Oto dwie równoważne wersje testu niezerowej wartości zmiennej "Licznik":

if (Licznik)                     if (Licznik != 0)
{                                {

Wersja druga jest zazwyczaj traktowana jako bardziej komunikatywna (warunek w dyrektywie if jest sformułowany explicite). Rzecz w tym, że wersja pierwsza, jakkolwiek formalnie sprzeczna z zasadą SWYM, jest powszechnie stosowana i można ją spotkać w wielu programach przykładowych, dołączanych do kompilatorów, gdzie występuje jako całkowicie poprawna konstrukcja (uważa się, że intencja programisty jest sformułowana dostatecznie jasno).

Podobnie rzecz się ma z innym domyślnym elementem języka C: traktowaniem nazwy tablicy jako jej adresu, np.:

char bufor[100];
...
gets(bufor);

Jeśli programista ma kłopoty ze zrozumieniem tych konstrukcji językowych, to - i argument ten jest trudny do odrzucenia - zna język C w stopniu zbyt słabym i lepiej niech nie zabiera się do czytania poważniejszych programów. Dylemat ten będzie wracał niżej: gdzie leży granica między złożonością kodu dopuszczalną a tą uznawaną za szkodliwą? Innymi słowy: jaki poziom znajomości języka C zakładać u potencjalnego czytelnika tworzonego programu?

Oprócz podanych wyżej zasad jest jeszcze jedna. Jest to zasada spójności: po wybraniu jednego rodzaju stylu czy zestawu reguł stylistycznych, należy konsekwentnie się ich trzymać. Uwaga ta dotyczy wszystkich elementów stylu. Program zakodowany w złym stylu czyta się źle. Program napisany w "stylu zmiennym" czyta się jeszcze gorzej. Z tego właśnie względu, modyfikując czyjś program, należy w miarę możliwości zachować we wprowadzanych poprawkach oryginalny styl programu.

Język C - po angielsku i po polsku

Tym, czym program w wersji źródłowej objawia się czytelnikowi - pomijając słowa kluczowe języka C - są identyfikatory, tj. nazwy nadawane zmiennym, funkcjom, typom i etykietom oraz komentarze. Pojawia się pytanie zasadnicze: pisać je po polsku czy po angielsku?

Oczywiście są sytuacje, gdzie wybór jest narzucony z góry, np. w przypadku programów pisanych dla firmy zachodniej, których kod źródłowy z założenia musi być w języku obcym (najczęściej angielskim). Jeśli z kolei pisze się przykładowe programy na kurs języka programowania w języku C, naturalny jest wybór języka polskiego. Ale są to, rzecz jasna, przypadki szczególne. Zazwyczaj sprawa nie jest tak oczywista.

Przede wszystkim trzeba pamiętać, że standardowy zbiór znakowy języka C obejmuje znaki łacińskie. W kodzie ASCII odpowiadają im znaki z tzw. pierwszej połówki kodu (w samym standardzie ANSI C nie ma oczywiście mowy o standardzie kodu). Wartości kodów znaków polskich zarówno w standardzie LATIN H, jak i w MAZOVII należą do tzw. drugiej połówki (wartości 128-255). Owszem, w komentarzach czy w stałych znakowych można używać znaków o wartościach kodów wyższych niż 127. Nie można natomiast napisać zmiennych o nazwach "Wskaźnik" czy funkcji o nazwach "Czyść", "Ładuj", "Wiąż", "Łącz". Chcąc mimo to stosować takie nazwy, trzeba posługiwać się quasi-polskimi słowami "Wskaznik", "Czysc", "Laduj", "Wiaz" czy "Lacz", co może prowadzić do ich złej interpretacji przez czytelnika. Ponadto, jeśli w nazwach zmiennych będziemy chcieli użyć tzw. notacji węgierskiej, może to okazać się dwuznaczne, a przynajmniej niespójne (przedrostki nazw pochodzące z języka angielskiego, nazwy polskie). Można wprawdzie pokusić się o stworzenie polskiej wersji tej notacji, ale nie byłoby to łatwe, a i sam sens takiego przedsięwzięcia byłby dyskusyjny.

W tej sytuacji lepszym wyjściem wydaje się kodowanie całego programu w języku angielskim (właściwie zalecenie naturalne w czasach, gdy powszechnie używane i odmieniane po polsku są słowa dealer, leasing, notebook itp.). Tak też były kodowane programy w zespołach programistycznych w IMM, gdzie pracowałem, i nigdy nie było problemów z ich zrozumieniem. Prawdę rzekłszy, programista, utrzymujący się zawodowo z programowania, powinien znać język angielski przynajmniej biernie.

Wspomniani tłumacze książki K&R zdecydowali się na kompromis: w przykładach nazwy zmiennych podawano w oryginalnym angielskim brzmieniu, a komentarze pisano po polsku. Jest to rozwiązanie o tyle dobre, że programista łatwo znajdzie odpowiednie nazwy angielskie na zmienne lub na funkcje (Table, CheckValue itp.). Trudniej mu natomiast będzie, zwłaszcza jeśli słabo zna angielski, opisać w tym języku sens kodowanych operacji. Niemniej niespójność pozostała.

Jakąkolwiek wersję językową się wybierze, należy oczywiście być konsekwentnym. Widziałem "angielsko- polski" program w języku C, w którym sąsiadowały obok siebie nazwy polskie i angielskie (np. zmienne Licznik i Counter, funkcje GetData i WczytajPlik itp.). I nie było w tym żadnej szczególnej motywacji (jak stwierdził autor owego dwujęzycznego programu: "Tak mi się napisało").

W tekście identyfikatory i komentarze będziemy pisać w języku polskim, ale bez polskich znaków narodowych. Nie jest to bynajmniej sugestia stylistyczna, a jedynie konwencja przyjęta dla zwiększenia czytelności tekstu.

Komentarze

Ponieważ komentarze nie wpływają bezpośrednio na generowany kod wynikowy, traktuje się je często jako mało istotny, czy nawet zbędny element programu. Argumentuje się też - i jest w tym dużo prawdy - że dobrze napisany kod sam się komentuje poprzez nazwy funkcji, zmiennych, typów (patrz niżej) i zastosowane konstrukcje języka C. Ale jest to prawda częściowa: samokomentowanie może być - ale też nie musi - wystarczające dla autora programu albo kogoś, kto zdążył już program dobrze poznać. Co więcej: przestaje być wystarczające w przypadku dużych wielomodułowych programów.

Pewne zalecenia dotyczące komentowania są oczywiste, np. że komentarz nie powinien powtarzać rzeczy opisanych w kodzie, być w miarę krótki i jasny. Inne sprawy bywają przedmiotem dyskusji, np. jak komentować operacje wewnątrz funkcji? Czy stosować tylko komentarze w linii, czy też komentować całe grupy zdań? Czy kilku liniowy komentarz, opisujący następującą po nim sekwencję operacji, ma być ograniczony linią z góry, czy z dołu?, itd.

Podobnie jest ze zmiennymi. Panuje zgoda, że struktury i unie należy komentować (do czego służą, co zawierają). Ale co do komentowania poszczególnych elementów tych struktur i zmiennych skalarnych zdania bywają rozbieżne. Czy wystarcza sama nazwa zmiennej, jeśli będzie odpowiednio informująca? Jak komentować inicjalizację zmiennych?

Z braku miejsca nie będziemy tych wariantów oraz ich zalet i wad opisywać. Wspomnimy o kilku najważniejszych sprawach.

Przede wszystkim cały moduł i wszystkie znajdujące się w nim funkcje powinny mieć nagłówki komentarzowe (moduł rozumiemy tu w wąskim sensie, jako plik translowany jako całość). Jeśli ich w module nie ma, czytelnik może stracić dużo czasu na dociekanie, jakiego typu operacje są realizowane w module, jakie funkcje publiczne moduł zawiera itd.

W skład komentarzowego nagłówka modułu powinna wchodzić informacja identyfikująca moduł, w tym nazwa projektu, opis funkcji, jakie moduł zawiera, które z nich są publiczne, do jakich zmiennych globalnych zewnętrznych funkcje się odwołują i które z nich modyfikują, kto i kiedy moduł utworzył i kiedy moduł był ostatni raz modyfikowany (będzie jeszcze o tym mowa).

Komentarzowy nagłówek funkcji powinien zawierać następujące informacje:

  • nazwę i cel funkcji,
  • listę argumentów wejściowych oraz zmiennych globalnych zewnętrznych, z których funkcja korzysta,
  • zwracaną wartość i listę modyfikowanych zmiennych globalnych zewnętrznych.

Działanie funkcji może być również opisane w jej nagłówku w tzw. pseudokodzie, czyli krótkim zapisie algorytmu w języku naturalnym (jest to dość popularna metoda).

Komentarze w programie - dotyczy to przede wszystkim nagłówków plików i funkcji - mogą być związane z pewnymi zasadami dokumentowania tworzonego oprogramowania. Relacja może tu być dwustronna: w nagłówkach modułów mogą być pewne elementy dokumentacji i odwrotnie - część dokumentacji technicznej projektu może być tworzona np. z nagłówków komentarzowych modułów i poszczególnych funkcji, np. publicznych. W jednej z warszawskich firm widziałem bardzo dobre rozwiązanie: program pomocniczy, przeglądając pliki źródłowe poszczególnych modułów, tworzył dokumentację techniczną projektu, zawierającą same nagłówki modułów, odpowiednio ułożone.

Wskazane jest, by komentarze w linii były tabulowane, tj. rozpoczynały się zawsze od pewnej określonej kolumny. Nie należy komentarzy zagnieżdżać, ponieważ program staje się nieprzenośny. Jedne kompilatory, jak np. BORLAND C 3.1, dopuszczają zagnieżdżanie komentarzy (deklaruje się to za pomocą opcji w IDE, czyli w warsztacie programisty pakietu BORLAND C 3,1), inne, jak MICROSOFT 6.0 i 7.0 - nie. Wreszcie me jest wskazane używać komentarzy do blokowania pewnych fragmentów programu. Jeśli chce się jakąś funkcję pominąć w kompilacji, nie usuwając jej z pliku źródłowego, często ujmuje się ją całą w komentarze. Jeśli w ciele funkcji znajdują się jej własne komentarze, sygnalizowany jest błąd, przynajmniej przez te kompilatory, które nie dopuszczają zagnieżdżania komentarzy. Lepszym wyjściem jest ujęcie blokowanego fragmentu w dyrektywy kompilacji warunkowej, np. #ifdef, z podaniem jako argumentu identyfikatora niezdefiniowanego:

#ifdef XXXXXX
Tu blokowany fragment programu 
#endif

Uwaga: użyliśmy tu terminów "publiczna" (nieco ściślej można powiedzieć "globalna publiczna") dla określenia funkcji lub zmiennej w module, z której mogą korzystać funkcje w innych modułach, oraz "globalna zewnętrzna" dla określenia funkcji lub zmiennej zdefiniowanej w innym module. Zmienne zewnętrzne lub funkcje zdefiniowane z klasą pamięci static będziemy określać mianem "prywatnych". Nazwę "lokalna" zarezerwujemy dla zmiennych zdefiniowanych w obrębie funkcji.

Identyfikatory

Podobnie jak to ma miejsce z komentarzami, istnieją różne szkoły budowania identyfikatorów, tworzenia skrótów itd. Nie będziemy ich tu szczegółowo omawiać. Podamy kilka podstawowych zasad, które są na ogół akceptowane.

  • Nazwy zmiennych i funkcji powinny odzwierciedlać ich sens. Powinny również różnić się w sposób wyraźnie zauważalny dla czytelnika (np. używanie jednocześnie identyfikatorów TablicaLinii1 i TablicaLinii2 może być mylące).
  • Jeśli program ma być przenośny, identyfikatory powinny różnić się znacząco - tj. nie poprzez różne wielkości tego samego znaku - na pierwszych 8 znakach (tyle wynosi maksymalna długość identyfikatora w wersji K&R języka C). Istnieją też pewne utarte zwyczaje nazewnicze. I tak litera c używana jest zazwyczaj do deklarowania zmiennych znakowych; litery i, j, k - do indeksów pętli i tablic; litery p, q - do wskaźników; s, t - do ciągów; x, y, z - do współrzędnych. Dobrze jest przestrzegać tych zwyczajów, a już na pewno niewskazane jest deklarować zmienne o tych nazwach i o zupełnie odmiennych typach, np.
    char i; long c;
  • Nazwy nie powinny brzmieć podobnie (np. Index i Indeks). Program powinien móc przejść poprawnie tzw. test telefoniczny Plaugera i Kernighana, tj. odczytany przez telefon, powinien zostać odebrany poprawnie.
  • Nazwy zmiennych i funkcji należy pisać małymi literami.

    Dobrym wyjściem jest pisać pierwsze litery słów wielkie, np. SzukajZnaku (pozwoli to odróżnić funkcje własne od funkcji bibliotecznych kompilatora). Wyjątkami mogą być identyfikatory jednoznakowe, które można kodować małymi literami. Przy czym - i jest to zalecenie naturalne - funkcje powinny mieć nazwy czasownikowe (np. SzukajZnaku), zaś zmienne - rzeczownikowe, np. Tablica, Licznik itp.

  • Nazwy stałych, dla odróżnienia od nazw zmiennych i funkcji, powinny być kodowane wielkimi literami.

    Po to, by wydzielić słowa w nazwie wielosłowowej kodowanej wielkimi literami, można te słowa rozdzielać podkreśleniami (patrz symboliczne definicje atrybutów plików niżej). Podobnie rzecz się ma z typami definiowanymi za pomocą dyrektywy typedef oraz z makroinstrukcjami - kodowanie ich wielkimi literami pozwala odróżnić je od funkcji (jest to zresztą zasada szeroko akceptowana). Notabene makroinstrukcje w książce K&R są definiowane małymi literami i jest to jeden z rzadkich przypadków, gdy sugestia podana w tej książce okazała się nieaktualna.

  • Identyfikatory nie powinny rozpoczynać się ani kończyć podkreśleniem (identyfikatory takie bywają rezerwowane dla kompilatorów).

    Przy dużej liczbie identyfikatorów o podobnym charakterze można stosować przedrostki lub przyrostki semantyczne, określające znaczenie identyfikatora. Oto zdefiniowane atrybuty plików:

    #define PLIK_DO_CZYTANIA    1
    #define PLlK_UKRYTY         2
    #define PLIK_SYSTEMOWY      4

    Niezależnie od przedrostków i przyrostków semantycznych można stosować przedrostki określające charakter identyfikatora (np. xStart i yStart - współrzędne początku, okna) lub jego typ. Niezłym rozwiązaniem jest stosowanie wspomnianej konwencji węgierskiej opracowanej przez programistę Charlesa Sionoyi z firmy MICROSOFT, mającej na celu uczynienie programu bardziej czytelnym. Polega ona na dodaniu do nazwy przedrostka, opisującego typ identyfikatora (litery określające typ są początkami odpowiednich słów angielskich). Na przykład - użyto tu konsekwentnie nazw angielskich - pcFirstLetter oznacza identyfikator o nazwie głównej FirstLetter, będący wskaźnikiem (p - pointer) na literę (c - char). Podobnie pszPathName oznacza identyfikator PathName, który jest wskaźnikiem (p) na ciąg znaków (s - string) zakończony zerem (z - zero).

  • Elementy struktur opisujące podobne obiekty (np. godzina, minuta, dzień, rok) i znajdujące się w różnych definicjach struktur powinny różnić się, np. poprzez przedrostki składające się z pierwszych liter etykietek struktur lub nazw typu, jakie nadano tym definicjom za pomocą dyrektywy typedef.

    W przeciwnym razie, jeśli elementy dwóch różnie zdefiniowanych struktur będą miały te same nazwy (np. Godz), może to być w tekście programu trudne do rozróżnienia. Lepiej zatem element określający godzinę w strukturze opisującej pracownika nazwać np. PracGodz, a w strukturze opisującej czas trwania zmiany - ZmianaGodz:

    typedet struct          typedef struct
    {                       {
      int PracGodz;           int ZmianaGodz;
    
    } PRACOWNIK;            } ZMIANA;
  • Te same nazwy powinny mieć rozłączne zasięgi.

    Nie jest wskazane deklarować w funkcji nazwę lokalną, brzmiącą identycznie jak nazwa zdefiniowana zewnętrznie. Wprawdzie większość kursów i podręczników programowania w języku C zaczyna się od opisywania zasięgu identyfikatorów, co wspiera się przykładami, w których zmienna lokalna przesłania zmienną zdefiniowaną zewnętrznie o tej samej nazwie. Ale niestety, nie zawsze podaje się, że deklarowanie takich samych identyfikatorów o nierozłącznych zasięgach jest praktyką złą.

    Zalecenie to można sformułować jeszcze inaczej: zasięg zmiennej i jej widoczność powinny być identyczne,

  • Długość nazwy powinna być związana z jej zasięgiem.

    Im większy zasięg zmiennej, tym jej nazwa dłuższa. A już na pewno nazwy zmiennych lokalnych nie powinny być dłuższe od nazw zdefiniowanych zewnętrznie.

Definicje stałych i typów

Jeśli w programie stosuje się stałe wartości liczbowe, należy definiować je w postaci symbolicznej. Zdanie:

if (Licznik < 64)

nie mówi praktycznie nic o sensie testu. Zupełnie inaczej to wygląda, gdy zdefiniujemy stałą symboliczną i posłużymy się nią w teście

#define LICZBA_LINII 64
   ...
if (Licznik < LICZBA_LINII)

Nie chodzi tu tylko o czytelność programu, ale także o łatwość jego modyfikowania. Jeśli w programie odwołania do zdefiniowanej wartości stałej znajdują się w kilku miejscach, przy jej zmianie wystarczy zmienić jej definicję - kompilator wstawi do każdego odwołania nową wartość stałej.

A skoro już mowa o stałych, to - jeśli to jest możliwe - należy używać ich w wyrażeniach bez stałych addytywnych (dotyczy to np. liczników pętli). Jeśli liczba elementów w tablicy wynosi MAX, to pętlę, przebiegającą cyklicznie wszystkie elementy tablicy, można zdefiniować np. dwojako:

for (k=0; k < MAX; k++)      for (k=0; k<= MAX-1; k++)
{                            {

W obydwu pętlach wartość indeksu k zmienia się od 0 do MAX-1. Ale rozwiązanie z lewej strony jest bardziej klarowne i naturalne. Jest to również rozwiązanie nieco lepsze od wersji:

for(k=0; k!=MAX; k++)
{

ponieważ zabezpiecza przed nieskończoną (a przynajmniej znacznie dłuższą niż być powinna) pętlą, która realizowałaby się w wypadku, gdyby wewnątrz ciała pętli wartość zmiennej k została przypadkowo powiększona do wartości przekraczającej MAX (wyrażenie k != MAX byłoby cały czas prawdziwe).

Równie przydatne jest definiowanie typów zmiennych. Dla przykładu, długość linii można definiować za pomocą zmiennej typu int. Ale jeśli okaże się, że zakres jej zmienności przekracza maksymalną wartość zmiennej typu int, trzeba zmienić jej typ, np. na unsigned int. Jeśli liczniki długości linii występują w programie w wielu miejscach, zmiana ich typu może okazać się bardzo kłopotliwa. Lepiej zatem zdefiniować typ:

typedef int LICZNIK;

i używać deklaracji liczników:

LICZNIK LiczCyfr, LiczLiter;

Zmiana w programie typu zmiennych zawierających liczniki sprowadzi się do modyfikacji definicji typu LICZNIK. Jednocześnie przy każdej deklaracji zmiennej typu LICZNIK będzie wiadomo, jaki jest charakter zmiennej (pomijamy sprawę bardziej złożonych wyrażeń, w których zmiana definicji typu LICZNIK może wymagać zastosowania rzutów). Przy definiowaniu tablic lepiej jest pozostawić kompilatorowi określanie ich rozmiaru, a jeśli w programie istnieje potrzeba odwoływania się do tego rozmiaru, definiować go symbolicznie, np.:

int Tab[]={1,4,6,3,7,8,5,8,9,0,4};
#define ROZMIAR_TAB sizeof Tab/sizeof Tab[0]

Dodanie lub usunięcie elementu z tablicy Tab spowoduje odpowiednią zmianę wartości stałej ROZMIAR_TAB. Przy czym w definicji tej zamiast sizeof Tab[0] można by było napisać również sizeof int, z identycznym skutkiem:

#define ROZMIAR_TAB sizeof Tab/sizeof int

Ta druga wersja jest jednak nieco gorsza. Jeśli zdecydujemy się zmienić typ tablicy, np. na long, trzeba będzie zmieniać także definicję stałej ROZMIAR_TAB.

Do definiowania typów nie należy używać dyrektywy #define. Owszem - formalnie rzecz biorąc - często można stosować zamiennie dyrektywy #define i typedef. Oto dwie wersje definicji typu LICZNIK i przykład korzystania z nich:

#define LICZNIK int            typedef int LICZNIK;
...                             ...
LICZNIK k, *m;                 LICZNIK k, *m;

Jakkolwiek wynik obydwu definicji jest w tym przypadku poprawny, jest między nimi istotna różnica. Dyrektywa typedef definiuje typ, podczas gdy #define powoduje jedynie zastąpienie w programie jednego tekstu drugim. Stosowanie dyrektywy #define do definiowania typów może też spowodować kłopoty.

Na przykład, jeśli spróbujemy zdefiniować wskaźnik na liczbę całkowitą za pomocą dyrektywy #define:

#define INT_PTR int *

to w deklaracji

INT_PTR p1,p2;

tylko p1 będzie wskaźnikiem na liczbę całkowitą (p2 będzie liczbą całkowitą). Sytuacji takiej nie będzie, jeśli INT_PTR będzie zdefiniowane za pomocą dyrektywy typedef.

Za pomocą dyrektywy #define można wprawdzie definiować także bardziej złożone typy danych, i to w zadziwiająco dużym zakresie. Ale możliwości korzystania z takich definicji są zbyt ograniczone. Definiowanie zaś jednych typów za pomocą #define, a innych za pomocą typedef prowadzi do niespójności i bałaganu w programie. Typ będący strukturą lepiej jest definiować za pomocą dyrektywy typedef, niż posługiwać się etykietką struktury (tag). Oto dwa przykłady:

typedef struct               struct prod
{                            {
  int numer;                   int numer;
  char nazwa[20];              char nazwa[20];
}     PRODUKT;               };
PRODUKT x1;                  struct prod x1;

Pierwszy zapis jest prostszy i bardziej naturalny (chociaż zdania tu bywają podzielone). Etykietki struktury muszą być natomiast używane w odwołaniach rekurencyjnych do struktur.

Problem, co i do jakiego stopnia definiować symbolicznie w programie, jest często kwestią dyskusyjną. Na przykład, jeśli w programie są funkcje zwracające tylko wartości 0 lub 1, często nadaje się tym wartościom nazwy symboliczne TRUE i FALSE, a dla samych funkcji definiuje typ BOOL. Niektórzy programiści uznają to jednak - niesłusznie zresztą - za przesadę, zwiększającą niepotrzebnie ilość kodu źródłowego i czas jego pisania, a nawet zaciemniającą program(i). Niemniej jednak zdarzają się też programy wyraźnie "przedefiniowane". Oto dość osobliwy przykład:

#define WART_POCZ 0
#define ZMIANA 1
#define WART_LIMIT 100
...
for (k = WART_POCZ; k < WART_LIMIT; k += ZMIANA)
...

Jest to oczywiście praktyka przesadna, w której prosta pętla urasta do skomplikowanej i mało czytelnej konstrukcji.

Wreszcie uwaga ostatnia: nie należy przedefiniowywać słów lub znaków specjalnych języka C. Bywa, że po to, by program w języku C uczynić bardziej przyswajalnym (dotyczy to najczęściej ludzi, którzy dotąd programowali w innych językach), redefiniuje się słowa kluczowe czy operatory języka, np.:

#define begin {
#define end }

i koduje program

if(...)
begin
  ...
end

Jest to praktyka zła, albowiem program przestaje być czytelny jako program w języku C. Można natomiast - i często tak się robi, zwłaszcza gdy korzysta się z własnych słów kluczowych kompilatora - definiować własne słowa opisujące pewne atrybuty identyfikatorów, np.:

#define PRYWATNA static 
#define UZYTKOWA pascal near

Druga część artykułu.