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:
    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?
    Vagy pl. „8 osztója van”:
    if (osztok_szama(x) == 8)
    Nem annyira szerencsés:
    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:
    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;
    }
    Modernebb szemlélet:
    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 egy struct 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:
    if (x < 0)
        printf("x negatív");
    if (y % 2 == 0)
        printf("y páros");
    Helytelen:
    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:
    if (x < 0)
        ;   // nem kellene ezt megfordítani?
    else
        printf("nem negatív");
    Inkább írjuk ezt:
    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ó, 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.
  • 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:
    for (i = 0; i <= n; i += 1) {
        if (i == 0 || i == n)
            printf("+");
        else
            printf("-");
    }
    Érthetőbben ugyanaz:
    printf("+");
    for (i = 1; i < n; i += 1)
        printf("-");
    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.
  • 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...
  • 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ó:
    if (!isalpha(ch)) // ok, logikai
    Ez már kevésbé:
    if (isalpha(ch) == 0) // működik, de miééért?!
    Ez így rendben van:
    if (strcmp(a, b) == 0) // 0-t ad, ha egyenlőek
    Ez viszont kevésbé érthető:
    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(...) é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.
  • Ne bonyolítsuk a logikai függvényeket redundáns if (...) return true; else return false; vezérlési szerkezettel. Helyes, rövid, egyszerű:
    bool paros_e(int szam) {
        return szam % 2 == 0;
    }
    Nem nyújt többet, viszont sokkal hosszabb:
    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:
    double tomb[100];
    
    double szum = 0;
    for (int i = 0; i < 100; ++i)
        szum += tomb[i];
    Működik ugyan, de nem szép stílus:
    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];
    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;                /* 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");
    }
    A lenti sokkal jobb, sőt így csak egyszer kell leírni:
    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).
  • 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!
  • 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 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", "r");
    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!