Hibakeresési, optimalizálási módszerek


  1. Felkészülés a hibakeresésre
    1. A CB felkészítése a hibakeresésre
    2. Az első projekt létrehozása
    3. A projekt fordítása és futtatása
  2. A töréspontok, és lehetőségeik
    1. A változók megfigyelése (watches)
    2. A hívási verem (call stack)
    3. További hibakeresési ablakok
  3. A try-catch szerkezet
    1. A kivétel objektumok
    2. A kivételek C alatt
  4. Optimalizálás
    1. A változók, hibakeresési adatok optimalizálása
    2. A függvényhasználat optimalizálása
    3. A pufferelés (gyorsítótárazás)
  5. A program közzététele
    1. Az alkalmazás tömörítése
    2. A súgó elkészítése
    3. A telepítőkészlet előállítása
    4. A hordozható csomag/programkód összekészítése

1, Felkészülés a hibakeresésre

A szintaktikai hibák megtalálása feltehetően senkinek nem okoz gondot mire eddig eljut, vagyis hogy megtaláljon egy elhagyott zárójelet, pontosvesszőt, elgépelést, stb. A fejezet ezért az elvi hibákkal fog foglalkozni, amik szintaktikailag helyesek - de a programot mégis összeomlasztják futás közben. 

Mind a sebtében összehajintott programokban, mind az előre megfontoltan elkészített óriási projektekben lehetnek elvi hibák, és ezek feltárása sok esetben komolyabb szellemi munka, mint maga a program megírása - hiszen végig kell gondolni a gép fejével hogy mit csinál ekkor és ekkor.

Ez a fejezet ezzel fog foglalkozni - az egyszerűség kedvéért pedig Code::Blocks (CB) alól.

Ahhoz hogy hibát tudjunk keresni, használhatjuk a parancssori GDB-t, vagy használhatjuk az IDE-t.  Mivel a CB a GDB-t használja, meg kell neki adni a helyét (ahol saját fordító, stb. van [pl. Borland C++] ott erre nincs szükség).

1a, A CB felkészítése a hibakeresésre

Természetesen a GDB felkutatásához ismernünk kell annak a helyét:

  • ha hivatalos csomagot szedtünk le, és külön tettük fel a MinGW-t akkor ott találjuk, ahova feltettük azt
  • ha az egyszerűsített csomagot (amit csak ki kell bontani) szedtük le, akkor a program mappájában lesz

Bárhol is van a .\bin\gdb.exe elérési úton találjuk. Nálam ez most:

D:\Programok\Programozas\CodeBlocks-EP\MinGW\bin\gdb.exe

Ha megtaláltuk a fájlt, akkor tallózzuk be az "Executable path: " című részhez, ebben az ablakban:

Ez az ablak a Settings > Debugger... menüpontban érhető el.

Ha ezzel végeztünk, az IDE már kész a hibakeresésre - de mi még nem.

1b, Az első projekt létrehozása

Ahhoz hogy hibát kereshessünk ezentúl projektben kell dolgozni, akkor is ha csak egy fájlból áll a program.

Ehhez első lépésként nyissuk meg az alábbi menüpontot:

A következő lépésben kapunk egy ilyen ablakot:

Ezen ablakból ami számunkra érdekes lehet azok az alábbiak:

  • Console Application: szöveges módú Windows alapú alkalmazás (.EXE)
  • Dynamic Link Library: dinamikus csatolású függvénytár (.DLL)
  • Win32 GUI project: grafikus módú (ablakos) Windows alapú alkalmazás (.EXE)
  • WinBGIm project: a Borland C++ grafikus képességeivel bíró, szöveges módú Windows alapú alkalmazás (.EXE)

Most egyelőre maradjunk a Console application típusú projektnél.

Ezek után meg fogja kérdezni hogy melyik programnyelven szeretnénk dolgozni, én egyelőre a C++ nyelvet választom ki, de típusszigorú C kódot fogok írni (ami megtehető).

Ez után a projekt nevét, és elhelyezkedését kell megadni. Érdemes a fájlnévben kerülni a szóközt és az ékezetet, mivel a GDB bizonyos esetekben nem működik jól, ha ilyenek vannak.

A projekt neve lesz különben a kész fájl neve is (na nem mintha nem lehetne átnevezni).

Több fordító esetén itt kiválaszthatnánk hogy mivel szeretnénk a programot fordítani, szerintem itt ez egyértelmű:

Itt fontos hogy legyen mind a Debug ("hibakeresési"), mind a Release ("kiadási") konfiguráció kipipálva - mindkettőre szükség lesz.

A Finish gombra kattintva oldalt megjelent a projektünk, benne egy alap progival:

Most két út áll előttünk - ha meglévő fájlokat (pl. korábban megírt .C + .H fájlokat) akarunk hozzáadni a projekthez akkor a projektre kattintsunk jobb gombbal, és válasszuk az "Add files..." menüpontot.

Amennyiben a programhoz új fájlt akarunk adni, akkor a megszokott módon kell megtenni:

Ezúttal a CB feltesz majd egy kérdést - hozzáadjuk az új fájlt a projekthez?

Természetesen igen. A mentésnél a projekt mappájába mentsük .C (vagy .CPP) kiterjesztéssel.

1c, A projekt fordítása és futtatása

A projekt fordítása továbbra történhet az F9 () segítségével, de ha ezt választjuk lényegében semmi plusz nem fog történni.

Azért csináltuk végig ezt a sok mindent, hogy a futtatás történhessen más módon is - először csak fordítunk (Ctrl+F9, ), majd futtatunk hibakereséssel (F8, ).

A hibakeresésnek ugyanakkor lesz egy plusz összetevője - a kész .EXE fájlban el kell helyezni hibakeresési információkat. Ilyen hibakeresési információ lesz pl. hogy az adott utasítás (.EXE fájlban), a programkód (.C) melyik sorához tartozik.

A hibakeresési információk kétélűek, ha benne felejtjük a programban, az sebezhető lesz és visszafejthető - ugyanakkor számunkra nélkülözhetetlenek a hibakereséshez.

Ezért van kétféle konfigurációnk a fordításhoz:

Ha a Debug változatot használjuk, tudunk hibát keresni - belefordulnak ezek az információk a .EXE fájlba.

Ha a Release változatot használjuk, a program kisebb lesz mivel a hibakeresési információkat nem fogja tartalmazni - így viszont nem lehet hibát keresni.

Mivel a Release módot kb. csak a feltöltés előtt szoktuk használni, általában Debug módban dolgozunk.

2, A töréspontok, és lehetőségeik

Tegyük fel hogy az alábbi programban nem tudjuk, hol fog összeomlani - és szeretnénk megtudni ezt.

#include <stdio.h>

int main()
{
  int i = 1, j = 0;
  printf("A hanyados: %d", i / j);
  return 0;
}

Erre vannak a töréspontok bevezetve - használatukhoz egészen egyszerűen annyit kell tennünk hogy az adott sor száma elé kattintunk, úgy hogy ott megjelenjen egy piros pötty:

A fordítás (Ctrl+F9), és futtatás (F8) során a program meg fog állni minden egyes sor lefuttatása előtt:

Jól látható hogy az 5. sorig eljutunk, tehát eddig nincs hiba - ha most összeomlik a következő sorban, akkor az 5. sor volt a ludas. Az F8 hatására folytatódik a program.

De nem teszi meg, simán eljut a 6. sorig, tehát gyanítható hogy addig helyes a program. Az F8 ismételt megnyomása hatására a program kivételt dob fel, és a CB megváltozik:

Első lépésben értesít minket egy hibáról, másrészt felugrik egy "Call Stack" nevű (hívási verem) ablak, ami megadja hogy melyik függvényben, melyik címen történt kivétel (ha a projekt sima C-hez lenne nem lenne ilyen sok adatunk a hibáról).

Arról is információt kapunk a jobb alsó sarokban, hogy számítási hiba történt (arithmetic exception = aritmetikai kivétel).

Az F8 ismételt lenyomására a program befejezi futását megint:  

De ez most nem probléma mert rengeteg információval lettünk gazdagabbak:

  1. Számítási hiba van a 6. sorban.
  2. Mivel a printf amúgy normálisan szokott működni, feltehetően az osztással lesz gebasz.
  3. Mivel az osztással sem szokott gond lenni, feltehetően a két oldalán lévő értékekkel lesz gond.
  4. Ha megnézzük hogy mit osztunk el mivel, akkor azt láthatjuk hogy a második tag nulla (j = 0) - így a program a nullával való osztás miatt hasal el.

Megoldás: pl. j = 1 hatására a progi hibátlanul fog futni.

A példa azért is érdekes mert a hiba nem azért volt, mert a 6. sorban rossz dolgot adtunk meg, hanem azért mert a korábban megállapított adatokkal (5. sor) ez a művelet nem végezhető el.

2a, A változók megfigyelése (watches)

A fenti példa nagyon gagyi, hiszen előre tudjuk hogy mivel fogunk osztani.

Mi van olyankor, ha valamilyen bonyolult számítás útján, netán algoritmussal jutunk el a fenti értékekre? 

Ilyenkor jó lenne nyomon követni a változók értékét, és egy két jó helyre kitett törésponttal ez meg is valósítható.

Megint kiemelném, hogy a töréspont az adott sor lefutása előtti állapotot térképezi fel, vagyis ha egy változó megváltozott értékére vagyunk kíváncsiak, akkor a számítás utáni sorba kell tenni a töréspontot.

 

Jelen programban valami számítás által kapjuk meg a két értéket, és mivel a számolt értékek érdekelnek minket a printf sorára tesszük a töréspontot.

Az F8 lenyomására megáll a printf-nél a program, de semmi információnk nincs a változókról. Ahhoz hogy erről adatokat kapjunk, jelenítsük meg a Watches ablakot:

És ezek után már is megkapjuk az i, és j számolt értékét:

Megjegyzés: Az algoritmus "egészen véletlenül", szándékosan ugyanazokat a számokat adja ki mint a fenti példában.

Mi van akkor ha a töréspont a szamol utasítás egyetlen sorába kerül?

Ilyenkor kiírja a két paramétert, de a visszatérési értéket nem: 

 

Ilyenkor be szoktunk vezetni ún. hibakeresési segédváltozókat:

#include <stdio.h>
#include <math.h>

int szamol(int a, int b)
{
  int _h_RetVal = b > 0 ? (int)sqrt(fabs(pow(a, -1))) : (int)sqrt(fabs(pow(a, 0) - 1));
  return _h_RetVal;
}

int main()
{
  int i = szamol(-1, 1), j = szamol(-1, -1);
  printf("A hanyados: %d", i / j);
  return 0;
}

Ezek a változók nem változtatnak a kódon, de lehetővé teszik a kiolvasását egy olyan értéknek, amit amúgy nem tudunk kiolvasni. Ezeket optimalizálás során érdemes eltávolítani, ezért célszerű megkülönböztetni őket (pl. "_h_" jellel kezdeni).

 

Mivel a szamol utasítást meghívjuk, ezek a töréspont kétszer is le fog futni:

 

Összességében tehát így használható a Watches ablak. Azért azt érdemes tudni hogy mindenre ez sem képes:

A szoveg egy char* típusú változó, ezzel még elboldogul, az i, j ugye int így egyértelmű, az sa egy statikus tömb - ezzel is egész jól boldogul, viszont a da egy dinamikus tömb - ezzel már nem, csak a memóriacímét kapjuk meg.

2b, A hívási verem (call stack)

Habár már szó esett róla mikor felugrott a hibánál, érdemes egy kicsit megnézni ezt is. 

Először is, arról hogy mi az a verem programozási szempontból:

 

Ez egy üres verem - de mi van ha teszünk bele elemeket?

 

Ezeket képzeljük el mint fóliával elválasztott homokrétegeket. Evidens hogy a mélyebben lévőket csak akkor tudjuk kivenni ha a felette lévőket kivettük - ez a verem lényege, ez egy LILO adatszerkezet (last-in, last-out).

A programozás során a gép a meghívott függvényekben hogy hol járt, egy ugyanilyen veremben tárolja - elindul egy fv. - elindít egy alfüggvényt - ha lefutott kiveszi a veremből - folytatja ahol abbahagyta az fv.-t. Ez a hívási verem.

A hívási verem tehát szépen meg fogja nekünk mutatni hogy melyik függvény, melyiket, milyen paraméterekkel hívta meg.

A fenti példára értelmezve, a szamol függvényben lévő töréspontra:

A hívási veremből jól látható hogy a main(), a teljes kód 15. sorában meghívja a szamol(-1, 1) utasítást. Ha esetleg a paraméterekben kifejezés van, itt már a kiértékelt kifejezést láthatjuk. 

Összetett programokban a hívási verem nagyon sokat segíthet, hiszen mint ezt láthattuk a kivétel nem mindig a helytelen adatoknál keletkezik, és ilyenkor ezáltal visszakövethető a helytelen adatok útja.


Korábban már foglalkoztunk a rekurzióval, és szó esett arról a faktorialis() függvény kapcsán, hogy miként néz ki a hívási verme. Nézzük ezt meg valójában (lásd a függvényt):

Így egyértelműen jól követhető, hogy mindig kiszámolja az eggyel kisebb faktoriálist, és azt szorozza meg azzal a számmal ahanyadik éppen kell - de a kisebb szám esetén is ezt csinálja - egészen amíg 1-et el nem érjük, mert akkor 1! == 1.

2c, További hibakeresési ablakok

Ha sok fájlunk van a Breakpoints ablak kiírja az adott fájlokban lévő töréspontok helyét.

 

Ha egy egyszerű dinamikus szerkezeten lévő adatokat szeretnénk megtekinteni akkor használható a Memory dump ablak.

 

A hátránya hogy az adatokat mint karakterek, illetve mint byteok halmaza tizenhatos számrendszerben kapjuk meg - ami relatíve nehezen értelmezhető.

Ha a program lefordított gépi kódja érdekelne minket, akkor az is elérhető Assembly nyelven a Disassembly ablakban.

 

Természetesen ha már ennyire érdekel minket hogy a processzor mit csinál, belepillanthatunk a regiszterekbe is a CPU Registers ablak által:

 

Ezek mind-mind természetesen töréspontokra értelmezettek. 

3, A try-catch szerkezet

Mint erről már szó esett a hibákat el is kell hárítani, aminek eddig az első két megoldásával ismerkedtünk meg:

  1. a program leakasztása, hibakemeneti üzenettel + hibakóddal (lásd itt)
  2. a hibák elkerülése, menet közbeni ellenőrzésekkel, és feltételes műveletvégzéssel (lásd itt)
  3. kivételkezelés: a hibák észlelése, jelzése - de közben figyelmen kívül hagyása/vészleakasztás

Ehhez a try-catch szerkezet lesz a segítségünkre, amivel a program elején bemutatott kivételeket lehet elkapni, és kezelni.

try
{
  // kritikus sorok
  // ha valami nem jó akkor: throw <hibakod>;

}
catch (<típus> hibakod)
{
  // mit csináljon a hibakóddal a program
}
// a program futása folytatódni fog, nem áll meg mint eddig...

Fontos: Ez a szerkezet csak C++ alatt érhető el - amiért a projektet is ilyennek választottuk. A try-catch kivételeket kezel, de kivételek csak C++ alatt lesznek. (kesőbb leírom miként lehet C alatt is ilyen szerkezetet használni).

Ahhoz hogy a try-catch szerkezetet megvalósíthassuk, egyszerre kell az első két pont megoldását használni - figyelni kell a hibát, és visszajelezni ha baj van.

Ez az egyszerű példa magyarázatot adhat a dolgokra: 

#include <stdio.h>

int main()
{
  int i = 1, j = 0;
  try
  {
    if (j == 0) throw 1;

    printf("A hanyados: %d", i / j);
  }
  catch(int hibakod)
  {
    fprintf(stderr, "A program futasa soran hiba tortent. Hibakod: %d.", hibakod);
  }
  return 0;
}

A throw szerepe hasonló a returnéhoz, de csak az adott try blokkot fogja megszakítani, és egyből ugrani fog a catch blokkra. Azt hogy a thrownak milyen típusú adatot kell visszadania, a catchben megadott típus dönti el, a példában ez most int.


Ha több try-catch blokkot ágyazunk egybe, akkor van lehetőségünk arra - hogy a belsőben történő kivételt kiirányítsuk a külsőbe.

try
{
  try

  {
    //nagyon kritikus szakasz
  }
  catch (<típus> hibakod)
  {
    throw; //kiküldés a külső szintre
  }
catch (<típus> hibakod)
{
  printf("Hiba tortent."); //a belső, és külső hibákat is itt kezeljük
}

3a, A kivétel objektumok

A kivétélek érkezhetnek objektumként is (std::exception típus), ekkor a what() utasítás fogja megadni a hibaüzenetet. Egy ilyen objektum elkészítését jelenleg nem firtatnám, amíg nem esik szó arról, hogy hogyan készíthetünk objektumokat.

try
{
}
catch (exception& kivetel)
{
  fprintf(stderr, "%s", kivetel.what());
}

Sok esetben külön ellenőrzés nélkül kapunk ilyen kivétel objektumokat, pl.:

név oka
bad_alloc ha a new[] utasításnak nem sikerül a foglalás
bad_cast ha a típuskonverzió helytelen
bad_exception általános hibák esetére
bad_typeid típusazonosítókkal kapcsolatos hiba
bad_function_call függvények hívása során fellépő hiba
bad_weak_ptr az ún."weak_ptr" objektummal kapcsolatos hiba
logic_error belső logikai hiba esetére
runtime_error bármely futásidejű hiba esetére

Maga a kivétel objektum, és ezek a standard kivételek az <exception> fájl beszerkesztését igénylik. Példa:

#include <stdio.h>
#include <exception>


int main()
{
  try
  {
    int* nagy_tomb = new int[0xFFFFFFFF];
  }
  catch(exception& kivetel)
  {
    fprintf(stderr, "A program futasa soran kivetel tortent: %s", kivetel.what());
  }
  return 0;
}

Attól nem kell félni, hogy ez a program véletlenül sikeresen lefoglalja a tömböt - a tömb mérete ~16GB lenne, 32 bites fordítóval pedig az előállított program legfeljebb ~2GB memóriát tud összesen lefoglalni.

A futás eredménye:

A program futasa soran kivetel tortent: std::bad_alloc

Process returned 0 (0x0) execution time : 0.000 s
Press any key to continue.

3b, A kivételek C alatt

Mint ígértem a C alatt is megmutatom, miként kivitelezhetőek ezek a try-catch szerkezetek. Beépítve nincsenek a nyelvbe, de ezen makrókkal pótolhatóak (forrás itt):

#include <setjmp.h>

#define TRY do{ jmp_buf ex_buf__; switch( setjmp(ex_buf__) ){ case 0:
#define CATCH(x) break; case x:
#define ETRY } }while(0)
#define THROW(x) longjmp(ex_buf__, x)

Ezeket csak be kell másolni, és már is összeüthető egy egyszerű példa:

#include <stdio.h>
#include <setjmp.h>

#define TRY do { jmp_buf ex_buf__; switch( setjmp(ex_buf__) ) { case 0: while(1) {
#define CATCH(x) break; case x:
#define ETRY break; } } }while(0)
#define THROW(x) longjmp(ex_buf__, x)


int main()
{
  int i = 1, j = 0;
  TRY
  {
    if (j == 0) THROW(1);

    printf("A hanyados: %d", i / j);
  }
  CATCH(int hibakod)
  {
    fprintf(stderr, "A program futasa soran hiba tortent. Hibakod: %d.", hibakod);
  }
  ETRY
  return 0;
}

Mivel a C alapból nem ismeri az objektumokat, a kivételobjektumokat is elfelejthetjük, de definiálhatunk hasonlóakat:

#define DIVIDE_EXCEPTION (1)

...

THROW(DIVIDE_EXCEPTION);

4, Optimalizálás

Ha már a programunk minden hibának ellenáll, és képtelenség összeomlasztani akkor megkezdődhet az optimalizálás.

Az optimalizálás során fontos eldönteni, hogy a program célplatformja mi lesz. Ha szervergépre írjuk a programot, akkor minél kevesebb számítást kell végezni, inkább sok változó árán, míg hogyha otthoni számítógépre írjuk, akkor a korlátos memória miatt a változók száma épelméjű keret közé kell szoruljon.

4a, A változók, hibakeresési adatok optimalizálása

Néhány dolgot ennek ellenére meg lehet ejteni platformtól függetlenül:

  • A fölösleges segédváltozók eltávolítása (pl. hibakeresési segédváltozók)
  • A felesleges kommentek kiszedése az átláthatóság javításáért
  • A hibakeresést segítő kódrészletek eltávolítása
  • A program fordítása Release módban

4b, A függvényhasználat optimalizálása

Szintén minden esetben használható optimalizálást végezhetünk a függvények esetén.

Bármikor, ha meghívunk egy függvényt, az lefut - következésképpen, a visszatérési értékére ha többször is szükség van, akkor azt mindenféleképpen tároljuk el a memóriában.

pl.:

#include <stdio.h>
#include <new>
#include <math.h>

int* szamol(int a, int b)
{
  int* tomb = new int[100000], i;

  tomb[0] = 0;

  for (i = 1; i < 100000; i++)
    tomb[i] = lround(sqrt((fabs(pow(a, i)) - 1) / (fabs(pow(b, i)) + 1))) + tomb[i - 1];

  return tomb;
}

Legyen ez a függvény, ami előállít egy 100 000 számból álló számsort. Ezen számsornak szeretnénk megtudni a 2048., és 4096. elemének az összegét, ha a=3, b=2.

int main()
{
  printf("%d", szamol(3, 2)[2047] + szamol(3, 2)[4095]);

  return 0;
}

Ebben a kódban remekül megjelenik a fentebb leírt hiba:

  • A szamol(3, 2) lefut és kiszámolja mind a 100 000 elemet.
  • Eredményül ad egy dinamikus tömböt, amiből mi kiveszünk egy elemet, de b*sszuk felszabadítani
  • A szamol(3, 2) lefut és megint kiszámolja mind a 100 000 elemet
  • Eredményül ad egy dinamikus tömböt, amiből mi kiveszünk egy elemet, de b*sszuk felszabadítani
  • A két kivett elemet összeadjuk, és kiírjuk

A felszabadítások hiánya miatt a program alapból vérzik, de tetézzük ezt azzal hogy duplán kiszámoltatjuk a géppel (feleslegesen) ugyanazt. Íme egy javított megoldás:

int main()
{
  int* adatok = szamol(3, 2);
 
  printf("%d", adatok[2047] + adatok[4095]);

  delete [] adatok;

  return 0;
}

A javított változat, egy tömböt használ végig, és azt fel is szabadítja - ráadásul mindössze két sorral hosszabb.

A futásideje az elsőnek 301ms, míg a másodiknak 153ms. Ha belegondolunk hogy mondjuk egy játéknak ez az összefüggés egy apró kis elemét számítja, nagyon nem mindegy hogy fele, vagy dupla annyi ideig tart a segédszámítás, hiszen 1000x lefuttatva már percekről beszélünk.

4c, A pufferelés (gyorsítótárazás)

Az előző példában lényegében egy puffert csináltunk a main függvényben adatok néven. A puffer/gyorsítótár egy olyan memóriaterület, amire ideiglenesen, a számítást lényegesen meggyorsító adatok kerülnek.

Képzeljük el, hogy szeretnénk leábrázolni egy matematikai függvényt, pl. y=f(x)=x*x

Ennek a legegyszerűbb módja számítógéppel, hogy behelyettesítünk bizonyos x értékekre, és megkapjuk az y magasságot. Ezeket a P(x,y) pontpárokat összekötve kapunk egy ábrát.

pl.:

x -4 -3 -2 -1 0 1 2 3 4
y 16 9 4 1 0 1 4 9 16

Azért az észrevehető, hogy az összekötő vonalak egyenesek, és nem ívek mint fenn - ezért egyrészt pontosabb vizsgálat kéne, másrészt nagyobb tartomány.

A másik dolog, hogy a kirajzolásról azt érdemes tudni, hogy a Borland C++ grafikáján kívül, rendesen ez egy ismétlődő művelet lenne. Ez azt jelenti, hogyha odébbmozdul az ablak, átméretezzük, stb. akkor mindig újra kell rajzolni.

Ha a rajzolás közben számítjuk a pontokat, az ablak villogni fog, és lassú lesz a program. Ilyenkor érdemes bevezetni a puffert.

int main()
{
  float* puffer[2], f;
  int i;

  puffer[0] = new int[1000];
  puffer[1] = new int[1000];
 
  for (f = -10; f < 10; f += 0.01)
  {
    i = (f + 10) * 100; //az indexet sokszor használjuk, ne kelljen mindig számolni
    puffer[0][i] = f;
    puffer[1][i] = f * f;
  }

  //...a függvény kirajzolása számtalan alkalommal a pufferből...

  delete [] puffer[0];
  delete [] puffer[1];

  return 0;
}

A grafikai résznél később még azt is felhasználjuk, animáláshoz, hogy csak azt a részt rajzoljuk át - ami megváltozott, ezzel is lényegesen gyorsítva azt.

5, A program közzététele

Ha rendelkezünk egy Release fordítású, optimalizált programmal már csak egy dolog maradt - a közzététel.

Erre alkalmas egyrészt a sourceforge.net, ha nyílt kódú megoldásban gondolkozunk, ha pedig nem akkor a saját weboldalunk.

Bármelyiket is választjuk, a közzététel előtt érdemes az alábbiakat elkészíteni:

  • az alkalmazás tömörítése
  • valamilyen súgó
    •  [/?] kapcsolóval szöveges módban
    • .hlp/.chm fájl segítségével grafikus alkalmazásnál
    • egy erre fenntartott külön weboldal (akár Wiki jellegű)
  • egy telepítőkészlet (pl. Inno Setup által)
  • egy hordozható csomag (pl. zip állományban)
  • a forráskód szépen összerendezve, és becsomagolva (pl. tar.gz állományban)

Ha kész vagyunk, a kívánt oldalra feltölthetjük ezután - illetve saját oldal esetén, célszerű egy Google Drive, vagy hasonló dologra feltenni a programokat, és belinkelni.

5a, Az alkalmazás tömörítése

Az elkészített alkalmazások sok esetben elég nagyok is lehetnek, ilyenkor érdemes a Release fordítás mellett tömöríteni is őket.

Erre való az UPX parancssori eszköz. Letöltése után érdemes valamely PATH környezeti változóbeli mappába kibontani, hogy bárhol elérhető legyen.

Használata elég egyszerű, de a súgója felvilágosítást nyújthat még ezen felül is:

b:\Teszt_projekt\bin\Debug>upx "Teszt projekt.exe"

Ultimate Packer for eXecutables
Copyright (C) 1996 - 2013
UPX 3.91w Markus Oberhumer, Laszlo Molnar & John Reiser Sep 30th 2013

      File size      Ratio     Format   Name
-------------------- ------ ----------- -----------
172938 -> 92042      53.22%  win32/pe  Teszt projekt.exe

Packed 1 file.

Jól látható hogy a fájl mérete közel a felére csökkent az UPX hatására, és ezt az arányt képes tartani akkor is, ha a fájl sokkal nagyobb (akár több 100MB).

Talán a nevéből is kitalálható, hogy csak futtatható fájlokhoz jó, így tehát .DLL, .EXE, .OCX fájlokat képes tömöríteni - mást nem nagyon.

5b, A súgó elkészítése

A kapcsolós megoldásról már esett szó, ezért ezt nem részletezném.

A súgófájlok előállításához én személy szerint a HelpMaker 7 alkalmazást ajánlom (ami nyílt kódú). Használata nagyon egyszerű, sok magyarázatot nem igényel - és mind .hlp, mind .chm, és kész weboldal mentésére is képes.

Ha Wiki jellegű oldalt készítünk akkor értelemszerűen nincs szükség külön alkalmazásra.

5c, A telepítőkészlet előállítása

Itt is több lehetőség van, pl. az NSIS, az InstallShield, de én itt a szintén nyílt kódú Inno Setupot tudom ajánlani.

A varázslószerű adatmegadással, pikk-pakk kaphatunk az összes állományt tartalmazó, manapság is divatos telepítőkészletet. A példa telepítők és a program súgója magáért beszél.

Íme egy általa csinált telepítő:

Az elkészített telepítő, beépített eltávolítóval fog rendelkezni, így emiatt sem kell aggódni.

5d, A hordozható csomag/programkód összekészítése

Elsőrendben nem kell mást tenni, csak a programot és a hozzá szükséges fájlokat egy mappába tenni, majd becsomagolni egy .ZIP-be. Ezt akár Windows Intézővel (jobbklikk > Küldés > Tömörített mappa) is megcsinálhatjuk, illetve Total Commander által.

A tar.gz előállítása érdekesebb, de ez sem vészes - a Total Commander erre képes alapból - csak a becsomagolás során át kell állítani a pöttyöt: