Kódolási stílus – javaslatok
Czirkos Zoltán · 2018.08.22.
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 optimalizá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, még papíron is. 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);
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?
- 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á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. - Használjunk értelmes 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(int szam) { for (int oszto = 2; oszto <= szam/2; oszto++) if (szam % oszto == 0) return false; return true; } int main(void) { int db, vizsgalt; vizsgalt = 2; db = 0; while (db < 20) { if (prim(vizsgalt)) { printf("%d ", vizsgalt); db++; } vizsgalt++; } 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 vagy függvényt a feladattól függően.
- Ne használjunk goto-t, break-et, continue-t feleslegesen. Egy-két nagyon speciális esettől eltekintve szebb kódot lehet írni nélkülük.
- 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.
- 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.
- 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ó.
Nyelvi eszközök
- 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.
- Lehetőleg ne legyen üres ciklustörzs, vagy ha mégis, akkor a ciklustörzs helyére tegyünk kommentet, hogy ordítson, hogy üres.
- Ne írjunk a feltételekhez üres igaz ágat, inkább tagadjuk a feltételt! Tehát ehelyett:
Inkább írjuk ezt:if (x < 0) ; else printf("nem negatív");
if (x >= 0) printf("nem negatív");
- Operátorok: 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 keverjük a logikai és az aritmetikai kifejezéseket, hiába van rá mód.
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:
- 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:
- 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:
Helytelen stílus:double tomb[100], szum; 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 kod... * */ /* hova lett a szum = 0? nem kéne az 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; /* <- inicializalt valtozo... de minek? */ /* kiirja a szamok primtenyezos felbontasat */ for (int i = 2; i <= 10; i++) { printf("%d: ", i); szam = i; while (szam > 1) { while (szam%oszto == 0) { printf("%d ", oszto); szam /= oszto; } oszto++; } oszto = 2; /* elso ranezesre: ez meg mit keres itt?! */ /* hiszen mar nem csinalunk vele semmit! */ /* most vegeztunk a primtenyezokkel! minek */ /* az osztonak ujra erteket adni akkor? */ printf("\n"); }
for (int i = 2; i <= 10; i++) { printf("%d: ", i); int oszto = 2; // ennek itt a helye! 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
- 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);
De ne feledjük, hogy ilyenkor is pointer adódik át! Sokan ezt sajnos félreértik.void tombot_kiir(int i[], int meret);
- 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, ha igen.
- 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.
- 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.
- 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", "rt"); 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);