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. 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();