Könyvhöz tartozó online melléklet

Könyv: C++ hibaelhárító (Stephen C. Dewhurst)

Melléklet: Olvasson bele!

Az, hogy egy hiba alapvető, nem azt jelenti, hogy nem súlyos, vagy hogy nem gyakori

Az, hogy egy hiba alapvető, nem azt jelenti, hogy nem súlyos, vagy hogy nem gyakori. Ami azt illeti, az ebben a fejezetben tárgyalt alapvető hibák nagyobb aggodalomra adnak okot, mint a későbbi fejezetekben bemutatandó műszakilag bonyolultabbak. Ennek pedig egyszerűen az az oka, hogy e hibák éppen alapvető természetükből adódóan bármely C++ kódban előfordulhatnak.

1. hiba: Túl sok megjegyzés használata

A forráskódokban szereplő megjegyzések közül nagyon sok szükségtelen. Nehezítik a kód olvasását és megértését, sőt gyakran félre is vezetik az olvasót. Nézzük például a következő programsort:

a = b;  // b értékét a-hoz rendeljük

Ez a megjegyzés semmi többet nem nyújt, mint a kód maga, vagyis szükségtelen. Ami azt illeti, nem is egyszerűen szükségtelen, inkább kifejezetten rossz. Rettenetes! Először is elvonja az olvasó figyelmét a tényleges kódról, így az több szöveget lesz kénytelen átnyálazni ahhoz, hogy kihámozza belőle a kód értelmét. Másodszor az ilyen megjegyzéseket a kóddal együtt kell karbantartanunk és szükség esetén módosítanunk, vagyis megnöveltük saját teendőink számát. Harmadszor, a megjegyzéseket általában elfelejtjük módosítani:

c = b; // b értékét a-hoz rendeljük

Egy figyelmes programozó, ha megkap egy ilyen kódot karbantartás végett, nem tételezheti fel egyszerűen, hogy a megjegyzés hibás. Kénytelen lesz hosszasan elemezni a teljes kódot, hogy eldöntse, hibáról, félreértelmezhető megjegyzésről (c egy a-ra vonatkozó hivatkozás) vagy valamiféle szövevényes kapcsolatrendszerről (a c változó értékét valamikor később tényleg megkapja az a változó) van szó. Mindez elkerülhető lenne, ha a kérdéses sort mindennemű megjegyzés nélkül írtuk volna le:

a = b;

 

Ez a kód teljesen világos az elrontható megjegyzés nélkül is. Amúgy természetét tekintve ez a megállapítás ugyanolyan, mint az a gyakran hangoztatott szólás, miszerint a leghatékonyabb kód az, ami nem is létezik. Így van ez a megjegyzésekkel is: az a legjobb, ha nem kell leírnunk semmit, mert a kód maga annyira világos, hogy „önmagát dokumentálja”.

A szükségtelen megjegyzések másik gyakori megjelenési helye az osztályok bevezetése (deklarálása). Ennek vagy valamilyen rosszul elképzelt és alkalmazott kódolási szabvány az oka, vagy az, hogy a programozó kezdő, és félelmében a „saját megjegyzéseibe kapaszkodik”.

class C {

 // Nyilvános felület

 public:

   C(); // Alapértelmezett konstruktor

   ~C(); // Destruktor

   // . . .

};

Az ilyen kódot olvasva az embernek az az érzése támad, hogy valakinek a sebtében papírra vetett megjegyzéseit olvassa. Ha egy programozót emlékeztetni kell a public: kulcsszó jelentésére, annak az embernek kár volt odaadni ezt a kódot karbantartásra. Egy gyakorlott C++ programozó számára ezek a megjegyzések használhatatlanok; csak felesleges munkát jelentenek neki, hiszen kénytelen lesz ezeket is karbantartani, ha módosítja a kódot.

class C {

 // Nyilvános felület

 protected:

   C( int ); // Alapértelmezett konstruktor

 public:

   virtual ~C(); // Destruktor

   // . . .

};

Számos programozóban él az a törekvés, hogy ne „vesztegesse a sorokat” a forráskódban. A „közhiedelem” szerint, ha egy szerkezeti elem (függvény, egy osztály nyilvános felülete és így tovább) szokványos, közérthető formában leírható egyetlen lapon, vagyis körülbelül 30-40 sorban, akkor az a kód érthető lesz bárki számára. Ha csak kicsit is átnyúlik a következő lapra, kétszer olyan nehéz lesz megérteni. Ha netán még a harmadik lapra is szükség lesz hozzá, akkor a megértése négyszeres erőfeszítést igényel majd.

Az egyik legutálatosabb szokás, ha a végrehajtott változtatások „naplóját” a forráskód elejére vagy végére szúrja be a programozó megjegyzések formájában:

/* 6/17/02 SCD kijavította a gaforniflat hibát */

Ilyenkor az ember soha nem lehet benne egészen biztos, hogy a megjegyzésnek tényleg van valami jelentősége a kívülálló számra, vagy a program írója egyszerűen henceg, és dobálózik a szakkifejezésnek tűnő értelmetlen mondatokkal. Egy ilyen megjegyzés egy vagy két hét múlva már annak sem mond semmit, aki odaírta. Viszont esetleg hosszú évekre bennragad a forráskódban, idegesítve és félrevezetve a fejlesztők újabb generációit. Az efféle megjegyzések kezelését célszerűbb a változatkövető rendszerre bízni, elvégre egy program forráskódjában semmi keresnivalója a heti bevásárlólistánknak.

A valóban szükséges megjegyzések száma jelentősen csökkenthető, a kód pedig tisztává és könnyen karbantarthatóvá tehető, ha a programozási egységek (változók, függvények, osztályok) jelölésére értelmes neveket használunk, illetve jól átgondolt elnevezési szabályokat alkalmazunk. A deklarációkban szereplő formális paraméterek világos elnevezése különösen fontos. Vegyünk például egy függvényt, ami három azonos típusú paramétert vesz át:

/*

Végrehajtja a megadott műveletet a forráson, és az eredményt elhelyezi a célként megadott helyen. Arg1 a művelet kódja, arg2 a forrás, arg3 a cél.

*/

void perform( int, int, int );

Nos igen. Ez végül is nem olyan borzalmas, de képzeljük csak el, hogy nézne ki ugyanez mondjuk hét vagy nyolc ilyen paraméterrel. Csináljuk tehát jobban:

void perform( int actionCode, int source, int destination );

 

Ez már jobb. Persze azért egy egysoros megjegyzésre még mindig szükségünk lesz, ha tudatni akarjuk az utókorral, hogy ez a függvény tulajdonképpen mit csinál (és akkor azt még el se mondtuk, hogyan csinálja). A szépen elnevezett formális paraméterek egyik legvonzóbb tulajdonsága az, hogy az ember önkéntelenül karbantartja őket a kód többi részével együtt. Ugyanezt a megjegyzésekkel nem mindenki teszi meg. Ami azt illeti, nem tudok elképzelni olyan programozót, aki felcserélné a fenti függvény első és második paraméterét anélkül, hogy megváltoztatná a nevüket is. Olyanból viszont egy hadseregnyit ismerek, aki ugyanezt szívfájdalom nélkül megtenné egy megjegyzéssel.

Mindezt a legvilágosabban talán Kathy Stark írta le Programming in C++ (Programozás C++ nyelven) című könyvében: „Ha értelmes vagy legalább értelmezhető neveket használunk egy programban, általában semmi szükség nincs a további megjegyzésekre. Ha viszont a használt nevek értelmetlenek, nincs az a megjegyzés, ami a dolog egészét világosabbá tenné.”

A megjegyzések számát úgy is csökkenthetjük, ha szabványos vagy közismert programozási elemeket használunk:

printf( "Hello, World!" ); // A „Helló világ!” szöveg kiírása

ĺ a képernyőre

 

Ez a megjegyzés egyrészt szükségtelen, másrészt csak időnként helyes. Itt most nem arról van szó, hogy az efféle szabványos programelemek önmagukért beszélnek, sokkal inkább arról, hogy az ilyen megoldások közismertek és jól dokumentáltak.

swap( a, a+1 );

sort( a, a+max );

copy( a, a+max, ostream_iterator<T>(cout," ") );

 

Mivel ebben a példában a swap, a sort és a copy szabványos függvények, bármilyen további megjegyzés beszúrása szükségtelen, viszont szinte szükségszerűen pontatlan lenne.

A megjegyzések nem eredendően rosszak, és gyakran tényleg szükség van rájuk. Viszont nem szabad elfelejteni, hogy a megjegyzéseket ugyanúgy karban kell tartani, mint a kódot magát, sőt ezek karbantartása rendszerint nehezebb. A megjegyzések ne tartalmazzanak nyilvánvaló állításokat, vagy olyasmit, aminek máshol a helye. A cél tehát nem az, hogy a megjegyzéseket bármi áron elkerüljük, hanem az, hogy a mennyiségüket a lehető legalacsonyabban tartsuk, illetve hogy általuk érthető és jól karbantartható kód keletkezzen.

2. hiba: Mágikus számok

A „mágikus számok” jelen összefüggésben olyan számállandók (literálok), amelyek helyett az adott környezetben célszerűbb lenne nevesített állandókat használni.

class Portfolio {

   // . . .

   Contract *contracts_[10];

   char id_[10];

};

A mágikus számokkal kapcsolatban az a legnagyobb baj, hogy értékükön túl nincs semmilyen egyéb jelentésük, amit kötni tudnánk bármihez. Azok amik: számok. Az hogy 10, semmi egyebet nem jelent, csak annyit, hogy 10. Nem derül ki belőle, hogy ez a megköthető szerződések számának felső határa, vagy egy azonosító kód legnagyobb hossza. Ezért ha mágikus számokat tartalmazó kódot kell karbantartanunk, kénytelenek vagyunk kitalálni a programozó eredeti szándékát. Ez pedig egy kívülálló számára munka. Szükségtelen és gyakran pontatlan munka.

A fenti, meglehetősen ügyetlenül megtervezett példában a megköthető szerződések száma tíz, és ez a határ egy mágikus szám segítségével van beállítva. Tíz szerződés az nem túl sok. Ha netán úgy döntünk, hogy felemeljük a határt mondjuk 32-re, akkor kénytelenek leszünk átnyálazni az összes állományt, ami használja a Portfolio osztályt, ugyanis bármelyikben szerepelhet a korábbi mágikus tízes. Ráadásul az sem biztos, hogy mindenütt minden mágikus tízes a szerződések legnagyobb számát jelenti. (És akkor még nem is említettük azt a problémát, hogy a pontosság és biztonság érdekében eleve nem ártott volna a szabványos vector típust használni.)

Ami azt illeti, a valós helyzet néha még ennél is rosszabb lehet. Egy igazán nagy és hosszú időtartamú munka során egyesek esetleg kijelenthetik, hogy a megköthető szerződések legnagyobb száma tíz. Ezt mindenki hallja, tudomásul veszi, és a mágikus tízes beépül olyan kódrészletekbe is, amelyek egyáltalán nem hivatkoznak a Portfolio osztályra, egyszerűen azért, mert valamilyen más szempontból kezelik a szerződéseket:

for( int i = 0; i < 10; ++i )

   // . . .

Hát ez a tízes itt mire vonatkozhat? A szerződések legnagyobb számára, vagy egy azonosító hosszára? Esetleg valami teljesen másra?

A mágikus számok véletlen összekeverésének lehetősége néha a legrosszabbat hozza ki a programozókból:

if( Portfolio *p = getPortfolio() )

   for( int i = 0; i < 10; ++i )

       p->contracts_[i] = 0, p->id_[i] = '';

A program karbantartójának immár valahogyan szét kell válogatnia a Portfolio egyes összetevőinek kezdeti beállításait, figyelve arra, hogy csupán véletlen egybeesés, hogy két eltérő elem értéke ugyanaz. Erre a szükségtelen bonyodalomra pedig egyszerűen nincs mentség, ha a megoldás ilyen egyszerű:

class Portfolio {

   // . . .

   enum { maxContracts = 10, idlen = 10 };

   Contract *contracts_[maxContracts];

   char id_[idlen];

};

 

A felsoroló típusok nem foglalnak helyet, nem követelnek nagyobb számítási teljesítményt futásidőben, viszont teljesen tisztává teszik, hogy mi mit jelent.

A mágikus számok kevésbé nyilvánvaló, de annál veszélyesebb mellékhatása, hogy meghatározatlan típusúak, s így tárolási méretük is változhat. A literális 40000 típusa például szinte biztosan rendszerfüggő. Ha az érték elfér egy egész típusban, akkor egész (int) lesz. Ha nem, akkor long. Ha el akarjuk kerülni az ezzel kapcsolatos hibákat, például a túlterhelések feloldása körüli gondokat, jobb, ha mi magunk mondjuk meg, hogy pontosan mit is akarunk, ahelyett, hogy ezt a döntést a fordítóra és az adott rendszerre bíznánk:

const long patienceLimit = 40000;

 

A literális értékekekkel kapcsolatos másik gond, hogy nincs címük. Bár ez nem túl gyakran okoz fejfájást, néha kifejezetten hasznos lehet, ha képesek vagyunk egy állandó értéket címző mutatót vagy hivatkozást használni:

const long *p1 = &40000; // Hiba!

const long *p2 = &patienceLimit; // Rendben.

const long &r1 = 40000; // Rendben, de nézzük meg a 44. hibát.

const long &r2 = patienceLimit; // Rendben.

Összefoglalva tehát, a mágikus számok használatának egyetlen előnye sincs, viszont számos problémát okozhatnak, ezért használjunk helyettük felsoroló típust, vagy kezdőértékkel ellátott állandókat.

3. hiba: Globális változók

Általában nincs mentség a „nyers” globális változók használatára. Ezek nehezen karbantarthatóvá teszik a kódot, és a program újrahasznosíthatósága is korlátozott lesz. Az újrahasznosíthatóság azért sérül, mert a globális változó „összenő” a kóddal. A kettő csak együtt használható, külön-külön egyiknek sincs értelme. Az efféle általánosság a karbantartást is nehezíti, hiszen az adott változó bárhonnan hozzáférhető, így semmi sem utal arra, hogy mely kódrészletek használják.

A globális változók erősítik az egyes kódrészletek összetartozását, egymásra utaltságát, és éppen ez a baj velük. Ezek a változók ugyanis gyakran egyfajta üzenetközvetítő szerepet töltenek be a különböző programelemek között. A módszer persze tökéletesen működhet, de egy globális változót eltávolítani egy nagyobb programból gyakorlatilag lehetetlen. Ugyanakkor az általános érvényűség miatt ezek az üzenetközvetítők semmiféle védelmet nem élveznek, így bármelyik kezdő programozó, akit megbíztak a kód karbantartásával, a rendszer teljes összeomlását idézheti elő.

Akik mindezek ellenére globális változókat alkalmaznak, általában a kényelmes használatot említik fő érvként. Ez persze csalóka, önigazoló érv, hiszen a fejlesztés általában kevesebb időt igényel, mint a karbantartás, a globális változók viszont ez utóbbit elképesztően megnehezíthetik. Tegyük fel például, hogy programunknak állandóan hozzá kell férnie egy globálisan elérhető „környezethez”, amelyből (megígérték nekünk, hogy így lesz) mindig pontosan egy létezik. Sajnálatos módon ezt a kapcsolatot a program és a környezete között egy globális változó segítségével oldottuk meg:

extern Environment * const theEnv;

Sajnos az ígéreteket gyakran nem tartják be. Pár nappal a szállítás előtt valaki mégis rájön, hogy két lehetséges környezete is lehet a programnak. Vagy három. Vagy az is lehet, hogy a környezetek számát majd a felhasználó fogja megmondani induláskor. Vagy ő sem fog mondani semmit, hanem a környezetek száma lesz „dinamikus”: hol ennyi, hol annyi. Ez az a bizonyos utolsó pillanatban bejelentett változás. Még egy nagy projektben is, ahol egyébként gondosan felépített forráskód-kezelő rendszert használnak, meglehetősen időrabló minden egyes fájlt módosítani, még akkor is, ha a módosítás egyszerű. A folyamat napokat vagy heteket vehet igénybe. Persze ha nem használtunk volna globális változót, az egész meglenne úgy öt perc alatt:

Environment *theEnv();

 

Ha a környezethez való hozzáférést egy függvénybe rejtjük, probléma esetén lehetőségünk van annak túlterhelésére, vagy valamelyik paraméterének módosítására. Ehhez nem lesz szükség a kód nagyobb arányú módosítására, csak a függvényt magát kell átszabni:

Environment *theEnv( EnvCode whichEnv = OFFICIAL );

Van a globális változókkal kapcsolatban egy másik, nem annyira nyilvánvaló gond is: gyakran futásidőben, statikusan kell nekik értéket adni. Ha a kezdőérték fordításkor nem számítható ki, futásidőben kell beállítani, aminek esetenként katasztrofális következményei lehetnek:

extern Environment * const theEnv = new OfficialEnv;

Ha a globális információhoz való hozzáférést egy függvény vagy osztály „felügyeli”, a tényleges értékadás késleltethető addig, amíg kiderül, hogy biztonságosan elvégezhető-e:

ŕ 3. hiba/environment.h

class Environment {

 public:

   static Environment &instance();

   virtual void op1() = 0;

   // . . .

 protected:

   Environment();

   virtual ~Environment();

 private:

   static Environment *instance_;

   // . . .

};

ŕ 3. hiba/environment.cpp

// . . .

Environment *Environment::instance_ = 0;

 

Environment &Environment::instance() {

   if( !instance_ )

       instance_ = new OfficialEnv;

   return *instance_;

}

 

Ebben az esetben a Singleton modell egy egyszerű megvalósítását alkalmaztuk, ami „lusta kezdőérték-adást” hajt végre a statikus környezeti mutatón, ezzel biztosítva, hogy soha ne lehessen egynél több érvényes környezet (Environment objektum). Az Environment objektumnak nincs nyilvános konstruktora, így a felhasználó kénytelen az instance tagot használni, ha hozzá akar férni a statikus mutatóhoz. Ez pedig lehetőséget ad arra, hogy az Environment objektum tényleges létrehozását elhalasszuk az első hozzáférésig:

Environment::instance().op1();

Vissza a könyv részletes adataihoz

Legutóbb látogatott oldalaim

Keresések Könyvek, termékek Kategóriák