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