Kódolási stílus – javaslatok
Czirkos Zoltán · 2022.05.25.
Megjegyzések és javaslatok a szépen írt, áttekinthető programokhoz.
Érdemes szem előtt tartani a következő dolgokat.
Olvashatóság – alapok
- Írjunk jól olvasható kódot. Ennek a legegyszerűbb módja azt írni, amit gondolunk. Ne trükközzünk a nyelvi elemekkel a szép, egyszerű programok írása helyett!
- Ne optimalizáljunk feleslegesen. Csak akkor, ha kiderül, hogy lassú a programunk.
- Különösen ne végezzünk olyan mikrooptimalizációkat kézzel, amelyekre
a fordító is képes.
%2
helyett&1
rossz ötlet,==0
helyett!
rossz ötlet. - Indentáljuk a kódot. Ez segít átlátni a program szerkezetét!
- Törekedjünk arra, hogy a programkódunk (a feladat megoldása) minél
jobban hasonlítson a feladat szövegére. Ne vigyünk bele a megoldásba
felesleges csavarokat! Pl. ha 1-től 100-ig ki
kell írni a számokat, a szép megoldás ez:
A működő, de nem annyira szerencsés megoldások pedig:for (i = 1; i <= 100; ++i) printf("%d\n", i);
Vagy pl. „8 osztója van”:for (i = 1; i < 101; ++i) printf("%d\n", i); // hol a „101” a feladat szövegében? for (i = 0; i < 100; ++i) printf("%d\n", i+1); // hol a „+1” a feladat szövegében?
Nem annyira szerencsés:if (osztok_szama(x) == 8)
if (osztok_szama(x) - 8 == 0) // miért?!
- Használjunk néven nevezett konstansokat és felsorolt típusokat mágikus
számok helyett! Nem szerencsés:
if (evszak == 2)
. Jobb ennél:if (evszak == nyar)
. - Használjunk beszédes változóneveket, és használjunk függvényeket a részfeladatokhoz!
Például, mit csinál a következő program?
int main(void) { int a, b, c, d; d = 2; a = 0; while (a < 20) { b = 1; for (c = 2; b && c <= d/2; c++) if (d % c == 0) b = 0; if (b) { printf("%d ", d); a++; } d++; } return 0; }
Megoldás
Ugyanazt, mint ez:
bool prim_e(int szam) { for (int oszto = 2; oszto <= szam/2; oszto++) if (szam % oszto == 0) return false; return true; } int main(void) { int vizsgalt = 2; int db = 0; while (db < 20) { if (prim_e(vizsgalt)) { printf("%d ", vizsgalt); db++; } vizsgalt++; } return 0; }
- Érdemes a változókat ott definiálni, ahol használjuk először. A 80-as évek C nyelvében még szükség volt a változók
kigyűjtésére a kód tetejére, manapság ez már idejétmúlt dolog. Ósdi stílus:
Modernebb szemlélet:int main(void) { int n, sz, i; scanf("%d", &n); sz = 1; for (i = 1; i <= n; i += 1) sz *= i; printf("%d! = %d\n", n, sz); return 0; }
int main(void) { int n; scanf("%d", &n); int sz = 1; for (int i = 1; i <= n; i += 1) sz *= i; printf("%d! = %d\n", n, sz); return 0; }
Karbantarthatóság – a program struktúrája
- Soha, de soha ne copy-pasteljünk kódot! A copy-pastelt kód helyett használjunk ciklust, tömböt vagy függvényt a feladattól függően.
- Függvény – hacsak nem ez a feladata – nem csinál I/O-t.
Azért lett függvény, hogy paraméterekben kapjon és visszatérési
értékben/paraméterben adjon vissza dolgokat. Az I/O a hívó
és a felhasználói felület dolga. Az esetleges diagnosztikai kiíratások
persze lehetnek kivételek, de az is inkább egy naplózó keretrendszer
használatával (vagy pl.
#ifdef DEBUG
-okkal védve) javasolt. - Függvény megírásánál mindig, külön kérés nélkül feltételeznünk kell
azt, hogy többször is meg fogják hívni azt. Így a függvénynek nem lehet
felesleges mellékhatása. Például egy kártyapaklit keverő függvénynek
valószínűleg nem dolga
srand(time(NULL))
-t hívni. - Szintén függvények: ne éljünk olyan előfeltételezésekkel, amelyekről a feladat nem ír, vagy amelyek a hívótól nem várhatóak el. Például ha a függvényünk dolga az, hogy megszámlálja egy szöveg magánhangzóit, és azok darabszámait betegye egy tömbbe, akkor a megszámlálás előtt igenis a függvény dolga a tömb elemeit nullázni.
- Függvény ne hívjon kilépéssel, hasonlókkal kapcsolatos dolgokat, mert a
felsőbb szintű kódot ez meglepetéssel fogja érinteni (fájlok lezárása,
takarítás elmarad). Ha szükséges, akkor legyen a programnak egy
fatal_error()
függvénye, azt hívjuk. - Haladóknak: mivel a C-ben nincsenek kivételek (exception), ezért sokszor bonyolult a hibakezelés. Más lehetőségünk nem nagyon van, mint hibajelző visszatérési értékeke használata. De ekkor is különítsük el a valós paramétereket a hibajelzésre használatos flag-ektől, ha lehet.
- Alakítsunk ki értelmes adatszerkezeteket. Fogjuk össze struktúrákba
az összetartozó adatokat. Például
int szeles, magas; char **elem
tipikusan egystruct Palya
nevű típusba való. Gondoljunk arra, hogy legjobb a feladatunkban megjelenő mindenféle entitáshoz egy külön típust rendelnünk a programban. - Legyen egyértelmű specifikáció arról, hogy mik a paraméterek és kinek a felelősége ellenőrizni a bemenő paraméterek értelmességét.
Nyelvi eszközök
- Ne sűrítsünk túl sok mellékhatást egy kifejezésbe, és ne tegyünk mellékhatással járó kifejezést váratlan helyre.
- Ne használjuk a rövidzár tulajdonságokkal rendelkező operátorokat vezérlési szerkezetek helyett.
Helyes:
Helytelen:if (x < 0) printf("x negatív"); if (y % 2 == 0) printf("y páros");
x < 0 && printf("x negatív"); // :( y % 2 == 0 ? printf("y páros") : 0;
- Ne írjunk a feltételekhez üres igaz ágat, inkább tagadjuk a feltételt! Tehát ehelyett:
Inkább írjuk ezt:if (x < 0) ; // nem kellene ezt megfordítani? else printf("nem negatív");
if (x >= 0) printf("nem negatív");
- A
for
ciklus tipikusan a „valahonnan valahová el akarok jutni, valamilyen lépésközzel” jellegű feladatokra való, awhile
ciklus a „tekerjünk addig, amíg valami feltétel fennáll” jellegűekhez. Ezért ha valóban ilyen feladatot oldunk meg, akkor a ciklus fejlécében ezek legyenek, a ciklus törzsében meg a tennivalók. Ne rakjuk át az iterátor léptetését a ciklustörzsbe, ne rakjuk a ciklustörzset be a fejlécbe stb. - Ne használjunk goto-t, break-et, continue-t feleslegesen. Egy-két speciális esettől eltekintve szebb kódot lehet írni nélkülük.
- Lehetőleg ne legyen üres a ciklustörzs, vagy ha mégis, akkor a ciklustörzs helyére tegyünk kommentet, jelezzük benne, miért üres.
- Ne írjunk loop-switch szekvenciákat (loop-switch sequence, for-case paradigm). Így nevezzük azt,
amikor egymás után következő műveletek kerülnek feleslegesen ciklusba, hogy aztán azon belül esetszétválasztással
kelljen újra szétválasztani őket. Pl. a
+----+
karaktersorozat kiírásához, nem túl jó megoldás:
Érthetőbben ugyanaz:for (i = 0; i <= n; i += 1) { if (i == 0 || i == n) printf("+"); else printf("-"); }
Az előbbi nem fejezi ki az egymásutániságot, az utóbbin viszont ránézésre látszik, hogy előbb plusz van, utána mínuszok, végül megint egy plusz.printf("+"); for (i = 1; i < n; i += 1) printf("-"); printf("+");
- Hiába van rá mód: ne keverjük a logikai és az aritmetikai kifejezéseket!
Például: egy szám akkor osztható egy másikkal, ha az osztás
maradéka nulla. Vagyis:
- helyes:
if (szam % oszto == 0)
... osztás maradéka, egyenlő, nullával. - (stilisztikailag) helytelen:
if (!(szam % oszto))
... a maradékot tagadom?! Kérdés: igaz-e az a kijelentés, hogy „Öt.”? Ez így nyilván nem is kijelentés...
- helyes:
- Ne használjunk logikai függvényt egész szám visszatérési értékűnek, és egész szám visszatérési
értékűt logikainak! Pl. ez így helyénvaló:
Ez már kevésbé:if (!isalpha(ch)) // ok, logikai
Ez így rendben van:if (isalpha(ch) == 0) // működik, de miééért?!
Ez viszont kevésbé érthető:if (strcmp(a, b) == 0) // 0-t ad, ha egyenlőek
if (!strcmp(a, b)) // egyenlőségre lesz igaz, de nem erre utal a tagadás
- Használjuk a
bool
típust, ahol logikai értékekkel dolgozunk! Nem szerencsés:int keres(...)
. Szerencsésebb:bool van_e(...)
ésint hol_van(...)
. Az utóbbiaknál a név és a típus is jobban kifejezi a függvényhívás eredményét. - Ne bonyolítsuk a logikai függvényeket redundáns
if (...) return true; else return false;
vezérlési szerkezettel. Helyes, rövid, egyszerű:
Nem nyújt többet, viszont sokkal hosszabb:bool paros_e(int szam) { return szam % 2 == 0; }
bool paros_e(int szam) { if (szam % 2 == 0) // „ha igaz, akkor igaz, ha hamis, akkor hamis” return true; else return false; }
- Az inicializálatlan változó veszélyes hibaforrás,
de ez nem jelenti azt, hogy minden változónak kényszeresen kezdeti értéket kell adnunk
definiáláskor. Helyes:
Működik ugyan, de nem szép stílus:double tomb[100]; double szum = 0; for (int i = 0; i < 100; ++i) szum += tomb[i];
Ha kicsit átírjuk a programot, és máshova másoljuk ezt a részt, végképp ki fog maradni az értékadás... Az erőltetett hamar-kezdeti-értékadás következménye szokott lenni az ilyen jellegű kód is:double tomb[100], szum = 0; /* * * Sok sornyi kód... * */ /* Hova lett a szum = 0? Nem kéne ilyen az * összegzés elejére? Vagy már egy meglévő * részösszeghez adjuk hozzá ezeket is? */ for (int i = 0; i < 100; ++i) szum += tomb[i];
A lenti sokkal jobb, sőt így csak egyszer kell leírni:int oszto = 2; /* Inicializált változó... de minek? */ int szam; /* Ez meg mi? */ for (int i = 2; i <= 10; i++) { printf("%d: ", i); /* Prímtényezős felbontás */ szam = i; while (szam > 1) { while (szam % oszto == 0) { printf("%d ", oszto); szam /= oszto; } oszto++; } oszto = 2; /* Első ránézésre: ez meg mit keres itt?! */ /* Hiszen már nem csinálunk vele semmit! */ /* Most végeztünk a prímtényezőkkel! Minek */ /* az osztónak újra értéket adni akkor? */ printf("\n"); }
for (int i = 2; i <= 10; i++) { printf("%d: ", i); /* Prímtényezős felbontás */ int oszto = 2; // ennek itt a helye! int szam = i; while (szam > 1) { while (szam % oszto == 0) { printf("%d ", oszto); szam /= oszto; } oszto++; } printf("\n"); }
Pointerek, tömb átadása függvénynek, dinamikus memóriakezelés
- A pointer aritmetika sok feladatnál szép és hatékony, de ha az algoritmusunk nem arra
épül, kerüljük a használatát! A tömb az legyen tömb:
- Helyes:
t[10]
, olvashatóság szempontjából helytelen:*(t+10)
- Ugyanez három dimenzióban:
t[2][5][7]
vs.*(*(*(t+2)+5)+7)
.
- Helyes:
- C-ben egy tömböt függvénynek átadva a tömb kezdőcíme adódik át,
azaz egy pointer. Ezt a pointert formálisan nem lehet megkülönböztetni
az egyetlen változóra mutató pointertől:
void intet_novel(int *i); int x = 3; intet_novel(&x);
void tombot_kiir(int *i, int meret); int tomb[10]; tombot_kiir(tomb, 10);
- Ha egy függvény pointerrel tér vissza, a dokumentációjába be kell írni, hogy az dinamikusan foglalt memóriaterület vagy nem, továbbá hogy kinek a dolga felszabadítani azt.
- A dinamikusan foglalt memóriaterületeket rendeljük hozzá gondolatban (és a dokumentációban) valamihez. Ez egyértelművé teszi, melyik programrésznél van a felszabadítás helye.
- A dinamikusan foglalt memóriát akkor foglaljuk, amikor először szükség van rá (ne előbb), és akkor szabadítsuk fel, amikor már nincs rá szükség (ne később)! Annyival is nehezebb megfeledkezni bármelyikről.
- Kövessük a
malloc()
-free()
, és azfopen()
-fclose()
logikáját! Ezt ismeri mindenki.
Fájl megnyitása, fájlművelet, bezárás:
Saját halmaz típus:FILE *fp; fp = fopen("fajl.txt", "r"); fprintf(fp, "Hello"); fclose(fp);
typedef struct Halmaz { int elemszam; int *dintomb; } Halmaz;
Halmaz *h; h = uj_halmaz(); halmaz_betesz(h, 5); halmaz_betesz(h, 9); halmaz_felszabadit(h);