A program fő függvénye, és a rendszer közötti kapcsolat


  1. A polimorf (többalakú) main függvény
  2. A környezeti változók
    1. A PATH környezeti változó
    2. Az errorlevel környezeti változó
    3. A környezeti változók lekérése - getenv
    4. A környezeti változók írása - putenv
  3. A program visszatérési értéke
    1. A hibakezelő függvény(ek)
    2. Az exit utasítás
    3. Minek a hibakezelés?
  4. A program paraméteres futtatása
    1. Minek egy grafikus programnak a paraméteres futtatás?
    2. A paraméterek típusai
    3. A bővebb main függvény
  5. Egyéb főprogram függvények
    1. A főfüggvény belépési pontja
    2. A függvény(mutatók) paramétereinek elhagyása

1, A polimorf (többalakú) main függvény

Remélhetőleg mire ehhez a cikkhez eljutunk, már ismerjük a main függvény alábbi alakját:

int main()
{
  return 0;
}

A legtöbb program elkészítése során ezt a változatot használtuk - ugyanakkor már ezen az alakon is érdemes néhány kérdést feltenni.

Mint ezt már tudjuk, a program futtatásakor a main függvény fog lefutni elsőként - esetleg a mainben meghívott további alfüggvények.

Feltehetően arra is rájöttünk hogy a return 0; sor szinte mindig megtalálható volt a main függvényben, és arra is hogy ez a program leállást okozza.

Az első kérdés tehát az lehet, hogy miért kell visszatérési érték a program fő függvényének?
Erre a későbbiekben választ fogunk kapni. 

Mielőtt azonban megkeresnénk erre a kérdésre a választ, bemutatnám a main függvény egy bővebb alakját:

int main(int argc, char* argv[])
{
  return 0;
}

A második kérdés egyértelműen az lehet, hogy mik ezek a plusz paraméterek, és honnan kapnak értéket?

Végül pedig, mivel három a magyar igazság - honnan fogja tudni az operációs rendszer, hogy hol lesz a main függvény a program lefordított változatában?

Ahhoz hogy erre a három kérdésre válaszokat kapjunk, egy kicsit ismerkedni kell az operációs rendszerrel, és az általánosíthatóság kedvéért, a karakterláncos részhez hasonlóan itt is az MS-DOS lenne a tesztalany.

2, A környezeti változók

Feltehetően ez a fogalom újdonság sokaknak, de a dolgok túlcizellálásának elkerülése végett csak röviden írnám le.

Arra valószínűleg gondolhatunk, hogy maga az operációs rendszer is egy program, és ezáltal rendelkezik változókkal. Ahhoz hogy testreszabható legyen a működése, ezen változók egy része átírható kell legyen a felhasználók által is.

A környezeti változók, olyan változók amik programtól függetlenek, és az operációs rendszer kezeli őket. Főként szöveges változókról beszélhetünk, de esetekben lehet szám típusú is.

Természetes konkrét példa nélkül, ez így lóg a levegőben, úgyhogy érdemes egy kicsit megnézni ezt.

Feltehetően legtöbben Windows-t használunk, és legtöbbünk találkozott már az ún. "AppData" mappával - a legtöbb játék ide teszi a játékállást, így egy újratelepítés előtt mindig érdemes ezt lementeni. Aki Minecraftot használt már, az pontosan tudja miről van szó, ide kell a textúracsomagokat, stb. másolni.

A kérdés az alábbi - honnan tudják a játékok, hogy hol helyezkedik el ez a mappa a számítógépen?

Ahhoz hogy erre választ kapjunk írjuk be az alábbi sort a Futtatásba (Win+R):

Amennyiben lenyomjuk az OK gombot, megnyílik ez a mappa - a böki csak az hogy mi nem ezt írtuk be a Futtatás ablakba:

A válasz a feltett kérdésre egyszerű - az "appdata" egy környezeti változó, amely nálam éppen a "C:\Users\Laci bá'\AppData\Roaming" értékkel bír. Hogy erről megbizonyosodjunk nyissunk egy Parancssort:

A karakterláncok kapcsán már találkozhattunk az echo paranccsal, őt fogjuk arra használni hogy megkapjuk a környezeti változók értékét a továbbiakban:

C:\Users\Laci bá'>echo %appdata%
C:\Users\Laci bá'\AppData\Roaming

A kérdés a következő lehet - mi is tudunk definiálni környezeti változókat?
Természetesen igen, a set parancs segítségével:

set <név>=<érték>

Íme erre egy példa:

C:\Users\Laci bá'>set változó=Ez egy teszt szöveg.

C:\Users\Laci bá'>echo %változó%
Ez egy teszt szöveg.

A környezeti változók elnevezésére, a példából látható módon - nem vonatkoznak a C/C++ nyelvben megismert korlátozások, és nincs megkülönböztetve sem a kis és nagybetű.

A set parancs alkalmas meglévő változók átírására is, így ideiglenesen az appdata értékét is átírhatjuk vele:

C:\Users\Laci bá'>set appdata=D:\AppData

C:\Users\Laci bá'>echo %appdata%
D:\AppData

Az átírás következményeitől nem kell aggódni, mihelyst kilépünk a Parancssorból, elvesznek a módosítások - a végleges átíráshoz az alábbi helyen találjuk ezt az ablakot:
Sajátgép > jobbklikk > Tulajdonságok... > (Speciális rendszerbeállítások) > Környezeti változók

Megjegyzés: A környezeti változók egy része az ablakból látható módon felhasználói fiókonként változhat - emiatt egy rendszergazdai parancssorban az echo %appdata% parancs már más eredménnyel fog szolgálni.

2a, A PATH környezeti változó

Arról már talán esett szó a fájlnevek kapcsán hogy mit értünk teljes elérési útnak, illetve rövid fájlnévnek.

Teljes elérési út alatt az olyan fájlnevet értjük, ahol az elérési út is meg van adva:
    pl. C:\Windows\System32\mspaint.exe

Rövid fájlnév alatt olyan fájlnevet értünk, ahol az elérési út (futtatható fájl esetén akár a kiterjesztés is) elmarad:
    pl. Teszt.txt, vagy mspaint

Arról is szó esett, hogyha beírunk egy fájlnevet, pl. mspaint.exe - akkor azt a rendszer először az aktuális mappában fogja keresni, illetve még sok helyen.

Ez a sok hely lesz a PATH környezeti változó - ha az aktuális mappa nem játszik, akkor szépen végignézi a PATH változóban felsorolt mappákat, ahol lehet hogy megtalálja. Ez egy egyszerűbb gépen:

C:\Users\Laci bá'>echo %path%
C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;%%USERPROFILE%\AppData\Local\Microsoft\WindowsApps; 

Ebből az fog következni, hogy mivel pl. a C:\Windows\System32\ mappa is része a PATH változónak, bármilyen - az ebben a mappában lévő programot futtathatunk a rövid nevével is - anélkül hogy ki kéne írni a teljes elérési utat.

Ezt az állítást könnyű letesztelni - írjuk be a Futtatásba hogy "mspaint":

 

Ennek hatására meg fog nyílni a Paint, aminek a teljes elérési útja amúgy C:\Windows\System32\mspaint.exe.

2b, Az errorlevel környezeti változó

Sok esetben egy program, mint ezt már a C/C++ esetében megszokhattuk csak úgy random összeomlik. Ilyenkor sok esetben nem lehetünk benne biztosak hogy mi okozta a hibát, de a programozója az alkalmazásnak felvilágosíthat minket ha ismerünk néhány adatot.

Teljes körű hibakeresést természetesen nem fogunk tudni végezni a programján, de ha ő ügyesen programoz, jó esetben el tudunk kapni egy hibakódot.

Azt hogy mi volt az a hibakód, azt a program futása után az errorlevel környezeti változóban találjuk meg - ha nem volt hiba az értéke mindig nulla, egyébként pedig ettől eltérő szám.

Erre is van egy egyszerű kis parancssori példa:

C:\Users\Laci bá'>copy Teszt.txt E:\Teszt.txt
    1 fájl másolása megtörtént.

C:\Users\Laci bá'>echo %errorlevel%
0

C:\Users\Laci bá'>copy Teszt.txy E:\Teszt.txt
A rendszer nem találja a megadott fájlt.

C:\Users\Laci bá'>echo %errorlevel%
1

Talán ezek után kitalálható az is, hogy ezt a hibakódot nem a rendszer fogja képezni, hanem maga a parancs, illetve program, attól függően hogy a futása hogyan sikerült.

2c, A környezeti változók lekérése - getenv

char* getenv(char* name);

name: a környezeti változó neve (pl. "appdata", "temp", "windir", "systemroot", ...)

A visszatérési értéke a környezeti változó értéke ha sikeres a művelet, ugyanakkor ha viszont nincs ilyen nevű változó akkor NULL.

2d, A környezeti változók írása - putenv

int putenv(char* set_string);

set_string: az átírást jelentő szöveg, hasonlóan a set parancshoz <név>=<érték> alakban

A visszatérési értéke nulla ha sikeres a művelet, egyébként egy nem nulla érték.

3, A program visszatérési értéke

A program visszatérési értéke nem lesz más, mint az a hibakód/kilépési kód amit a rendszer felé fog jelezni. A return 0; sornak ezek után a programban érthető módon fontos szerepe lesz, hiszen ennek hiányában a rendszer random számot fog kapni hibakódként - ezáltal minden alkalommal úgy fogja észlelni hogy a futás során hiba történt.

A parancssori példánkban ez le lett tudva egy kiírással, de grafikus esetben akár hibaüzenetet is kaphatunk pl. Total Commander alóli futtatás esetén, ha a hibakód eltér nullától. 

Ezek után, talán megfontolás alá vehető a programhibák esetén egyéb kilépési kódok bevezetése is, mint az 1, 2, stb. - azokra az esetekre ha hiba történik.

Ezt az alábbi rövid példa mutatja be: 

#include <stdio.h>

int
main()
{
  int i, j;
  printf("Kerek egy szamot: "); scanf("%d", &i);
  printf("Kerek egy masik szamot: "); scanf("%d", &j);
  printf("A hanyadosuk: %f", (float)i / j);
  return 0;
}

Ez az egyszerű program elég sebezhető, ezért végig kell gondolni, hol lehet összeomlasztani.

  1. Ha a számnak begépeljük hogy "Az eg kek." jó esélyünk lesz rá - legjobb esetben csak hülyeség lesz az eredmény.
  2. Ha a j == 0, akkor nullával való osztás miatt garantáltan össze fog omlani a program.

A következő lépés hogy csoportosítsuk a hibákat, én az alábbi csoportosítást választottam:

  1. beviteli hiba - érvénytelen begépelt értékek esetére
  2. nullával való osztás

Ezeket a csoportokat, az előttük látható sorszám alapján különböző hibakódokkal fogom ellátni:

#include <stdio.h>

int
main()
{
  int i, j, k;
  printf("Kerek egy szamot: "); k = scanf("%d", &i);
  if (k < 1 || k == EOF)
    return 1;

  printf("Kerek egy masik szamot: "); k = scanf("%d", &j);
  if (k < 1 || k == EOF)
    return 1;

  if (j != 0)
    printf("A hanyadosuk: %f", (float)i / j);
  else
    return 2;

  return 0;
}

Ebben az esetben ha hiba történik, ha nem is közli ezt velünk a program - minimális életjelet ad a rendszer felé az errorlevel környezeti változón keresztül.

3a, A hibakezelő függvény(ek)

Mivel jól látható hogy sok másolás történt mindenféleképpel javallom a hibakezelő függvény(ek) bevezetését, ami több szempontból is előnyös:

  • mint említettem korábban a dinamikus szerkezeteket mindig fel kell szabadítani kilépés előtt - ezt elvégezheti

  • különféle hibaüzeneteket írhat ki a hibakimenetre kilépés előtt

  • és ha ez mind megvan akkor megszakítja a program futását

Ez már egy elég barátságos megoldás, és a kivitelezése sem sokkal bonyolultabb, íme:

#include <stdio.h>

int hiba(const int ertek)
{
  fprintf(stderr, "Hiba: ");
  switch (ertek)
  {
     case 2: fprintf(stderr, "nullaval valo osztas tortent.");
             return 2;
     case 0:
     case EOF:
             fprintf(stderr, "ervenytelen a bevitt adat.");
             return 1;
     default:
             return 0;
  }
}

int main()
{
  int i, j, k;
  printf("Kerek egy szamot: "); k = hiba(scanf("%d", &i));
  if (k != 0) return k;

  printf("Kerek egy masik szamot: "); k = hiba(scanf("%d", &j));
  if (k != 0) return k;

  if (j != 0)
    printf("A hanyadosuk: %f", (float)i / j);
  else
    return hiba(2);

  return 0;
}

A program valljuk be nem lett egyszerűbb - ami azért van, mert a fölösleges if szerkezeteket meg kellett tartani, mivel nem ismerjük azt hogy egy függvény miként szakíthatja meg a program futását.

Megjegyzés: A break sorok direkt maradtak el, mivel a return úgyis megszakítja a függvény futását az adott pontban.

3b, Az exit utasítás

Az exit utasítás a program bármelyik pontjából megszakítja annak futását, a megadott hibakóddal:

void exit(const int exitcode)

exitcode: a kilépési kód

Az eljárásnak nincs visszatérési értéke.

Fontos: Az exit használata előtt mindig szabadítsunk fel, minden korábban lefoglalt dinamikus összetevőt (amelyik igényli ezt).

Most hogy ismerjük ezt is, a fenti példa lényegesen egyszerűsíthető:

#include <stdio.h>

void hiba(const int ertek)
{
  fprintf(stderr, "Hiba: ");
  switch (ertek)
  {
     case 2: fprintf(stderr, "nullaval valo osztas tortent.");
             exit(2);
     case 0:
     case EOF:
             fprintf(stderr, "ervenytelen a bevitt adat.");
             exit(1);
  }
}

int main()
{
  int i, j;
  printf("Kerek egy szamot: "); hiba(scanf("%d", &i));
  printf("Kerek egy masik szamot: "); hiba(scanf("%d", &j));

  if (j != 0)
    printf("A hanyadosuk: %f", (float)i / j);
  else
    hiba(2);

  return 0;
}

Ez a program már egészen barátságos, mind a kódjának átláthatóságában, mind viselkedésében:

B:\>Teszt
Kerek egy szamot: alma
Hiba: ervenytelen a bevitt adat.
B:\>echo %errorlevel%
1

Ez a program a copy parancshoz hasonló viselkedést mutat hibakód alapján már, és a kódja lényegesen nem hosszabb az eredetinél.

3c, Minek a hibakezelés?

A hibakezelés plusz kóddal jár, és lényegében nem ad sokat a programhoz - elsőre ezt gondolnánk.

Az első ok amiért érdemes hibakezelésben gondolkozni, hogy a dinamikus változókat felszabadítsuk. Ez egy nagyon nyomós ok, erről korábban leírásra került hogy nagyon fontos.

A második ok, hogy a felhasználó tudni fogja hogy miért szakadt meg a program, és erről egy barátságos hibaüzenetet fog kapni.

A harmadik ok, hogy ezek a hibaüzenetek nekünk is segítséget jelenthetnek, hiszen nem az lesz hogy oké kilépett ... hol a hiba? történet, hanem legalább egy támpontunk van. Ezt mind az üzenet, mind a hibakód erősíti.

A negyedik ok, hogy a felhasználó, ha nem is a hibakódot - de az üzenetet elküldheti a programozónak, ezáltal is segítve a fejlesztést.

Az utolsó ok pedig hogy a programunk programozhatóvá válik. Ez az operációs rendszerrel való együttműködés gyümölcsöző kapcsolat, hiszen elsőre nem is gondolnánk - de a parancssor is programozható - ennek az egyik módja az errorlevel felhasználása - pl.:

B:\>if %errorlevel%==0 echo Minden rendben történt!

Ennek természetesen nem itt van jelentősége, hanem kötegfájlokban (.BAT, .CMD). Ezek a fájlok olyan szövegfájlok, amelyek DOS parancsokat tartalmaznak begépelve, és futtatható programként jelennek meg a rendszerben.

Elindításukkor ezek a parancsok sorban lefutnak, és ilyenkor ezek a szerkezetek meghatározóak lehetnek - mivel pedig a mi progink is bír normális hibakóddal, ezért beépíthető ilyen fájlokba.

A hibakód leírásánál már említettem hogy sok grafikus program is felhasználja, pl. a Total Commander, nullától eltérő hibakód esetén "Programfuttatási hiba" hibaüzenetet dob fel.

4, A program paraméteres futtatása

A Parancssor használata közben bizonyára észrevehettük, hogy néhány parancs adatokat kér be, mint pl. a másolás parancsa:

copy <forrás> <cél>

Ezeket a bekért adatokat neveztük paramétereknek a függvények esetében, és analóg módon itt is - de most az egész programra értelmezve.

4a, Minek egy grafikus programnak a paraméteres futtatás?

Első pillanatra talán bevillanhat, hogy teljesen felesleges egy grafikus alkalmazást paraméterezni - hiszen a Paintet sem Parancssorból szoktuk elindítani. Ez való igaz, de a paraméterezésnek a Windows alatt is megmaradt a szerepe.

Hogyan nyitunk meg a Paint segítségével egy képfájlt?

  1. A .bmp fájlok társítva vannak hozzá, így kétszer rákattintva megnyílik a Paint, és betölti a képet
  2. Ráhúzzuk a Paint ikonjára a képfájlt

A kérdés az az lehet hogy miként tudja meg a Paint, hogy mit kell megnyitnia - erre pedig a paraméterek lesznek a válaszok. Próbáljuk ki, az alábbi példát:

  1. Mentsünk el egy .BMP fájl valami egyszerűen elérhető helyre (pl. D:\Teszt.bmp)
  2. Írjuk be a Futtatás ablakba ezt mspaint "D:\Teszt.bmp"
  3. A futtatás után meglepő módon megnyitja a képet

Ebből azt lehet leszűrni, hogy a Windows a különféle társításokat paraméterezés útján végzi el - de ezt elrejti előlünk. Ez magyarázatot ad arra hogy minek kell paraméterezni manapság is a programot.

Természetesen ez azt is jelenti hogy a grafikus alkalmazások, nagyjából mindig a megnyitandó fájl nevét kapják meg paraméterben (ugyanakkor lehet több fájl is, ha sokat húzunk rá az ikonjára - ilyenkor mindegyik külön paraméter).

4a, A paraméterek típusai

A paraméterek alapvetően kétfélék lehetnek:

  • kötelező: mindig meg kell adni (mint a copy parancsnál a forrás és a cél)
  • opcionális: olyan adatok, amiket nem kötelező megadni
    • opcionális érték: olyan paraméter ami értéket ad meg, pl. számot, fájlnevet, stb. - de nem kötelező megadni
    • kapcsoló: olyan paraméter ami a program futását módosítja, de szintén elhagyható
    • alternáló kapcsolók: olyan kapcsolók, amik közül egyszerre csak az egyik adható meg

A kapcsolókkal még nem foglalkoztunk, ezért röviden bemutatnám őket.

A kapcsoló mindig úgy néz ki hogy "-S", "/S", vagy "--switch" alakban fogadja a program. Az utolsó inkább Unix alapú rendszereknél megszokott, míg az első kettő inkább Windows esetére. Általában egy angol szó kezdőbetűjét takarja a betű, és állhat mögötte más adat is, pl.: "/N=1024".

Íme erre egy gyors példa. A format paranccsal már megismerkedtünk, de ha szeretnénk egy MS-DOS rendszert tartalmazó, gyorsformázással ellátott floppy lemezt készíteni az alábbi utasítást kell kiadnunk (legfeljebb Windows XP alatt):

format A: /q /s

Ahol az "A:" kötelező paraméter, a meghajtó betűjele, míg a másik kettő kapcsoló (így opcionálisak). Az első kapcsoló a quick szót takarja, míg a másik a system szót jelöli.

A legtöbb szöveges módú alkalmazás rendelkezik egy "-h", "-?", stb.. kapcsolóval, ami megjelenít egy súgót, hogy milyen paramétereket vár a program. Példa:

B:\>echo /?
Üzenetek megjelenítése és a parancsmegjelenítés (echo) be- vagy kikapcsolása

ECHO [ON | OFF]
ECHO <üzenet>

A beállítások megjelenítéséhez írja be az ECHO parancsot paraméter nélkül.

Ezekben a súgókban a kötelező paramétereket általában <...> jelek közé teszi, míg az opcionálisakat [...] jelek közé szokták tenni. Ha alternáló kapcsoló van akkor [ ... | ... | ... ] megoldással szokták jelölni. 

4b, A bővebb main függvény

Hogyha ilyen paramétereket bármikor kaphat a programunk, akkor érdemes felkészíteni azok fogadására.

int main(int argc, char* argv[])

argc: megadja az argv tömb hosszát
argv: tartalmazza a program teljes elérési útját (argv[0]), és az esetlegesen beírt paramétereket (argv[1]..argv[argc-1])

A program fő függvényének visszatérési értéke a kilépési kód lesz.

Megjegyzés: Amint látható, a paraméterátadás mindig szöveges, ezért ilyenkor érdemes bevetni a szövegkonverziós utasításokat, illetve az sscanf utasítást is.

Példa:

#include <stdio.h>

void help()
{
  printf("Tetszoleges mennyisegu szam osszeadasa\n\n");
  printf("TESZT <szam1> <szam2> [szam3] ... [szamN]\nTESZT [/?]\n\n");
  printf(" /?\tMegjeleniti ezt a sugot.\n\n");
  printf("A program kapcsolok nelkuli futtatasa is ezt a sugot jeleniti meg.");
  exit(0);
}

int main(int argc, char* argv[])
{
  if (argc <= 1 || (argc >= 2 &&
    (strlen(argv[1]) >= 2 ? argv[1][1] == '?': 0))) help();

  float szumma = 0, szam;
  int i;
  for (i = 1; i < argc; i++)
    if (sscanf(argv[i], "%f", &szam) == 1)
      szumma += szam;


  printf("A szamok osszege: %.3f\n", szumma);

  return 0;
}

A program első lépésben megvizsgálja hogy hány paraméter lett megadva:

  • ha egy sem (argc <= 1), akkor megnyitja a súgót - ami majd be is fejezi a program futását
  • ha legalább egy meg van adva (argc >= 2), akkor:
    • megnézi hogy a megadott paraméter van-e két karakter hosszú legalább (strlen(argv[1]) >= 2)
      • ha igen, és a második karaktere kérdőjel (argv[1][1] == '?') akkor megnyitja a súgót
      • ha nem, akkor a program fut tovább

Ezek után for ciklus segítségével végighalad az összes paraméteren, és megpróbálja értelmezni azokat az sscanf által. Hogyha a beírt adat érvényes, akkor hozzáadjuk az összeghez.

Az összeadás végeztével kiírja az eredményt is, és jelezve hogy nem történt hiba kilép.

A program futásának eredményei:

B:\>Teszt -?
Tetszoleges mennyisegu szam osszeadasa

TESZT <szam1> <szam2> [szam3] ... [szamN]
TESZT [/?]

/? Megjeleniti ezt a sugot.

A program kapcsolok nelkuli futtatasa is ezt a sugot jeleniti meg.
B:\>Teszt 1 5 1 3 14.2 0.8
A szamok osszege: 25.000
B:\>Teszt sajt 1 5 1 3 "Az eg kek" 14.2 0.8 d
A szamok osszege: 25.000


Két kérdés merülhet fel a programmal kapcsolatban, az első hogy hol van a hibakezelés, a másik pedig hogy ez a kérdőjeles megoldás nem-e túl engedékeny?

Az első kérdésre meg kell nézni a kritikus szakaszokat.

Egyik ilyen a paraméterek ellenőrzése - de mivel a számukat is figyelembe vesszük mielőtt hivatkozunk rájuk + a hosszukat is megnézzük mielőtt a második karakterhez nyúlnánk ez biztonságos. Ha nem felel meg valami, egyszerűen nem teljesül a feltétel.

A második kritikus szakasz, ha az sscanf nem talál számot a paraméterben, de akkor sincs gáz - mert nem írjuk ki hogy "hülyeséget adtál meg", nem akasztjuk meg a programot - a program figyelmen kívül fogja hagyni a hülyeséget, de a lényeget elvégzi (lásd az eredményeknél).

Ezek a fentebb bemutatott hibakódos, leakasztós megoldásnál egy kicsit intelligensebb, bár bizonyos esetek nem engedik meg ezt - olyankor szükséges a leakasztás - a két számot elosztó feladat erre jó példa.

A második kérdésre a válasz az, hogy igen, engedékeny.

Ha belegondolunk annyi volt az egyetlen feltételünk hogy vagy ne legyen paraméter, vagy ha van, akkor az első legyen legalább kétbetűs, ahol a második "?". Így akkor is kiírja a súgót ha az alábbi parancsokat adjuk ki:

Teszt /?
Teszt -?
Teszt &?!?
Teszt h?alma

Ugyanakkor ez felhasználói szemmel, véleményem szerint nem fog feltűnni - a forráskód ismerete nélkül, és programozási tudás nélkül kétlem hogy valaki direkt erre próbálgatná a programot - ezáltal ez szerintem vállalható kompromisszum.

5, Egyéb főprogram függvények

Bizony, bizony, előállhat olyan eset is amikor a megszokott main függvénytől el kell búcsúzni, és más variációkat kell kipróbálni.

Amennyiben Windows alapú grafikus alkalmazást szeretnénk írni, mindenféleképpen át kell térni a WinMain, illetve wWinMain (Unicode) utasításra, ami olyan adatokat fog megadni, amik az ablakok létrehozásához kellenek.

Szintén egy másik variáció, ha .DLL fájlt (dinamikus csatolású függvénytár) szeretnénk írni, ekkor a modul jellege miatt a DllMain utasítás lesz az emberünk Windows alatt.

Amennyiben foglalkoztunk már mechatronikával, lehet ismerős az Arduino nevű vezérlőeszköz - ez a kis eszköz elektronikai vezérléseket valósíthat meg, és a programját C++ alatt kell megírni - itt két fő függvény is van a loop és a setup.
Ezek közül az második lefut egyszer, a szerkezet bekapcsolásakor, és miután lefutott a loop utasítás fog körbe körbe futni.

A kérdés az lehet, hogy honnan tudja a rendszer azt hogy ez a sokféle függvény hol helyezkedik el a fájlban?

5a, A főfüggvény belépési pontja

A függvénymutatókkal való barátkozás során már felmerült az, hogy a függvényeknek is van memóriacíme, és ha ismerjük a paraméterlistát akkor összességében ez által futtatható.

Az főfüggvény belépési pontja alatt azt a címet értjük, ahol az adott programhoz tartozó fő utasítás kezdődik. Ha itt átadjuk a szükséges paramétereket, a program indításra készen áll - már csak el kell indítani a főfüggvény futtatását (cím szerint).

A belépési pont abban különbözik a függvénymutatótól, hogy ez az állományra vonatkozik - itt kell belépni az állományba, és megkezdeni a futtatását.

Egy állománynak több is lehet, attól függően hogy mit szeretnénk elérni. A mi esetünkben ez mindig egy volt, ami a main függvény címe.

Ugyanakkor ha megnézzük az Arduino esetét, ott mindjárt kettő ilyen fő belépési pont lesz, illetve .DLL fájlok esetén egy fő belépési pont van, és számtalan egyéb belépési pont (ott egy ún. "export tábla" írja le ezeket).

Az hogy megkeresse a fő függvényt, és a megfelelő paramétereket adja át neki, az az operációs rendszer feladata lesz, következésképpen megkönnyíthető a dolga ha erről információkat szolgáltatunk neki. Ezeket az adatokat a fordító az állományba beleteszi automatikusan, számunkra teljesen automatizált a dolog.

Ha a rendszer ismeri az állomány jellegét, akkor nem kell kereseni a főfüggvényt, mert az állomány úgy lesz összeszerkesztve hogy egy adott helyen helyezkedjen el ez a belépési pont - ami főfüggvényenként változhat.

Egy szöveges módú alkalmazásnál a Windows egyértelműen mindig a main() függvényt fogja keresni, illetve egy grafikus módú .EXE fájlnál mindig a WinMain() függvényt fogja keresni, az előre megadott helyen.

5b, A függvény(mutatók) paramétereinek elhagyása

Erről még nem esett szó, de remek kérdés hogyha a main függvény teljes alakja legalább két paraméterből áll, akkor miért hagyhatjuk el ezeket?

Először is azt kell elfogadni, hogy a visszatérési érték az mindig különáll a többi paramétertől - ezért ha van az nem hagyható el, legfeljebb ekvivalens méretű típus választható helyett (pl. short int helyett wchar_t).

A másik dolog a bemenő paraméterek listája. Itt az a helyzet hogy a megkapott értékeknek legalább annyinak kell lennie (lehet több is), mint amennyit a függvény valójában bekér. Ha többet kérünk be, mint amennyit megadunk, akkor összeomlással kell számolni.

De a szuper kérdés lehet, hogy most ez darabszámra értelmezett dolog, vagy micsoda? A válasz az hogy byteokra van értelmezve, így a hivatalos main() bemenő paraméter listája (32-biten):

relatív cím 0 1 2 3 4 5 6 7
típus int (4 byte) karaktermutató (4 byte)

Természetesen ez azt is jelenti, hogyha ebbe a táblázatba más módon helyezzük el a típusokat, pl. a int helyére 4db char-t, az is megfelel hiszen méretében ekvivalens.

Íme ezáltal a main függvény néhány szintén elfogadott variációja (a teljesség igénye nélkül):

int main();
int main(void);
int main(int argc);
int main(int argc, char** argv);
int main(int argc, char* argv[]);
unsigned int main();
unsigned int main(void);
unsigned int main(unsigned int argc);
unsigned int main(unsigned int argc, char** argv);
unsigned int main(unsigned int argc, char* argv[]);

A fentebb leírt típusok mind 100%-ban kompatibilisek egymással, mivel a felsorolt variációk mind azonos méretű változókat használtunk. Az esetleges unsigned int - int konverzió okozhat meglepetéseket, mint az az "Adattárolás kettes számrendszerben" című részben ki lett tárgyalva.

Mivel az operációs rendszer a main() függvényt egy függvénymutató által éri el, a C/C++ nyelvben ezek minden függvénymutatóra igazak lesznek. pl.:

typedef int (*TFuggveny)(int x, int y);

int fv1(int a, int b)
{
  return a / (b != 0 ? b : 3);
}

unsigned int fv2(unsigned int a)
{
  return a * 2;
}

int
fv3(char a1, char a2, char a3, char a4)
{
  return (a1 + a2 + a3 + a4) / 4;
}

int main()
{
  TFuggveny p = &fv1;
  int i = p(3, 2); //i = 3 / 2 = 1
  p = &fv2;
  int j = p(3, 2); //j = 3 * 2 = 6
  p = &fv3;
  int k = p(3, 2); //j = (0 + 0 + 0 + 3) / 4 = 0


  return 0
}

Mivel néha a regisztereken keresztül kapja meg a függvény a paramétereket, nem érdemes nagyobb típusokká alakítani sok kicsit, mivel azok lehet hogy már nem jelennének meg pl. egy struktúrában, vagy unionban - még akkor sem ha elvileg memóriabeli elrendezés alapján rendben lenne.

Ilyen alapon, ha a függvénymutató 4 db char típust ad át, az már nem sűríthető egy intté, a regiszteres átadás miatt - ebből az következik, hogy az átadott típusok darabolhatóak, de nem összegezhetőek.

#include <stdio.h>

typedef
int (*TFuggveny)(char a1, char a2, char a3, char a4);

unsigned int fv(unsigned int a)
{
  return a * 2;
}

int main()
{
  TFuggveny p = &fv;
  int i = p('\0', '\0', '\0', '\3'); //i = 0x00000003 * 2 = 6

  printf("%d", i);

  return 0;
}

Ebben a példában a program jó esetben hülyeséget fog kiírni, rossz esetben összeomlik - tehát semmiféleképpen sem a várt hatos fog megjelenni a képernyőn.

A zöld példa célja az volt hogy bemutassa a darabolás működőképességét, míg a pirosé az összegződés működésképtelenségét.


Megjegyzés: Habár nem szabványos, de a rendszerek nagy részén elérhető a main függvény esetében egy harmadik paraméter is, ami a környezeti változókat adja át:

unsigned int main(unsigned int argc, char* argv[], char* envp[]);

Ha mindenféleképpen szükségünk van a környezeti változókra inkább használjuk az erre beépített függvényeket - ez a main függvény pl. néhány Linux verzió alatt hibát okozhat.