fel
le

Kimenő paraméterek

Kérdés: tudunk-e a paraméterváltozókon keresztül értéket visszaadni?

Erre sajnos a C#-ban nem egyszerű válaszolni. A helyzet igen bonyolult mindaddig, amíg a referencia típusú változókkal alaposan meg nem ismerkedünk.

Addig is használjuk az alábbi szabályokat: amennyiben a paraméterváltozók típusa a nyelv alaptípusai (int, double, char, string, …) közül valamelyik, úgy nem tudunk értéket visszaadni a hívás helyére, ha a paraméter típusa tömb, akkor a tömbelemeken keresztül azonban igen.

Pl.:

static void Kiiras(int a,int b)
{
  Console.WriteLine("A {0}+{1}={2}",a,b,a+b);
  a = a+b;
}
 

Ha a fenti eljárást az alábbi módon hívjuk meg ...

int x=12;
Kiiras(x,20);
Console.WriteLine("A hivas utan az x erteke x={0}",x);
 

... ekkor kiíráskor az ’x’ változó értéke még mindig 12 lesz. Mivel a paraméterátadás során a híváskori érték (12) átadódik az eljárás ’a’ változójába mint kezdőérték (a=12), de utána a hívás helyén szereplő ’x’ változó, és a fogadó oldalon szereplő ’a’ változó között minden további kapcsolat megszűnik. Ezért hiába helyezünk az ’a’ változóba az eljáráson belül más értéket (a+b), az nem fogja befolyásolni az ’x’ értékét, marad benne a 12.

A fenti technikát érték szerinti paraméterátadásnak nevezzük. Ekkor az aktuális paraméterlistában feltüntetett értéket a fogadó oldal átveszi – a nélkül, hogy a kapcsolatot fenntartaná. Az érték egyszerűen átmásolódik a fogadó oldal változóiba.

1. ábra: A paraméterátadás működése

Mint az ábrán is látszik, az ’x’ aktuális értéke (12) átmásolódik a memória külön területén helyet foglaló ’a’ változóba. Ezért az a=a+b értékadás eredménye is ezen a külön területen kerül tárolásra, és nem zavarja, nem változtatja meg az ’x’ értékét.

Amennyiben azonban a paraméterátadás-átvétel során tömböket adunk át, úgy a helyzet megváltozik:

static void Feltoltes(int[] tomb)
{
  tomb[0] = 10;
}

// --
// használat
// --
static int[] kisTomb=new int[10];
Feltoltes( kisTomb );
Console.WriteLine(”A 0. tombelem={0}”, kisTomb[0] );
 

A hívás helyén szereplő kis tömb még feltöltetlen, amikor átadjuk a ’Feltoltes’ eljárásnak. Az eljárás fogadja a tömböt a ’tomb’ változóban, majd megváltoztatja a ’tomb’ elemeinek értékét. Ezen változás azonban beleíródik a ’kisTomb’ által reprezentált tömb-be is, ezért az eljárás lefutása után a kisTomb[0] értéke már 10 lesz.

Az ilyen jellegű paraméterátadást referencia-szerinti átadásnak nevezzük!

A referencia típus nem jelentkezik külön a változó deklarációja során mint külön kulcsszó, vagy egyéb jelzés, ezért nehéz felismerni. Egyelőre fogadjuk el szabályként, hogy azok a változók lesznek referencia típusúak, amelyeknél a ’new’ kulcsszót kell használni az érték megadásakor (példányosítás).

2. ábra: A vektor a memóriában

A referencia-típusú változóknál dupla memóriafelhasználás van. Az elsődleges területen ilyenkor mindig egy memóriacímet tárol a rendszer. Ezen memóriacím a másodlagos memóriaterület helyének kezdőcíme lesz. Ezen másodlagos memóriaterületet a ’new’ hozza létre.

A nyelvi alaptípusok (int, double, char, …) értékének képzésekor nem kell a ’new’ kulcsszót használni …

int a = 10;
 
  • de a tömböknél igen:

int[] tomb = new int[10];
 

Az ilyen jellegű változókat általában az jellemzi, hogy viszonylag nagy memória-igényük van. Egy ’int’ típusú érték tárolása csak 4 byte, míg a fenti tömb tárolása 40 byte.

Az érték szerinti paraméterátadás során az érték az átadás pillanatában két példányban létezik a memóriában (lemásolódik):

int x=12;
Kiiras(x);

static void Kiiras(int a)
{
      ...
}
 
3. ábra: Értékek átadása-átvétele

A referencia típusú változók esetén azonban nem másolódik le az érték még egyszer a memóriában (ami a tömb esetén a tömbelemek lemásolását jelentené), hanem csak a szóban forgó tárterület címe (memóriacíme) adódik át. Ekkor a fogadó oldal megkapja a memóriaterület címét, és ha beleír ebbe a memóriaterületbe, akkor azt a küldő oldal is észlelni fogja:

4. ábra: Referenciák átadása-átvétele
static void Feltoltes(int[] tomb)
{
        tomb[0] = 10;
}
// --
// használat
// --
static int[] kisTomb=new int[10];
Feltoltes( kisTomb );
 

A fenti ábrán látszik, hogy a ’kisTomb’ változónk aktuális értéke átmásolódik a ’tomb’ aktuális értékébe. Csakhogy a ’kisTomb’ értéke egy memóriacím, ez az, ami valójában átmásolódik a ’tomb’ váltózóba, és nem a tömb elemei.

Mivel a ’tomb’ változó is ismerni fogja a tömbelemek tényleges helyét a memóriában, ezért amennyiben megváltoztatja azokat, úgy a változtatásokat a ’kisTomb’-ön keresztül is el tudjuk majd érni, hiszen mindkét esetben ugyanazokra a tömbelemekre hivatkozunk a memóriában.

Ez az oka annak, hogy a tömbelemek értékének módosítása a fogadó oldalon maradandó nyomot hagy a memóriában – amikor az eljárás már befejezte a futását, az értékek akkor is hozzáférhetőek.

A referencia-szerinti paraméterátadás is érték szerinti paraméterátadás, a változó értéke ilyenkor egy memóriacím, mely mint érték másolódik át a paraméterváltozókba. Az már más kérdés, hogyha a fogadó oldal megkapja a terület kezdőcímét, akkor azt meg is változtathatja. Mivel az eredeti változó is ugyanezen területre hivatkozik, ezért a változásokat a küldő oldalon később észlelni, (pl: kiolvasni) lehet.

Amennyiben a fogadó oldalon referencia típusú paramétert fogadunk, úgy a küldő oldalon csak olyan kifejezés szerepelhet, melynek végeredménye egy referencia:

Feltoltes( new int[20] );
 

Az előzőekben bemutatott kifejezés létrehoz egy 20 elemű int tömböt. A new foglalja le számára a memóriát, majd visszaadja a terület kezdőcímét. Ezt a kezdőcímet általában egy megfelelő típusú változóban tároljuk, de amennyiben erre nincs szükség, úgy a memóriacímet tárolás nélkül átadhatjuk egy eljárásnak paraméterként.

static int[] Feltoltes(int[] tomb)
{
        for(int i=0;i<tomb.Length;)
        {
                Console.Write("A tomb {0}. eleme=",i);
                string strErtek=Console.ReadLine();
                int x = Int32.Parse( strErtek );
                if (x<0) continue;
                tomb[i] = x;
                i++;
        }
        return tomb;
}
 

Az előzőekben bemutatott függvény paraméterként megkapja egy int típusú tömb kezdőcímét. A tömb elemeit billentyűzetről tölti fel oly módon, hogy csak pozitív számot fogad el. A feltöltött tömb címét (referenciáját) visszaadja.

Mivel a függvény ugyanazt a memóriacímet adja vissza, mint amit megkapott, ennek nem sok értelme látszik. Figyeljük meg azonban a hívás helyét:

int[] ertekek = Feltoltes( new int[40] );
 

A hívás helyén a frissen elkészített tömb memóriacímét nem tároljuk le, hanem rögtön átadjuk a függvények. A tömb a hívás helyén készül el, üresen (csupa 0-val inicializálva), a függvény feltölti ezt a tömböt elemekkel, majd visszaadja az immár feltöltött tömb címét.

A fenti megoldás azért szép, mert egy sorban oldja meg a tömb létrehozását, feltöltését. Amennyiben a Feltoltes() csak egy eljárás lenne, úgy az alábbi módon kellene meghívni:

int[] ertekek = new int[40];
Feltoltes( ertekek );
 
Hernyák Zoltán
2013-03-14 16:53:16