WYSOKIE C MAREK KOTOWSKI.txt

(73 KB) Pobierz
Marek Kotowski - Wysokie C
czyli
Wskazówki dla programujšcych w C

Artykuł oryginalnie opublikowany w nie wychodzšcym już czasopismie PCKurier - link do artykułu.
Wskazówki dla programujšcych w C

Program jest zapisem myli ludzkiej, i tak jak myl 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 okrela 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, jeli 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 dowiadczenia. Nie ma zbyt wielkiej przesady w stwierdzeniu, że stylów kodowania jest tyle, ilu jest programistów.

Okrelenie 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(){
....
}

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

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

Innym przykładem, będšcym doprowadzeniem do krańcowoci 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 oczywicie 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ównoczenie kilka dużych tablic, co oczywicie 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 wymylić 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ę bezporednio 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ę programicie, 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 jakoci programu jeszcze kilkanacie 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 pojemnoci pamięci i szybkoci działania komputerów, kryteria efektywnoci 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 programici. Dlatego też dzisiaj ważniejsze sš inne sprawy: czytelnoć programu, łatwoć jego modyfikowania i rozwijania.

Oczywicie nie znaczy to, że efektywnoć programu przestała być ważna. Przeciwnie - i nie ma tu sprzecznoci. Po pierwsze: gdy nie ma innych kryteriów (patrz niżej), o wyborze takiej czy innej konstrukcji językowej powinny decydować względy efektywnoci. Po drugie, i ważniejsze: właciwy styl kodowania w istocie zwiększa efektywnoć programu (patrz przykład super-funkcji wyżej), nawet jeli 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 (okrelani 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 czytelnoci programu było już sformułowanych przez K&R w ich ksišżce.

Czytelnoć programu nie jest oczywicie jedynym kryterium poprawnoci 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ć przenony. Przenonoć programu jakkolwiek zwišzana z czytelnociš 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...
Zgłoś jeśli naruszono regulamin