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:
    for (i = 1; i <= 100; ++i) printf("%d\n", i);
    A működő, de nem annyira szerencsés megoldások pedig:
    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(...) és int 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 egy struct 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ó, a while 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:
    if (x < 0)
        ;
    else
        printf("nem negatív");
    Inkább írjuk ezt:
    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...
  • 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).
  • 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:
    double tomb[100], szum;
    
    szum = 0;
    for (int i = 0; i < 100; ++i)
        szum += tomb[i];
    Helytelen stílus:
    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];
    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:
    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");
    }
    A lenti sokkal jobb, sőt így csak egyszer kell leírni:
    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);
    Dokumentáljuk a függvényél azt, hogy egy változóra mutató pointert vagy tömböt kap! Előnyös lehet, ha a definiálatlan méretű tömb szintaxist használjuk a függvényparamétereknél.
    void tombot_kiir(int i[], int meret);
    De ne feledjük, hogy ilyenkor is pointer adódik át! Sokan ezt sajnos félreértik.
  • 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 az fopen()-fclose() logikáját! Ezt ismeri mindenki.
    Fájl megnyitása, fájlművelet, bezárás:
    FILE *fp;
    fp = fopen("fajl.txt", "rt");
    fprintf(fp, "Hello");
    fclose(fp);
    Saját halmaz típus:
    typedef struct Halmaz {
       int elemszam;
       int *dintomb;
    } Halmaz;
    Halmaz *h;
    h = uj_halmaz();
    halmaz_betesz(h, 5); halmaz_betesz(h, 9);
    halmaz_felszabadit(h);
    Ebben az elemeket tároló tömb is dinamikus, és a struktúrák is dinamikusan foglalódhatnak. Mégis milyen könnyű használni!