2016. augusztus 23., kedd

Címtípusok

      Minden számítógépes program figyeli és változtatja a memóriában tárolt értékeket. Irányítja, hogy hol, milyen sorrendben és milyen értékek vannak, hogy a kimenet megjósolható legyen. Emiatt szükség van olyan adattípusra, ami a memóriacímet képviseli. A magasabb absztrakciós szintű programozási nyelvek már nem igénylik címtípusok használatát, de a háttérben akkor is ott vannak és automatikusan végzik a dolgukat. A C++ két címtípussal dolgozik: a referenciával és a mutatóval.

Referenciák
      A referenciákkal létező változókra hivatkozunk, ezért minden referencia deklarálásnál rögtön meg is kell határozni (inicializálni kell), hogy mire megy a hivatkozás:

     int x = 2;
     int &r = x;

A fenti példában r az x memóriacímén tárolt értékre hivatkozik. Ha x memóriacíme 0x22fe37, akkor az r memóriacíme is az lesz és a benne tárolt érték is.

#include<iostream>
using std::cout;

int main()
{
     int x = 2;
     int &r = x;
    
     cout << "x = " << x;
     cout << "\nr = " << r;
     cout << "\n&x: " << &x;
     cout << "\n&r: " << &r;
    
     return 0;
}

A kimenet pedig:

x = 2
r = 2
&x: 0x22fe34
&r: 0x22fe34

Más szóval a referencia egy másolat a változóról, x és r teljesen ugyanolyan című és értékű. A másolat nem szószerint értendő, ugyanis továbbra is egy változó és egy memóriahely van, csupán két nevet használunk. Ha elvégezzük az r = x + 10; műveletet, mindkét változó értéke 12 lesz. Ha egyiknek új értéket adunk, a másik értéke is megváltozik; úgy is montható, hogy r az x másik neve vagy álneve.

A referenciák a függvények paramétereiben bizonyulnak igazán hasznosnak, mert memóriát spórolhatunk velük. Alap esetben, mikor egy változót küldünk paraméterként a függvénynek, arról a háttérben egy másolat készül új memóriacímmel és azt használja a függvény, majd a végén letörli. Ha referenciát, azaz memóriacímet adunk meg, akkor közvetlenül azzal a változóval dolgozik, és nem készít másolatot. A függvény meghívása során viszont vigyázni kell, hogy a referencia nevét (az álnevet) írjuk a paraméterek listájába és ne egy konkrét értéket vagy kifejezést:

Deklarálás:                    void Fuggveny(double szam, int& szamlalo);

Helyes hívás:                Fuggveny(y, i);
                 Fuggveny(9.81, i);
                 Fuggveny(4.9*sqrt(y), i);

Helytelen hívás:            Fuggveny(y, 3);

A következő program értékkel és referenciával használt függvényekre ad példát:

#include <iostream>
using std::cout;
using std::endl;

int Negyzet_ertekkel(int);
void Negyzet_referenciaval(int&);

int main()
{
     int x = 2;

     cout << "x= " << x << endl
          << "Negyzet_ertekkel(x): " << Negyzet_ertekkel(x) << endl
          << "x= " << x << endl
          << "Negyzet_referenciaval(x)..." << endl;
     Negyzet_referenciaval(x);
     cout << "x= " << x;

     return 0;
}

int Negyzet_ertekkel(int a)
{
     return a * a;
}

void Negyzet_referenciaval(int& b)
{
     b = b * b;
}

A kimenet:
x= 2
Negyzet_ertekkel(x): 4
x= 2
Negyzet_referenciaval(x)...
x= 4

Mutatók
      A mutató a referenciák továbbfejlesztett változata. A mutatók a referenciákkal ellentétben külön változók és nem értéket, hanem memóriacímet tárolnak. Bármely más változó memóriacímét tárolhatják. Amíg egy változó közvetlenül hivatkozik az értékére, addig a mutató közvetett (indirekt) módon, a memóriacím révén hivatkozik arra ez értékre. Mivel a mutatók külön változók, nem kell őket inicializálni a deklarálás során.

double *x, *y;

Az x és y double típusú mutatók, azaz csak double típusú változók memóriacímeit tartalmazhatják. Ha azt szeretnénk, hogy a mutató sehová se mutasson, akkor a zéróra vagy NULL-ra kell állítani:

double *x = NULL;

Mivel a mutató is változó, ezért neki is megvan a maga címe, azaz létezik az &x.

#include <iostream>
using std::cout;

int main()
{
     int a;
     int* aP;

     a = 7;
     aP = &a;

     cout << "a= " << a
          << "\n&a= " << &a;
            
     cout << "\naP= " << aP
          << "\n&aP=" << &aP
          << "\n*aP=" << *aP
          << "\n*&aP=" << *&aP
          << "\n&*aP=" << &*aP;

     return 0;
}

A kimenet:
a= 7
&a= 0x22fe2c
aP= 0x22fe2c
&aP=0x22fe20
*aP=7
*&aP=0x22fe2c
&*aP=0x22fe2c

A fenti programban látszik, hogy a mutatónak csakis memóriacímet lehet értékként adni, és hogy a tárolt memóriacímen lévő értékre a csillag operátorral lehet hivatkozni. Éppen ezért a csillag és a referenciajel kioltja egymást (a csillagot dereferenciáló operátorként is nevezik). A *aP az a értékére való hivatkozás, ezért értékadásra is használható: *aP = 5. Akár a referenciák esetén, a *aP = a + 10; műveletet után, mindkét változó értéke 17 lesz. A memóriacím mint érték tároló lehetővé tesz néhány alapvető műveletet a mutatókkal, például hozzáadhatunk vagy kivonhatunk egész számokat belőlük, vagy akár mutatókat is, és összehasonlíthatóak. Ezek segítségével például a szomszédos memóriacímre léphetünk, ami a tömbök vagy listák esetén a következő elemet jelenti.

#include <iostream>
using std::cout;
using std::endl;

int Negyzet_ertekkel(int);
void Negyzet_referenciaval(int*);

int main()
{
     int x = 2;

     cout << "x= " << x << endl
          << "Negyzet_ertekkel(x): " << Negyzet_ertekkel(x) << endl
          << "x= " << x << endl
          << "Negyzet_referenciaval(&x)..." << endl;
     Negyzet_referenciaval(&x);
     cout << "x= " << x;

     return 0;
}

int Negyzet_ertekkel(int a)
{
     return a * a;
}

void Negyzet_referenciaval(int* b)
{
     *b = (*b) * (*b);
}

A kimenet:
x= 2
Negyzet_ertekkel(x): 4
x= 2
Negyzet_referenciaval(&x)...
x= 4

A fő különbség a program referenciás változatához képest, hogy a Negyzet_referenciaval függvény memóriacímet kapott paraméterként, amit a mutató *b vett fel.

#include <iostream>
using std::cout;

int main()
{
     int v[5];
     int* vPtr = &v[0]; //vPtr a v[0] címet tartalmazza

     cout << "vPtr= " << vPtr;

     vPtr += 2;
     cout << "\nvPtr += 2 = " << vPtr;

     vPtr--;
     cout << "\nvPtr-- = " << vPtr;

     --vPtr;
     cout << "\n--vPtr = " << vPtr;

     int* v2Ptr = &v[4];
     cout << "\nv2Ptr-vPtr = " << v2Ptr-vPtr;
    
     return 0;
}

A kimenet:
vPtr= 0x22fe10
vPtr += 2 = 0x22fe18
vPtr-- = 0x22fe14
--vPtr = 0x22fe10
v2Ptr-vPtr = 4

Mivel int típusról van szó, a 2-vel való növelés nem 2 byte-ot, hanem 2x4 byte-ot növel, hisz az int a 32 bites rendszeren 4 byte. A mutatókat akkor lehet összevonni, ha ugyanolyan típusúak, különben típusátalakítást kell végezni. Egyetlen kivétel ez alól a void* típusú mutató, amely bármilyen típusú pointerrel képes együtt működni. A többi mutatóval ellentétben ez az általános mutató csupán memóriacímeket tárol, így nem rendelhető érték a megcímzett változóhoz (mert a fordító nem tudja előre meghatározni az adott címen lévő byte-ok számát). Az értékadás ebben az esetben csakis típusátalakítással lehetséges.

#include<iostream>
using namespace std;

int main()
{
     int x = 10;
     void* ptr = &x;
    
     cout << "ptr = " << ptr;
     //cout << "*ptr= " << *ptr; // hiba
     cout << "\n*(int *)ptr = " << *(int *)ptr;
     typedef int * iptr;
     cout << "\n*iptr(ptr) = " << *iptr(ptr);
     cout << "\n*static_cast<int *>(ptr) = " << *static_cast<int *>(ptr);
     cout << "\n*reinterpret_cast<int *>(ptr) = " << *reinterpret_cast<int *>(ptr);  
}

A kimenet:
ptr = 0x22fe34
*(int *)ptr = 10
*iptr(ptr) = 10
*static_cast<int *>(ptr) = 10
*reinterpret_cast<int *>(ptr) = 10

A tömbök és a mutatók a C++ nyelvben nagyon közel állnak egymáshoz. Ha a v[] tömb nevének memóriacímét nézzük (&v) és ezt átadjuk egy mutatónak (*vPtr = &v), akkor az az első elem címét fogja tartalmazni. A harmadik elemre a következőképp lehet hivatkozni: *(vPtr+3). Nem kötelező mutatót használni, elegendő a csillag operátor is: *(v+3). Alap esetben [] operátor segít az adott elemre hivatkozni, például a harmadik elemre a v[3] utal és ez alkalmazható a tömb mutatóján is: vPtr[3] . A különbség a v és a vPtr között, hogy a v-vel nem lehet aritmetikai műveletet végezni, mint például a v+=3. Ezt leszámítva elmondható, hogy a tömb ugyanaz mint egy konstans mutató. Ha a karakterekből álló stringet vesszük példának, akkor nem másról van szó, mint a string első karakterére mutató mutatóról:

#include<iostream>
using std::cout;

int main()
{
     const char s[] = {"Char típusú változókból felépített string..."};
     const char* p[] = {"Char típusó mutatókból felépített string..."};
    
     const char* szamok[4] = {"Egy", "Ketto", "Harom", "Negy"};
    
     cout << "s = " << s
          << "\ns[0] = " << s[0]
          << "\n*p = " << *p
          << "\n*p[0] = " << *p[0] << "\n"
    
          << "\n*szamok[0] = " << *szamok[0]
          << "\nszamok[0] = " << szamok[0]
          << "\n&szamok[0] = " << &szamok[0]
          << "\n*(szamok) = " << *szamok
          << "\nszamok = " << szamok << "\n"
    
          << "\n*szamok[1] = " << *szamok[1]
          << "\nszamok[1] = " << szamok[1]
          << "\n&szamok[1] = " << &szamok[1]
          << "\n*(szamok+1) = " << *(szamok+1)
          << "\nszamok+1 = " << szamok+1;
    
     return 0;
}

A kimenet:
s = Char típusú változókból felépített string...
s[0] = C
*p = Char típusú mutatókból felépített string...
*p[0] = C

*szamok[0] = E
szamok[0] = Egy
&szamok[0] = 0x22fde0
*(szamok) = Egy
szamok = 0x22fde0

*szamok[1] = K
szamok[1] = Ketto
&szamok[1] = 0x22fde8
*(szamok+1) = Ketto
szamok+1 = 0x22fde8

A mutatók lehetővé teszik például azt is, hogy különböző hosszúságú stringekből álló tömböt alkossunk. A tömb mindenik eleme az ott lévő string első karakterére mutat. Ezért a *szamok[0] értéke E, a *szamok[1]  értéke pedig K. Mindenik string más memóriacímen van, ám egymással szomszédosak. Az E és a K és a többi karakter is nyolc byte távolságra van egymástól, mert egy memóriacím (0x22fde0) összesen 8 byte-ból áll. Ha ugyanezt két dimenziós tömbbel valósítanánk meg, akkor a tömb mindenik elemét a leghosszabb string hosszúságára kéne szabni és az üres karakterek fölöslegesen foglalnák a memóriát.

Mivel a mutató egy változó, ezért rá is lehet mutatni egy másik mutatóval:

#include<iostream>
using std::cout;

int main()
{
     int x = 2;
     int* p = &x;   // int típusú mutató
     int** q = &p;  // int típusú mutatóra mutathat.
     int*** z = &q; // int típusú mutatóra mutató mutatóra mutathat.
    
     cout << "x = " << x
          << "\n*p = " << *p
          << "\n**q = " << **q
          << "\n***z = " << ***z;
    
     return 0;
}

A kimenet:
x = 2
*p = 2
**q = 2
***z = 2

A konstansok esetén a mutató is szigorúan konstans kell legyen. Konstans mutatója pedig lehet a változóknak is és a mutató deklarálása határozza meg, hogy mit lehet és mit nem:

#include<iostream>
using std::cout;

int main()
{
     int x = 2;
     int y = 7;
     const int* p = &x; // nem engedi az x módosítását, engedi a mutató módosítását
     int *const q = &x; // engedi az x módosítását, nem engedi a mutató módosítását
     const int* const z = &x; // nem enged semmit sem
    
     //*p = 4; // hiba
     cout << "x = " << x << "\n";
     p = &y;
    
     *q = 4;
     cout << "x = " << x << "\n";
     //q = &x; // hiba
    
     //*z = 8; // hiba
     cout << "x = " << x << "\n";
     // z = &y; // hiba
    
     return 0;
}

A kimenet:
x = 2
x = 4
x = 4

Nincsenek megjegyzések:

Megjegyzés küldése