9. gyakorlat – 2014. november 12. (függvények) Függvények használata C++ nyelvben
Eddigi gyakorlatunkban bizonyos esetekben nehezen tudtuk elkerülni a kódismétlés jelenségét. Jellemzően, akkor kényszerülünk sok kódmásolásra, mikor be szeretnénk olvasni változóink értékeit, hiszen mondjuk két tömbméretet reprezentáló int típusú változó beolvasása nem sokban tér el egymástól, csak épp a bekérést megelőző kiírás, valamint annak a változónak a neve, amibe beolvasunk jelentik a különbséget. Általánosabb esetben elmondható, hogy az egyes beolvasandó változók különböző típusúak lehetnek, rájuk más feltételek vonatkozhatnak és ezek megsértése esetén más-más reakciót követelhetünk meg a program részéről (pl más-más hibaüzenetet íratunk ki). Ilyenkor nem fogunk tudni egyszerűsíteni. Példa arra, amikor két kódrészlet csak kevéssé tér el egymástól: int n; cout<<”Kerem irja be n erteket!”<<endl; do { cin>>n; hiba = cin.fail() || n<=0; if(hiba) { cout<<”Pozitiv egeszet irj be!”<<endl; cin.clear(); cin.ignore(1000,’\n’); } } while(hiba);
int m; cout<<”Kerem irja be m erteket!”<<endl; do { cin>>m; hiba = cin.fail() || m<=0; if(hiba) { cout<<”Pozitiv egeszet irj be!”<<endl; cin.clear(); cin.ignore(1000,’\n’); } } while(hiba);
Mi a probléma a kódismétléssel?
A kódunk hosszabb lesz, nehezebb lesz átlátni Ha változtatni szeretnénk a kódunkon, az összes ilyen előfordulásnál módosítanunk kell rajta. Hasonlóan, ha a „sablon” hibás, az összes helyen hibás lesz, átírandó Nehéz megállapítani, hogy mely pontokban kell változtatni a „mintához” képest. Ennek tipikus példája lehet az egymásba ágyazott for ciklus, ahol bár a belső ciklus változóját pl. j-nek hívjuk, mégis „megszokásból” i-t növeljük a ciklus végén, ezzel okozva nehezen felfedezhető hibát. Egy másik lehetőség lehet a fenti példa ignore utasítása. Aki nem ismeri a \n jelentését, hiheti azt, hogy a második verzióban \m lenne a helyes. Ez most egy elég szélsőséges példa volt, de az elv kétségkívül létezik. Bár a beolvasandó változóinknak megeshet, hogy semmi közük nincs egymáshoz, de ha így „ömlesztve” használjuk a fentihez hasonló kódrészleteket, akkor ezek a változók akár vétlenül is hathatnak egymásra (pl. a két beolvasás közös „hiba” változót használ. A mellékelt példában ugyan ilyesmi nem tud előfordulni, de ha óvatlanul használunk közös változókat, előfordulhat pl., ha az első beolvasás valahogyan igazzá állítja a hiba változót, akkor a második mondjuk már nem tud lefutni. Holott azt várnánk, hogy a két hiba változó teljesen független egymástól Sok olyan változónk lesz, amit csak ritkán, a kód egy részén használunk, de mégis a teljes program számára látható lesz, ez feleslegesen növeli a bonyolultságát, valamint
veszélyes lehet ha olyan helyen nyúlunk ezekbe bele, ahol nem kéne, vagy ahol nem számítunk rá Összességében tehát elmondhatjuk, hogy ilyenkor logikailag elkülönülő funkciókat fizikailag közösen kezelünk, ezzel hosszú távon megkeserítve saját és munkatársaink életét. A megoldás a függvények bevezetése. A függvény egy paraméterezhető kódrészlet, mely a paramétere „behelyettesített” értéke alapján más és más értéket tud kiszámolni vagy más és más viselkedést képes megvalósítani, de úgy, hogy azonos kód, azonos algoritmus van mögötte. Így, ha javítani akarunk ezen az algoritmuson, netán ki szeretnénk cserélni, akkor egyetlen (ráadásul könnyen beazonosítható) helyen kell csak ezt a változtatást megtennünk. Tegyük fel, hogy ki szeretnénk számolni a szinusz függvény értékét (mondjuk valami közelítő értékkel) 0 és pi/2 helyen. Ekkor ahelyett, hogy a potenciálisan bonyolult kódunkat kétszer egymás után megírjuk, ahol a második esetben valahány helyen át kell írnunk valamit 0-ról pi /2-re (aztán az se biztos, hogy olyan könnyen rájövünk hogy hol!)… ehelyett inkább, mint ha valami matematikai kifejezést használnánk, csak leírjuk, hogy sin(0), illetve sin(pi/2). Persze, az algoritmust se ússzuk meg, meg kell írnunk, ha még senki se írta meg. Nézzünk egy egyszerű példát, pl az abszolútérték-függvényt: Szeretnénk két int típusú változónak értéket adni, az egyikbe kerüljön 5, a másikba pedig -4 abszolútértéke. int a = abs(5); int b = abs(-4); Írjuk meg az abszolútérték-függvényt: int abs(int a) { return a<0 ? –a : a; } Mi történik a kód végrehajtása során? Elvégezzük az abs(5) kifejezés kiértékelését. A fordító talál egy abs nevű függvényt mely egy int típusú paramétert vár és inttel tér vissza tehát úgy veszi, hogy itt ennek a függvénynek a törzsét kell elvégeznie. Mivel a meghíváskori ún. aktuális paraméter értéke 5, emiatt a függvény törzsében található amúgy a-nak nevezett formális paraméter helyébe mindenhol 5-öt ír be1. És az így kapott kifejezéssel returnol, tér vissza, azaz ott fogunk tartani, mintha ezt írta volna be a felső két sor helyett: int a = 5<0 ? –5 : 5; int b = -4<0 ? 4 : -4; Tehát: int a = 5; int b = 4;
1
Vigyázat, ez a két a nevű változó közel sem azonos. Az egyiket a hívó környezetben (mondjuk a main függvényben) hoztuk létre statikus változóként, a másik pedig egy függvény paraméterváltozója, mindegyik csak az őt tartalmazó blokk záró } jeléig él és nevezhető nevén
Függvénydeklarációnak hívjuk az ilyen alakú felírást: double div(int, int); vagy: double div(int a, int b); Egy típusnévvel kezdődik, ez az amit a függvény értékként visszaad (ami kvázi bemásolódik a függvény hívási helyére annak végrehajtásával), aztán jön a függvény neve végül a formális paramétereinek a listája, azok típusaival és opcionálisan nevével megadva. Ez a deklaráció mindaz, amit muszáj tudnunk a függvényről, hogy használni tudjuk. Tudnunk kell mi a neve, hogy milyen típusú paramétereket adhatunk meg neki, és hogy milyen típusú változónak adhatjuk értékül (általánosabban:milyen típusú kifejezésbe írhatjuk bele2). A deklarációban megadott adatokat hívjuk a függvény szignatúrájának. Egy helyes meghívása ennek: int a = 2; double d = div(4,a); A deklarációt folytatólagosan követheti a függvény definíciója, avagy a függvény törzsének megadása. Ez az a rész, ami a { }-ek között van, és amikor a függvényt meghívjuk ez a rész fog szekvenciálisan lefutni. Amikor egy return utasításhoz ér, akkor az az utáni értékkel visszatér a hívás helyére. Az esetleges returnt követő részeket már nem fogja lefuttatni. A definíció külön is megadható. Megtehetjük tehát azt, hogy valahol csak deklaráljuk a függvényt, valahol pedig kifejtjük, hogy mit csináljon, valahogy így: int abs(int a); és int abs(int a) { return a<0 ? –a : a; } Ezt egyébként a változókkal is meg lehet tenni: int a; a = 5; Előrevetett deklarációnak (forward declaration) nevezik azt, amikor a forrásfájlon belül először felsoroljuk a függvények deklarációit, majd csak ezek után, a forrásfájl legvégén a definíciókat. Ezáltal valamivel olvashatóbb lesz a kód, amint megnyitjuk szövegszerkesztővel a forrásfájlt, látjuk, hogy ebben milyen szignatúrájú, milyen nevű függvények lesznek definiálva, és ha a függvények nevei „értelmesek”, ebből ránézésre ki tudjuk találni, ezek a függvények mit is csinálnak, anélkül, hogy átbogarásztuk volna az akár több 100 soros definícióikat.
2
ha van egy bool típusú függvényünk, akkor nem csak azt tehetjük meg, hogy bool típusú változónak értékül adjuk, hanem pl. if vagy while feltételébe és nyugodtan beírhatjuk a függvényhívást… mert az bool típusú kifejezést vár!
Az előrevetett deklaráció másik haszna a következő furcsaság kikerülése: int a(int &i) { if(i==0) b(i); return ++i; } int b(int &i) { if(i==1) a(i); return ++i; } (Az elágazások meg bonyolítások csak azért kerültek bele, hogy ne okozzon egy „végtelen ciklus” jellegű dolgot a függvény meghívása, de a pirossal jelölt részek a fontosak) Az a() függvény meghívná b()-t, a b() függvény pedig meghívná a()-t. Ahhoz tehát, hogy a fordító elfogadja a() definícióját, már tudnia kell hogy van b(). És ahhoz, hogy elfogadja b()-ét, tudnia kell a()-ról. Tehát a()-t hamarabb kell definiálni mint b()-t, és tehát b()-t hamarabb kell definiálni, mint a()-t. Ez így nem megy. De ha előredeklaráljuk mindkettőt, akkor amikor a fordító a piros részekhez ér, „megnyugszik”, hogy lesz majd a forrásfájl későbbi szakaszában ilyen függvény is, hiszen deklarálva is van. Tehát a megoldás: int a(int &i); int b(int &i);
//ennek a kettonek a sorrendje //egymashoz kepest mar mindegy
…
//main fv. meg stb
int a(int &i) { if(i==0) b(i); return ++i; }
//es itt is mindegy a sorrend
int b(int &i) { if(i==1) a(i); return ++i; } Mint láthattuk egy függvényben meghívhatunk más függvényeket. Egy speciális osztályát képezik a függvényeknek a rekurzív függvények. Ezek olyan függvények, amik saját magukat hívják meg, persze más paraméterezéssel, mert ha ugyanolyannal hívnák, akkor egy végtelen hívási lánc alakulna ki, és a program sose érne véget (azaz más szóval: sose terminálna)3. 3
ez nem teljesen igaz, mert előbb bekövetkezne a stack overflow nevű hiba, és a program kilépne. A program futása során a meghívott függvények és a bennük deklarált változók egy verem (angolul stack) nevű adatszerkezetbe kerülnek. Amikor egy
Egy rekurzív függvényben mindig kell lennie egy a „rekurzió végén” esedékes sima visszatérésnek és egy rekurzív hívásnak, mondjuk az összeadásra vonatkozó Peano-axiómák alapján készíthető egy rekurzív függvény…: int osszead(int a, int b) { if(b == 0) return a; return osszead(a,b-1) + 1; }
//feltelezve, hogy a es b is //termeszetes szamok!!! (EF) //rekurzio vege //rekurziv hivas
Nézzünk egy példát: osszead(3,2); osszead(3,1) + 1; osszead(3,0) + 1 + 1; 3 + 1 + 1; 5;
//eredeti kifejezes //rekurziv hivas //meg egy rekurziv hivas //rekurzio vege //sima osszeadas eredmenye
Persze ez nem egy túl hatékony megoldás, de nem is feltétlenül ilyen esetekben kell használni. Már a korábbi órákon is láthattunk függvényeket. A stringet C-típusú stringgé konvertáló c_str() egy függvény. Ennek nincs direktben paramétere, viszont egy „stringen hívjuk meg”, azaz egy string példány után egy ponttal elválasztva kell meghívni, onnan fogja tudni, hogy azt a stringet kell konvertálnia (amúgy ezt a fajta paraméterezést implicit paraméternek hívjuk). Deklarációja ilyen: const char* std::string::c_str() const; A const char* (bármi4 is legyen az) jelenti a függvény visszatérési értéket, a C típusú sztringet. A kékkel jelölt rész adja meg azt a típust, amin a függvény megadható, tehát azt a bizonyos implicit paramétert. A :: jelet scope operátornak nevezik, két féle szituációban kerülhet elő, ez a példa speciel mindkettőre példa. std::akármi azt jelenti, hogy az ún. std névteren (namespace std) belüli valamiről van szó, string::akármi pedig hogy a string típus akármijéről van szó. Tehát itt az std névtér string típusának c_str() nevű paraméter nélküli konstans karaktertömbbel visszatérő értékű függvényéről van szó. Fuck yeah. Ami még szót érdemel az az utolsó const. Az azt jelenti, hogy azon a bizonyos implicit string paraméteren nem fog változtatni a függvény, ez a paraméter konstans. És valóban nem, hiszen a string megmarad ugyanannak, csak éppen a függvény visszatér egy char*-gal, ami ugyanezt a stringet másképp reprezentálja. Az int main() metódusunk is egy függvény! A végén a return 0; azt jelenti, hogy „hiba nélkül” lefutott. Ha a 0 helyett más számot írunk, akkor azzal az szokott lenni a szándékunk, hogy a hívó közegnek (operációs rendszer) jelezzük, valami hiba történt, és a nullától függvény véget ér, akkor kikerül a verem tetejéről, így már az őt hívó függvény fogja a veremtetőt képezni: mindig az aktuálisan aktív függvény adatai lesznek tehát itt. Ha a hívási lánc túl mély (vagy egy függvény túl nagy méretű tárat foglalt), akkor elfogyhat a stackre szánt memória, és bekövetkezik a fenti hiba 4 egy a dinamikus memóriában tárolt karakterekből álló fix értékű tömb, aminek az utolsó eleme egy ’\0’ null karakter
különböző szám alapján azt is meg tudjuk mondani milyen fajta hiba volt ez (a main visszatérési értéke tehát szándék szerint egy hibakód). Egy programban csak egy main függvény lehet, és automatikusan annak végrehajtásával fog kezdődni a program végrehajtása. Az elméletben három fajta függvény-típust tudunk megkülönböztetni, a C++-ban bár nyelvi elemek szintjén nem tudunk direktben különbséget tenni, de mindhármat ki tudjuk fejezni valahogy:
tiszta függvény mellékhatásos függvény eljárás (procedúra)
Tiszta függvénynek nevezzük azt, ami az összes paraméterére nézve konstans, és végrehajtása után visszatér egy bizonyos értékkel. A matematikai értelemben vett függvények (cos(x) és társai) ilyenek, hiszen x-en nem változtatnak, de visszaadnak egy számot. Ilyen függvény pl. a const char* std::string::c_str() const; de mondjuk egy int abs(const int a); is az várhatóan. Persze nyilván ez a függvény törzsétől függ leginkább. Mellékhatásos függvény az, ami mindezeken felül még valami más látványos dolgot is csinál. Vagy módosítja a paramétereit, vagy a globális változókat5, vagy mondjuk kiír konzolra6 valami szívhez szólót. Az ilyenekkel nyilvánvalóan sok dolgot könnyebben ki tudunk fejezni, de mégis veszélyesek, ha nem ésszel használjuk ezeket. Hagyományos statikus tömbök beolvasásának esetében például használatuk megkerülhetetlen (már ha a beolvasást külön függvénybe akarjuk szedni7). Bár tömbbel csak úgy direktben nem lehet visszatérni, de vannak rá eszközök hogy mégis, mindenesetre tömb beolvasáskor két adatnak is értéket adunk, a tömbnek és a méretét megadó számnak, és mivel csak egy dologgal térhetünk vissza, a másikat mellékhatásként kell alakítani. Általában így szoktunk tömböt beolvasni: int tombbe(int t[]); Ekkor a t tömb elemeit amikor megadjuk akkor igazából a tombbe függvény meghíváskori aktuális paraméterébe íródnak be, majd mondjuk érdemes visszatérni a tömbmérettel, amit mondjuk ebben a függvényben kértünk be (ált. n-nek szoktuk nevezni). Fordítva (n-nek adjunk értéket mellékhatásként és a tömbbel térjünk vissza) már kicsit neccesebb a dolog. Egyrészt nem lehet „int[]” egy függvény visszatérési értéke, másrészt ebben a félévben úgy is minden tömböt konstans MAXN mérettel foglaljuk le, tehát sok értelme sincs a tömb lefoglalását külön függvénybe rakni. Következő félévben megismerkedünk a vector típussal, majd 3. félévben a mutatókkal, ezekkel már sokkal kényelmesebben meg lehet csinálni ezt a fajta kódot, ráérünk akkor foglalkozni vele. 5
ha esetleg eddig nem lett volna teljesen világos, hogy miért veszélyes sok globális változót használni, hát ezért: mert boldogboldogtalan változtathat rajtuk és ez egy idő után követhetetlen, valamint az ilyen programoknál a részek egymástól való függősége nagyon nagy, ezért nehezen bővíthetők, nehezen átalakíthatók 6 ez az előző eset része, hiszen a cout is egy globális változó! 7 márpedig úgy menő
De mi van akkor, ha ki akarom számolni egy osztásnál az egész osztás eredményét és a maradékot IS? int div(const int osztando, const int oszto, int maradek) { maradek = osztando % oszto; return osztando / oszto; } Kb. ezt akarnánk csinálni, mégis azt vesszük észre, ha ezt a függvényt meghívjuk: int a = 10, b = 3, c,d; c = div(a,b,d); cout<
Az f objektum mérete több száz megabájt. Egyszerűbb lenne referencia szerint átadni, mert nem akarom egy darab függvény miatt ezt a sok MB-ot feleslegesen másolgatni majd felszabadítgatni. Erre való a konstans referencia. Ugye ha valamit referencia szerint adok át, akkor módosíthatom, cserébe nem kell másolni. Ha viszont konstans, akkor nem módosíthatom. Tehát ha valami konstans referencia, akkor nem kell másolni, de nem is módosíthatom! Pont ezt akarom ilyenkor. int darab(const Fenykepalbum& f) { return f.kepekSzama(); } Maga a const szócska két dolgot csinál. Egyrészt minden függvénytörzsbeli f-re vonatkozó módosítási szándékomra fordítási hibát dob, azaz megvéd attól, hogy olyat csináljak a függvény törzsében, amit a függvény szignatúrájában még nem akartam. Másrészt tippeket ad a fordítónak, hogy hol lehet esetleg hatékonyabbá tenni a programot (optimalizálni). Ebbe mi nekünk már nem kell belelátnunk, a lényeg az, hogy ha egy függvényről tudjuk, hogy valamin nem változtat, mindig írjuk oda a const-ot, mert mind nekünk programozóknak, mind a program használóinak jól jön. De akkor mi volt az előbb a tömbbeolvasással? Ott miért nem kellett a &? Ez a tömb memóriabeli ábrázolása miatt van, a tömbök gyakorlatilag a tömb első elemének memóriabeli címét adják meg. Ezért is van, hogy a tömböt 0-val indexeljük, mert a tömbindex nem más, mint az első elem címétől való eltérés8. Az első elem az első elemtől 0val tér el, a második 1-gyel stb… Tehát a tömb, mivel eleve cím, mindig referencia szerint adódik át, nem kell erről a tényről külön megemlékezni, viszont ha bemenő szemantikája van egy tömb paraméternek, akkor neki is kijár a const jelölés. Általános szabályok a const és a & használatára: parabemenő paraméter méterlis- fontos a kezdeti értéke, ta elemei de nem módosítom beépített const int i típus saját const T& t típus tömb const T t[]
be- és kimenő param. fontos a kezdeti értéke, de módosítom
kimenő paraméter nem használom fel a kezdeti értékét, de módosítom
int& i
int& i
T& t
T& t
T t[]
T t[]
Tehát ha valami nem beépített típus, akkor MINDIG referencia szerint adjuk át. Ha valami bemenő, akkor MINDIG konstansként adjuk át. Tömböt MINDIG referencia szerint ad át, de nem jelöljük. Primitív típusú elemeket, ha módosítani akarok, akkor referencia szerint kell átadnom.
A mellékhatásos függvényeket kivégeztük 8
na, ez se teljesen igaz, ha a tömbben mondjuk intek vannak, és mondjuk az intet 4 byte-on tároljuk, akkor igazából a 2. elem 4, a 3. elem 8 egység messze van az elsőtől. De ezt a fordító mind-mind tudja, mert ismeri a tömb elemtípusát. Ha én ehhez a címhez 1-et hozzáadok, akkor ő igazából sizeof(*t) (3. félévben lesz róla szó, mi ez)-vel beszorozza ezt az egyet, ha 2-t adok hozzá, azt szintén beszorozza vele, stb. Ezt hívjuk pointeraritmetikának.
Utolsó típusunk az eljárás, avagy procedúra. C++ programozási nyelvben erre egy elég fura nyelvi eszköz van, a void visszatérésű értékű függvények lesznek az eljárások. Eljárás az, ami bár mellékhatással bír, de nem tér vissza semmivel. Például egy tömbbeolvasást így is ki lehet fejezni: void tombbe(int& meret, int tomb[]); hívása: int n; int t[MAXN]; tombbe(n, t); Vagy ha ki szeretnék többször is írni egy hosszú szöveget, nem érdemes a forráskód több pontjára is begépelni, inkább használjunk egy eljárást, amit meghívunk: void kiiras() { cout<<”…”; } Ilyen függvények nem térnek vissza semmivel, nem is szükséges return utasítást kiírni, bár lehetséges. De akkor nem szabad semmilyen értéket kiírni mögé, amivel visszatérjen, hanem egyszerűen csak return. Ez azt jelenti, hogy itt a függvény futása megszakad, véget ér, a hívás helyére visszaugrik, és ott folytatódik a program végrehajtása. Lehet egy függvénybe több returnt is írni, de csak az egyik fut majd le, amelyikre hamarabb ráfut. Egy rendes függvénynél viszont garantálni kell, hogy minden lehetséges végrehajtási ág vissza is tér valamivel9. Még érdemes a függvények paraméterezésével kapcsolatban a következőket megjegyezni: Adhatunk a paramétereknek alapértelmezett értéket. Ekkor, ha híváskor nem töltjük azt ki, akkor ez az alapértelmezett érték fog a híváskor behelyettesítődni a formális paraméter helyébe: int rakovetkezo(int a = 5) { return a+1; } cout<
9
//ok, hivhato igy is: egy();
arra nem árt odafigyelni, hogy tapasztalatok szerint a Code::Blocks környezet ezt nem mindig várja el, nem mindig kapunk hibaüzenetet ha elfelejtjük kiírni a returnt, így nagyon csúnya futás idejű hibákat kaphatunk
int ketto(int a, int b = 2);
//ok, hivhato igy is: ketto(1);
int harom(int a, int b=2, int c=3);
//ok, hivhato igy is: harom(1);
int negy(int a=1, int b, int c=3); //forditasi hiba, mert ha negy(4,5)ottel hivom meg, akkor most vajon arra gondoltam, hogy a opcionalis parameternek adtam a 4-es erteket es b kotelezo parameternek az 5-ost, avagy b-nek az 4-est es c-nek az 5-ost? Megtehetjük azt is, hogy a függvényeinket különböző típusokkal paraméterezzük, esetleg más-más mennyiségű paramétert használunk (erre az előző is egy példa volt!). Függvénytúlterhelésnek hívjuk azt, amikor több azonos nevű, de paraméterezésében eltérő függvényt adunk meg. Ez inkább csak a programozó számára hasznos, a fordító teljesen külön függvényként fogja értelmezni a másképp paraméterezett változatokat. int max(int a, int b); int max(int a, int b, int c) { return max(max(a,b),c); }
//ket szam maximuma //harom szam maximuma, //amit amugy visszavezethetunk //ket szam maximumara
double oszt(int a, int b); //ne csak egeszeket oszthassunk maaa el… double oszt(double a, double b); int egy(int a, int b); int egy(int a) { return egy(a,0); }
//ez vegso soron egy pelda //az alapertelmezett ertekre!
Ez nem egy helyes túlterhelés, hiszen csak visszatérési értékében tér el: int a(int a, int b); bool a(int a, int b); Ez azért problémás, mert a függvény hívásánál nem biztos, hogy egyértelmű lesz, hogy melyiket hívtuk. Egyáltalán nem elvárás ugyanis az, hogy a visszatérési értékkel (is) rendelkező függvényeket olyan körülmények között hívjuk meg, hogy a visszatérési értéküket fel is használjuk. Mert ez első kettő még egyértelmű lenne, de a harmadik nem: int sz = a(1,2); bool l = a(3,4); a(5,6);
//intes verziora gondolhattunk //boolosra gondolhattunk //???
Egy függvény, mint láttuk return értékként egy féle adatot tud csak visszaadni. Azt is láttuk, hogy tömböt csak úgy nem tud. Ha mégis tömböt, vagy több féle dolgot akarok visszaadni, akkor létre kell hoznom egy olyan típust, amibe bezárom ezeket a típusokat és ezzel térek vissza. Elég nehézkes megoldás. Így egyébként áldinamikus tömbbeolvasást is meg lehet valósítani (tehát amikor nem const MAXN méretű egy tömb tényleges mérete, hanem bekéréstől függő), de ezt inkább ne erőltessük, következő félévben jön az std::vector, amiben ez alapból benne van, 3. félévben pedig a mutatók, amivel normális, dinamikus memóriakezelést lehet megvalósítani nagyon egyszerűen. Kitartás.