0 - Politechnika Krakowska
Transkrypt
0 - Politechnika Krakowska
POLITECHNIKA KRAKOWSKA - WIEiK – KATEDRA AUTOMATYKI i TECHNIK INFORMACYJNYCH Metody Programowania www.pk.edu.pl/~zk/MP_HP.html Wykładowca: dr inż. Zbigniew Kokosiński [email protected] Wykład 13: Styl programowania • Styl programowania a jakość programu • Nazwy • Wyrażenia i instrukcje • Jednolitość stylu, idiomy • Makroinstrukcje zamiast funkcji? • Liczby „magiczne” • Komentarze • Rola dobrego stylu • Zasady dobrego stylu Styl programowania a jakość programu Jakość programu jako wytworu programisty nie sprowadza się wyłącznie do jego poprawnego działania. Na jakość programu wpływa w znaczący sposób styl programowania, stosowane algorytmy i struktury danych, interfejsy, staranność uruchomienia i testowania, wydajność (parametry czasowe i pamięciowe), przenośność itp. Niewłaściwy styl programowania może być przyczyną trudności w : • logicznej konstrukcji i pisaniu kodu programu • uruchomieniu programu • szybkim wyszukaniu ewentualnych błędów • łatwej rozbudowie programu • pracy zespołowej • zrozumieniu budowy i funkcji programu po pewnym czasie albo przez innego programistę. Styl programowania a jakość programu Na niewłaściwy styl programowania składają się wielorakie błędy programisty, występujące oddzielnie bądź łącznie : • „nielogiczność” programu • brak prostoty (programy powinny być krótkie i łatwe do zarządzania) • brak przejrzystości kodu (zrozumiałość przez człowieka i maszynę!) • brak ogólności programu (ogranicza uniwersalność zastosowań) • nieprzestrzeganie przyjętych konwencji i notacji mylone często z oryginalnością • stosowanie kilku różnych konwencji jednocześnie • myślenie o sobie jako o jedynym użytkowniku programu • przekonanie, że program nie będzie wymagał modyfikacji w przyszłości • niedbałość o użyteczność (reusability) napisanego kodu w przyszłości Nazwy Nazwa zmiennej lub funkcji określa obiekt i przekazuje informację o jego przeznaczeniu. Nazwa powinna zawierać jak najwięcej informacji, przy zachowaniu zwięzłości o łatwości do zapamiętania. Dodatkowe informacje powinien dostarczać kontekst użycia danej nazwy (sposób użycia i miejsce jej zdefiniowania). Im zasięg nazwy jest większy, tym więcej informacji powinna ona nieść. Globalne zmienne, funkcje, klasy i struktury powinny mieć opisowe nazwy kojarzące się z ich rolą w programie. Lokalne zmienne powinny posiadać krótkie nazwy, zgodne z powszechnie przyjętymi konwencjami. Nazwy - przykłady 1. Zmieniając rozmiar tablicy/bufora nie zmieniamy znaczącej nazwy: # define # define # define STO 100 DZIESIEC 10 DWADZIESCIA 20 # define # define # define TABLICA 100 ROZM_BUF_WE ROZM_BUF_WY 10 20 2. Opisową nazwę globalną opatrujemy dodatkowo komentarzem: int nbiezace = 0; // bieżąca długość kolejki wejściowej Inne dopuszczalne warianty nazwy: n_biezace, nBiezace itp. 3. Dla zmiennej lokalnej wybieramy krótką nazwę: numberOfPoints, npoints, n 4. Konwencje : i, j p, q s, t - liczniki pętli; - wskaźniki; - napisy; Nazwy - przykłady 5. Porównaj czytelność powyższych fragmentów kodu: for (theElementIndex = 0; theElementIndex < numberOfElements; theElementIndex++) elementArray[theElementIndex] = theElementIndex; for (i = 0; i < nelems; i++) elem[i] = i; 6. Wprowadzamy nazwy konsekwentnie, podkreślając występujące pomiędzy nazywanymi rzeczami związki i różnice: class UserQueue { int noOfItemsInQ, frontOfTheQueue, queueCapacity; public int noOfUsersInQueue() {…} } Niekonsekwencje w powyższym kodzie: różne człony nazw oznaczają to samo (kolejkę) – Q, Queue, queue , chociaż wystarcza sam kontekst: Nazwy - przykłady class UserQueue { int nitems, front, capacity; public int nusers() {…} } Nie występuje wówczas nadmiarowość w takich zapisach jak: queue.queueCapacity++; n = queue.noOfUsersInQueue(); queue.capacity++; n = queue.nusers(); 7. Stosujemy nazwy czynności dla funkcji: now = date.getTime(); putchar(’\n’); 8. Funkcje przekazujące wartości logiczne powinny mieć nazwy określające znaczenie wyniku (np. prawda , gdy argument jest w kodzie ósemkowym): if (checkoctal(c)) … if (isoctal(c)) … Nazwy - przykłady 9. Dbamy o prezycję ( unikamy sytuacji, gdy intencja nadanej nazwy jest sprzeczna z wykonaniem): #define is octal(c) ((c) >= ’0’ && (c) <= ’8’) zamiast #define is octal(c) ((c) >= ’0’ && (c) <= ’7’) Podobnie nazwa funkcji inTable() sugeruje wartość logiczną przeciwną do przekazywanej, ponieważ znaleziony obiekt może mieć indeks j z przedziału (0, nTable -1), a nie znajdując obiektu funkcja zwraca wartość nTable : public boolean inTable(Object obj) { int j = this.getIndex(obj); return (j == nTable); } public boolean inTable(Object obj) { int j = this.getIndex(obj); return (j != nTable); } Nazwy - przykłady Inny przykład błędów logicznych związanych z wyborem nazw i wartości : #define TRUE 0 #define FALSE 1 if ((ch = getchar()) == EOF) not_eof = FALSE; Warianty poprawne to: #define TRUE 1 #define FALSE 0 #define TRUE 1 #define FALSE 0 if ((c = getchar()) != EOF) not_eof = TRUE; if (c = getchar()) == EOF) eof = TRUE; Nazwy - przykłady Jeszcze inny przykład błędu logicznego (nazwa funkcji nie odpowiada jej treści) : int smaller (char *s, char *t) { if (strcmp(s, t) < 1) return 1; else return 0; } Ponieważ porównując łańcuchy s i t przez porównanie odpowiadających sobie znaków funkcja strcmp zwraca wartość <0 jeśli s<t, =0 jeśli s=t, oraz >0 jeśli s>t, to poprawna jest postać int smaller (char *s, char *t) { if (strcmp(s, t) < 0) return 1; else return 0; } Wyrażenia i instrukcje Wyrażenie jest ciągiem operatorów, operandów i znaków przestankowych określających sposób wykonywania danego obliczenia. Kluczowe znaczenie posiadają operatory, określające co, w jaki sposób i w jakiej kolejności zostanie obliczone. Operatory mogą być jedno- lub dwuargumentowe (wiązane z jednym bądź dwoma operandami). O kolejności obliczeń decyduje priorytet operatorów i sposób ich wiązania z operandami (np. dla operatorów równorzędnych wiązanie od lewej do prawej lub od prawej do lewej) oraz użyte nawiasy. Dla różnych operandów wykonywane są niejawne konwersje typów z zachowaniem hierarchii typów. Instrukcje sterują przebiegiem programu i dzielą się na proste, warunkowe i iteracyjne. Wyrażenia i instrukcje - przykłady 1. Jak pokazano w poprzednich przykładach czytelne pokazanie struktury programu (np. instrukcji iteracyjnych czy warunkowych) wymaga stosowania wcięć. 2. Wyrażenia warunkowe piszemy w naturalnej postaci, negacje chętnie eliminujemy zmieniając operatory relacji na przeciwne : if (!(block_id < actblks) || !(block_id >= unblocks) if ((block_id >= actblks) || (block_id < unblocks) 3. Stosujemy nawiasy aby ułatwić zrozumienie kolejności operacji (pomimo istnienia priorytetów dla operatorów relacji, logicznych, bitowych. Błędny zapis if (x&MASK == BITS)... oznacza chociaż chodziło o if (x & (MASK == BITS))... if ((x&MASK) == BITS) ... Tam, gdzie nawiasy nie są bezwzględnie potrzebne często poprawiają czytelność (rok przestępny, ang. leap-year) : leap_year = y % 4 == 0 && y % 100 != 0 || y % 400 == 0; leap_year = ((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0); Wyrażenia i instrukcje - przykłady 4. Tam gdzie występują operatory przypisania stosowanie nawiasów jest obowiązkowe: while ((c = getchar()) != EOF) ... 5. Dzielimy wyrażenia złożone, jak np. *x += (*xp=(2*k < (n-m) ? c[k+1] : d[k--])); dzieląc je na logiczne części i pokazując strukturę: if (2*k < n-m) *xp = c[k+1]; else *xp = d[k--]; *x += *xp; Wyrażenia i instrukcje - przykłady 6. Unikamy zawiłości kodu. Lapidarny zapis z użyciem operatora warunkowego ? : child=(!LC&&!RC)?0:(!LC?RC:LC); zastępujemy wprawdzie dłuższym, ale klarowniejszym kodem : if (LC == 0 && RC = 0) child = 0; else if (LC == 0) child = RC; else child = LC; Operator warunkowy ? : możemy zastosować do zapisu krótkich wyrażeń, takich jak: max = (a > b) ? a : b; lub printf(”Ojciec ma %d syn%s\n”, n, n==1 ? ”a” : ”ów”); Wyrażenia i instrukcje - przykłady Zmienna subkey jest przesuwana w prawo o liczbę pozycji określoną przez trzy najmniej znaczące bity zmiennej bitoff . Niejasne wyrażenie subkey = subkey >> (bitoff – ((bitoff >> 3) << 3)); zastępujemy przez : lub subkey = subkey >> (bitoff & 0x7); subkey >>= bitoff & 0x7; 7. Uważamy na efekty uboczne. Operacje typu ++ czy wejścia-wyjścia pociągają za sobą wykonanie ukrytych czynności. Podstawienie w rodzaju array[i++] = i; daje wynik niejednoznaczny, bo kolejność wykonania inkrementacji zmiennej i oraz przypisania wartości elementowi tablicy nie jest zdefiniowana. Wyrażenia i instrukcje - przykłady Argumenty funkcji scan są obliczane przed jej wywołaniem. Pobranie dwóch powiązanych ze sobą wartości doprowadzi do błędu, ponieważ zmiana wartości zmiennej year, nie pociąga za sobą aktualizacji argumentu zmiennej profit[year] scan(”%d %d”, &year, &profit[year]); Poprawne jest rozbicie tej instrukcji na sekwencję następujących dwóch: scan(”%d”, &year); scan(”%d”, &profit[year]); Jednolity styl i idiomy Na jednolitość stylu składają się następujące elementy: 1. Takie same obliczenia wykonywane są w ten sam sposób. (np. przeglądanie tablic w pętli odbywa się zawsze w określonym porządku). 2. Rozmieszczenie wcięć i nawiasów klamrowych powinno być jednolite. 3. Stosujemy idiomy przyjęte w danym języku programowania. Wykorzystujemy sprawdzone wzorce przyjęte przez ogół programistów. 4. Modyfikując obcy program zachowujemy styl, w którym został on napisany. Jednolity styl i idiomy - przykłady 1. Idiomatyczne postaci pętli for (bez i z deklaracją zmiennej indeksującej): for (i = 0; i < n; i++) array[i] = 0; for (int i = 0; i < n; i++) array[i] = 0; 2. Standardowe pętle nieskończone: for (;;) ... while (1) ... 3. Typowa operacja przypisania wewnątrz pętli : while ((c = getchar()) != EOF) putchar(c); 4. Pętla do przeglądania elementów listy : for (p = list; p != NULL; p = p->next) ... Jednolity styl i idiomy - przykłady 5. Idiomy do przydzielania miejsca w pamięci dla napisów (dla C i dla C++) : p = malloc(strlen(buf)+1); strcpy(p, buf); p = new char[strlen(buf)+1]; strcpy(p, buf); Funkcja strlen pomija znak ’\0’ kończący napis dlatego rezerwując pamięć dodajemy 1. Popularna funkcja biblioteczna strdup, wykonuje bezpiecznie kopię napisu, ale nie należy ona do standardu ANSI C. 6. Idiom konstrukcji wielokrotnego wyboru (wykonywana jest instrukcja po pierwszym spełnionym warunku) : if (condition_1) instruction_1 else if (condition_2) instruction_2 ... else if (condition_k) instruction_k else default_instruction Makroinstrukcje w roli funkcji Makroinstrukcje w C były przeznaczone do wykonywania krótkich, powtarzających się często obliczeń. Makroinstrukcje działają jak podstawienia tekstowe : argumenty formalne definicji makroinstrukcji są zastępowane przez argumenty wywołania, po czym pełny tekst makroinstrukcji jest umieszczany w programie zamiast jej wywołania. Przykład: #define square(x) (x) * (x) 1/square(x) // równoważne 1/(x)*(x) #define square(x) ((x) * (x)) 1/square(x) // równoważne 1/((x)*(x)) Generalnie NIE STOSUJEMY już makroinstrukcji w roli funkcji. Liczby „magiczne” Liczby „magiczne” to stałe, rozmiary tablic, pozycje znaków, współczynniki przekształceń oraz inne wartości literałów numerycznych występujących w programie. Liczbom „magicznym” nadajemy nazwy. Stałe całkowite w C i C++ można definiować za pomocą instrukcji enum. Stałe dowolnych typów można w C++ deklarować przy pomocy modyfikatora const . Powszechnie stosowane definiowanie stałych instrukcją #define nie jest zalecane, bo to makroinstrukcja ! Liczby „magiczne” - przykłady 1. Zastosowania liczb magicznych do obliczenia rozmiaru obszaru w tablicy : enum { MINROW = 11, // dolna MAXROW = 20, // górna MINCOL = 6, // dolna MAXCOL = 25, // górna }; ... rowrange = MAXROW – MINROW + 1; colrange = MAXCOL – MINCOL + 1; n_cells = rowrange * colrange; granica granica granica granica 2. Zastosowanie definicji z modyfikatorem const : const int MAXROW = 20, MAXCOL = 25; zakresu zakresu zakresu zakresu wierszy wierszy kolumn kolumn Liczby „magiczne” - przykłady 3. Do obliczania rozmiarów obiektów używaj operatorów języka sizeof() : char buf[1024]; fgets(buf, sizeof(buf), stdin); 4. Makroinstrukcja obliczająca rozmiar tablicy array, jeżeli jej nazwa jest widoczna: #define NELEMS(array) (sizeof(array) / sizeof(array[0])) ... double dbuf[100] for (i = 0; i < NELEMS(dbuf); i++) ... Komentarze Komentarze powinny ułatwić zrozumienie programu. Komentarze nie powinny powtarzać dosłownie informacji zawartych w kodzie, mogą omówić program w sposób bardziej ogólny, a bardziej szczegółowo jedynie trudniejsze miejsca kodu. Bezwartościowe komentarze utrudniają zrozumienie programu. Komentujemy funkcje i dane globalne, definicje stałych, pola w strukturach i klasach. Podajemy informacje o zastosowanym algorytmie, czy jego wariancie, ewentualnie źródle skąd pochodzi i opisujemy wprowadzone modyfikacje. Podajemy też zakres danych, dla którego działa program, datę ostatniej aktualizacji , informację o autorze itp. Komentarze - przykłady 1. Komentarze zamieszczamy w kodzie pomiędzy znakami /* a */ . /* funkcja obliczająca pole bryły */ 2. Komentarze zamieszczamy w wierszu po znaku // . #define PI 3.14 // stała „pi” Rola dobrego stylu Dobry styl ułatwia zrozumienie programu. Dobry styl zapobiega wielu typowym błędom. Dobry styl gwarantuje jakość i zwięzłość produktu jakim jest program komputerowy. Dobry styl przyspiesza programowanie, ponieważ pozwala na „zautomatyzowanie” wielu czynności bez obawy, że popełnimy błąd. Dobry styl zapewnia, że wszystkie nasze programy będą standardowe, a więc łatwe do przeanalizowania i modyfikacji nawet po latach. Dobry styl jest wizytówką programisty tak jak charakter pisma jest wizytówką człowieka. Dobry styl powinien być nawykiem, odruchem, który należy wyćwiczyć aby móc się skupić na twórczej stronie programowania. Zasady dobrego stylu 1 Używaj nazw opisowych dla definicji globalnych, a krótkich nazw dla definicji lokalnych. Programuj w sposób jednolity. Stosuj nazwy czynności w nazwach funkcji. Dbaj o precyzję. Stosuj wcięcia by ujawnić strukturę programu. Używaj naturalnej postaci wyrażeń. Używaj nawiasów, jeżeli pozwoli to uniknąć niejasności. Dziel wyrażenia złożone. Wcięcia i nawiasy klamrowe rozmieszczaj w sposób ujednolicony. Zasady dobrego stylu 2 Używaj idiomów właściwych danemu językowi. Programując konstrukcje wielokrotnego wyboru stosuj instrukcję else-if. Unikaj stosowania makroinstrukcji w roli funkcji. Treść i argumenty makroinstrukcji zapisuj w nawiasach. Nadawaj nazwy „liczbom magicznym.” Definiuj liczby jako stałe. Stosuj stałe znakowe zamiast liczb całkowitych. Do obliczania rozmiarów obiektów używaj operatorów języka. Zasady dobrego stylu 3 Pisz kod jasno, w miarę potrzeby objaśniaj go i unikając zbędnych komplikacji. Zawsze komentuj funkcje i dane globalne. W komentarzach unikaj pisania rzeczy trywialnych. Popraw zły kod zamiast go komentować. Dbaj, aby komentarz był zgodny z kodem. Literatura Kernighan B.W., Pike R. : Lekcja programowania, WN-T, Warszawa 2002 Kernighan B., Ritchie D. : Język C, WN-T, Warszawa 1988 Stroustrup B. : Język C++, WN-T, Warszawa 1997 Zalewski A. : Programowanie w językach C i C++ z wykorzystaniem pakietu Borland C++, Wydawnictwo Nakom, Poznań 1998