Memóriakezelés

Czirkos Zoltán · 2019.02.27.

A memória „típusairól” és a memóriakezelésről.

A C és C++ programozás tanulása során sok problémát okoz a memóriakezelés megértése. Az alábbi magyarázattal próbálom a lényeges elemeket összefoglalni.

1. A memóriaterületek

A program a futása során három jól elkülöníthető szerepű memóriaterülettel rendelkezik, ezek funkciók szerint a globális változók memóriaterületei, a verem és a dinamikusan lefoglalt memóriaterületek.

A memóriakezelés megértése lényegében azon múlik, hogy az ember tisztában van-e vele, a különféle módokon deklarált változók és adatterületek melyik helyre kerülnek. Abból már egyértelműen következnek az órákon általában bemutatott ökölszabályok is, például hogy miért nem lehet lokális változóra mutatót visszaadni egy függvényből.

A globális memóriaterület

A globális memóriaterületen helyezkednek el, ahogyan a neve is mutatja, a globális változók. Ezek a program egész futása alatt léteznek. Az ott létrehozott változók elhelyezkedése nem változik meg. A kiosztásuk már a program fordításakor eldől, és még azelőtt létrejönnek, hogy a main() függvény első sorát elkezdené végrehajtani a gép.

1. int a;
2. char s[100] = "hello";
3. char* ptr = "szoveg";
4.
5. int main()
6. {
7.     printf("%d", i);
8. }

A változók elhelyezését könnyű megérteni, ha betűről betűre ragaszkodunk ahhoz, amit a kódrészlet tartalmaz. A gép úgysem képes másra... Vastag betűvel kiemeltem, hogy melyik sorban mit, milyen típusú változót deklarálunk.

Az 1. sorban egy integert (int) hozunk létre a globális memóriaterületen. A 2. sor egy száz elemű, karakterekből álló tömböt (char [100]) hoz létre. A tömb első 6 karakterét használjuk (az öt betűs „hello” és a lezáró nulla a sztring végén), a többi tartalék hely, hogy hosszabb sztringet is másolhassunk ide.

A 3. sor cselesebb. Ott csak egy pointert deklarálunk (char*), amelyik egy, a globális memóriaterületen elhelyezett névtelen karakter tömbre mutat. Ez nagyon fontos különbség az előzőhöz képest, ahol nem egy pointer és egy tömb jött létre, hanem csak egy tömb. Itt egy pointer is létrejön, amely igazából független a tömbtől, csak most éppenséggel kezdeti értékként ráállítottuk arra a tömbre.

A 7. sorban a printf hívás hasonlít ehhez. Megadunk egy formátum sztringet, ami alapján tudja, hogy hogyan kell kiírnia a változót. Ez a formátum sztring is a globális memóriaterületen helyeződik el, ugyancsak névtelen tömbként. A printf() egy pointert kap erre futás közben, ahogy a harmadik sor ptr-je is csak egy mutató az előző névtelen tömb elejére.

Ha a printf() után azt mondanánk, hogy ptr=s; akkor onnantól kezdve ptr az s[] tömbre mutat. A „szoveg” sztringet akkor többé semmi módon nem érjük el, mivel névtelen tömb, és nem tudjuk, hol van a memóriában – nem mutat rá pointerünk.

A verem (stack)

A verembe a függvények lokális változói kerülnek. A verem speciális tulajdonsága, hogy a tartalma fel-le változik; ha egy függvény belsejének végrehajtásába kezdünk, akkor a verem tetején létrejönnek a függvény lokális változói, ha pedig a függvényből visszatérünk, akkor azok a változók megszűnnek. Az adott függvényhíváshoz tartozó memóriaterületet a veremben stack frame-nek nevezzük. A veremben minden függvény csak a saját lokális változóit látja. Ha a függvény saját magát hívja meg, akkor különböző és egymástól független példányok keletkeznek a lokális változóiból.

1.  void fv(int b)
2.  {
3.    char* ptr = "global";
4.    char tomb[] = "ding";
5.    b = 6;
6.  }
7.
8.  int main()
9.  {
10.   int a = 5;
11.   char s[50] = "verembe";
12.
13.   fv(a);
14. }

Ennél a példánál a következő módon alakul a memóriaterületek tartalma. A program indításkor a main() függvényt kezdi el végrehajtani. A main() függvénynek két lokális változója van, egy integer (int a, 10. sor), és egy ötven elemű karakter tömb (11. sor). A karakter tömb maga, vagyis az egyes karakterek is a veremben helyezkednek el.

Ha meghívjuk az fv() függvényt (13. sor), akkor az induláskor létrejönnek a veremben a paraméterei (!) és lokális változói, b nevű egész (1. sor), a ptr nevű pointer (3. sor) és a tomb nevű tömb (4. sor). A ptr-rel megint csak egy pointert deklarálunk, nem pedig egy tömböt! Ha char*-ot írunk, a gép char*-ot ért alatta. A verembe, amely a lokális változókat tárolja, így csak a pointer kerül, amely be is állítódik a „global” szót tartalmazó tömbre. A tömb az előzőekhez hasonlóan a globális memóriaterületre került, és a program egész futása alatt létezik. Az 4. sorban nem pointert, hanem egy tömböt deklarálunk; annak tartalma is a verembe kerül, még így is, hogy méretét nem adtuk meg, hanem az inicializáló sztring alapján számolja a fordító. A deklaráció pontos értelmezése mindent eldönt!

A lokális változók csak addig léteznek, amíg a függvény belsejében vagyunk, a 6. sorig bezárólag. Ha visszatérünk a függvényből, és újra a main()-ben vagyunk, akkor már nem. A b = 6 emiatt értelemszerűen nem a main() változóját, a-t módosítja, hanem a veremben lévő másolatot.

A kupac (heap)

A dinamikus memóriaterület, vagyis a heap olyan terület, amelyből egy adott nagyságú részt a program futása közben kérhetünk, és ha már nem kell, visszaadhatjuk. Így foglalhatunk le akkora méretű memóriát, amelynek a nagyságát a program írása, fordítása közben még nem ismerjük.

A lefoglaláskor egy pointert, vagyis egy mutatót kapunk arra a memóriahelyre, ahol a gép megfelelő nagyságú területet talált; amikor arra már nincs szükségünk, akkor felszabadítjuk azt. A terület a lefoglalástól kezdve mienk, egészen addig, amíg vissza nem adjuk. C-ben ez a malloc() és free() függvényhívásokkal, C++-ban a new és a delete operátorokkal történik.

A C++ szabvány megkülönbözteti ezt a kettőt. A malloc()-free() páros által használt memóriaterület neve ott heap, a new-delete operátorok által használt pedig free store. Ezek persze lehetnek közösek; és egy programban lehet használni egyszerre mind a kettőt. Csak amit a malloc() foglalt, azt free()-vel kell felszabadítani, nem delete-tel. Ugyanez igaz fordítva is.

A lentebbi példa kód 3. sorban először is deklarálunk egy char* típusú pointert. A pointer maga a veremben jön létre, és beállítjuk egy dinamikusan lefoglalt memóriaterületre, amely száz karaktert képes tárolni. C-ben ehhez a malloc() függvényhívást használjuk, C++-ban pedig a new[] operátort. A száz karakternek való hely a dinamikus memóriaterületen foglalódik le, vagyis a heap-en.

A 4. sorban egy Komplex számra mutató pointert deklarálunk, és foglalunk helyet a heapből egyetlen egy Komplex számnak. Az 5. sor pedig megint csak egy pointert deklarál csupán a veremben; a pointernek akkor lesz értelme, ha beállítjuk, hogy valahova mutasson, valami számunkra hasznos helyre. Jelen esetben egy nagy, ezer egész számot tartalmazó, dinamikusan foglalt tömbre.

A 7. sorban a lefoglalt karakter tömbbe másolunk egy sztringet. A sztring hossza 11 betű, meg van még egy lezáró nullánk, vagyis 12 karakterből áll; 100 karaktert foglaltunk, vagyis ez rendben van. Az eredeti sztring egyébként az előző példákhoz hasonlóan a globális memóriaterületen van, névtelenül. Onnan másolódik át most a heapen lefoglalt területre.

C-ben:
1.  int main()
2.  {
3.    char *tea = malloc(100*sizeof(char));
4.    Komplex *k = malloc(sizeof(Komplex));
5.    int *sok = malloc(1000*sizeof(int));
6.
7.    strcpy(tea, "bai ji guan");
8.
9.    free(sok);
10.   free(k);
11.   free(tea);
12. }
C++-ban:
1.  int main()
2.  {
3.    char *tea = new char[100];
4.    Komplex *k = new Komplex;
5.    int *sok = new int[1000];
6.
7.    strcpy(tea, "bai ji guan");
8.
9.    delete[] sok;
10.   delete k;
11.   delete[] tea;
12. }

A 8. sorban felszabadítjuk a Komplex típusú adatunknak lefoglalt memóriaterületet. Ezt érdemes egyből megtenni, amikor már nincsen szükség arra a változóra. A k mutató ezután továbbra is oda mutat, ahol az a Komplex szám volt, de ezután már nem szabad hivatkozni a területre, hiszen visszaadtuk a malloc()-nak (new-nak), hogy használja újra, ha majd másra kell. Ezt a szabályt mindig, minden körülmények között be kell tartani; az nem indok, hogy úgysem foglaltunk még új memóriát, vagy hogy „csak már ott van még lécci” az a szám, ahol eredetileg volt. A programunk lehet többszálú, és akkor egy másik szálban bármikor lefoglalódhat az a terület más célra. Sőt akár az operációs rendszerhez is visszakerülhetett, és egy másik program használja.

A többi terület felszabadítása ugyanígy történik. Érdemes megfigyelni, hogy C++-ban, amikor csak egyetlen Komplex-nek foglaltunk helyet, akkor a new operátort használtuk, a felszabadításhoz pedig a delete-et. A tömb foglalásakor a new[] operátor kellett, és a felszabadításhoz pedig a delete[]. A kettőt nem szabad keverni; ami new, az később delete; ami new[], az pedig később delete[]. Nem ugyanazt jelentik a new char és a new char[1] kifejezések!

Ennek nem csak az az értelme, hogy egy ptr = new Objektum[100] utáni delete ptr-nél csak az első objektum destruktora hívódik meg. A new és a new[] operátorokat külön kell átdefiniálni; az egyik lehet, hogy teljesen más helyről ad memóriát, és más nyilvántartást vezet a lefoglalt területekről, mint a másik.

Erre igazából C-ben és C++-ban is nekünk kell figyelni. A pointeren nem látszik, hogy az egyetlen egy adatra mutat, vagy egy tömbre. Vagyis egy önálló Komplex szám memóriacíme, és egy Komplex tömb memóriacíme ugyanaz a típus: Komplex*.

Ugyanígy, egy pointeren nem látszik az, hogy dinamikusan foglaltunk memóriát, és arra mutat a pointer; vagy a pointert beállítottuk egy, a globális memóriaterületen, esetleg a veremben elhelyezkedő változóra (a következő kód 6-8 sora). Csak azt a memóriát kell kézzel felszabadítanunk, amit mi magunk foglaltunk le; a többiről a fordító gondoskodik. A veremből úgyis eltűnik, amikor vége a függvény végrehajtásának; a globális memóriaterületről pedig, amikor a programénak.

C-ben:
1.  char global[100];
2.
3.  int main()
4.  {
5.    char tomb[200];
6.    char* veremben;
7.    char* heapen;
8.    char* globalisban;
9.
10.   heapen = malloc(100);
11.   globalisban = global;
12.   veremben = tomb;
13.
14.   …
15.
16.   free(heapen);
17. }

2. Tömbök átadása függvényeknek, sizeof

A tömböket függvényeknek kezdőcímükkel lehet átadni. A kezdőcím a tömb méretét nem tartalmazza; a függvénynek ezért semmi módja nincsen azt megtudni, hacsak explicit módon meg nem mondjuk neki. Az átadás lehetséges formái:

int osszeg(int* tomb, int meret);
int osszeg(int tomb[], int meret);

Mind a kettő egyformán jó, mert ugyanazt jelentik. Az előbbi jobban kifejezi, hogy egy kezdőcímről van szó. A függvény belsejében a tomb nevű pointert, amely csak egy pointer, akár meg is változtathatjuk (pl. tomb++); ezt gyakran csinálják sztringeket kezelő függvényeknél. A második forma kihangsúlyozza, hogy tömbről van szó, de nem teszi lehetővé a pointer megváltoztatását; illetve kicsit arra utal, mintha az egész tömb lemásolódna, ami nem igaz.

Mivel a függvény az egész eredeti tömbből csak egy kezdőcímet lát, semmiképp nem használható benne a sizeof operátor. Ha belül azt írjuk, hogy sizeof(tomb), akkor egy pointer méretét kapjuk meg, nem pedig a tömb méretét. Ez sztringeknél okoz sok keveredést.

char s1[20] = "hello";
char s2[50] = "hello";
char* s3 = "hello";
char s4[] = "hello";

A sizeof(s1) kifejezés értéke 20, mert 20 darab karakter a tömb mérete (sizeof(char) definíció szerint 1). A sizeof(s2) kifejezésé pedig 50, ugyanezen okból. Hiába tartalmazza ugyanazt a sztringet! A sizeof(s3) értéke gépfüggő, lehet például 4, ha éppen az adott gépen annyi bájt a pointer mérete. A sizeof(s4) az 6, mert megint csak a tömb méretét kérdezzük; 5 karakter a hellónak és 1 a lezáró nullának. A sizeof a típus méretét adja meg, nem figyel a tartalomra! Ugyanakkor strlen(s1)=strlen(s2)=strlen(s3)=strlen(s4)=5, mert 5 betűből áll a szó; valószínű erre vagyunk kíváncsiak.

3. A memóriakezelési hibák látható jelei

A memóriakezelési hibák kellemetlen tulajdonsága, hogy sokszor észrevétlenül maradnak. Egy felszabadítatlan memóriaterületnek a heapen például semmi látható hatása nincs. „Csak” annyi, ha a windowsos feladatkezelőben, linuxos topban vagy hasonló helyen nézzük a program memóriaigényét, akkor azt látjuk, hogy egyre csak nő. Eszi el a többi programtól a memóriát, észre meg csak akkor vesszük, ha már lassulni kezd miatta a gép. Minél nagyobb a programunk, ez annál könnyebben lehet gond. Kellően összetett, memóriaszivárgással teli programhoz ha új részt írunk, már kideríteni sem tudjuk, hogy vajon az új kódrészletekkel rontottunk-e a helyzeten.

Másik gyakori jelenség a memóriakezelési hibák esetén a változók értékeinek misztikus megváltozása. Ilyesmi akkor szokott előfordulni, ha egy lefoglalt tömböt nagyobbnak gondolunk, mint amekkora valójában.

char tomb[10] = "ez";
char tomb2[20] = "az";
strcpy(tomb, "memoriakezelesi hiba");

A fenti kódrészletben például a tíz karakterből álló tömbbe, amely egyébként kilenc betűt és a lezáró nullát tartalmazhatja maximum, egy sokkal nagyobb sztringet másolnánk. Ettől lehet, hogy tomb2 meg fog változni. De az is lehet, hogy nem. Az is lehet, hogy lefagy a programunk, de akár előfordulhat az is, hogy észrevétlenül fut tovább. Minden attól függ, hogy a fordító hogyan helyezte el a tömbjeinket a memóriában. Ez különösen akkor érdekes, ha a két tömb egy függvény lokális változója, ugyanis akkor a veremben vannak, és a verem nem csak változókat, hanem például azt a memóriacímet is tartalmazza, ahol a program végrehajtását folytatni kell a függvényből kilépés után. Ha azt véletlenül felülírjuk, az elszállás garantált. A fordítón múlik, hogy hogyan helyezi el a tömböket.

char* tomb = malloc(10);
char* tomb2 = malloc(20);
strcpy(tomb, "memoriakezelesi hiba");

Ugyanez a helyzet a dinamikusan lefoglalt esetben is. Ha tomb után tomb2 van a memóriában, felülírjuk. Ha egy éppen lyukas rész van ott (lásd a Komplex felszabadítása), akkor észre sem vesszük. A program különböző futtatásai során ráadásul máshol találhat nekünk szabad memóriát a malloc(): egyszer működik, másszor pedig lefagy. Egyik gépen működik, a másikon meg nem, mert esetleg nem ugyanaz a memóriakezelési stratégia. A konklúzió az, hogy akkor van szerencsénk, ha legalább lefagy a program. Akkor legalább kiderül, hogy valami baja van.

4. Feladatok

Van hiba az alábbi kódrészletekben? Ha igen, hol?

  1. int* fv()
    {
        int t[] = {1, 2, 3, 4, 5};
        return t;
    }
    Megoldás

    A tömb a függvény lokális változója, a veremben jött létre. Nem adhatunk vissza rá pointert, mert ahogy a függvényből kijöttünk, már nem létezik.

  2. char* fv()
    {
        char* szo = "hello";
        return szo;
    }
    Megoldás

    Nincs benne hiba. A függvényben csak a pointer a lokális változó; a sztring a globális memóriaterületen van, vagyis a függvényen kívül is létezik.

  3. const char* szam2string(int i)
    {
        static char str[30];
        sprintf(str, "%d", i);
        return str;
    }
    Megoldás

    Ebben sincs hiba. A tömb, bár lokálisnak van kikiáltva, statikus. Vagyis igazából globális változóként viselkedik, amely megmarad a függvényből kilépés után is. (Ez az egyetlen olyan hely, ahol a static kulcsszó tényleg azt csinálja, amit az angol szó jelent.) A függvény a számból sztring létrehozásának egy – igazából nem túl szerencsés – megvalósítása. A karakter tömbből ugyanis csak egyetlen példány van; a hívások között megtartja az értékét, de több hívás során már nem. Például az alábbi sor nem írja ki a 4-et és az 5-öt. Nem szeretjük a globális változókat, ugyebár.

    printf("%s %s", szam2string(4), szam2string(5));
  4. const char* ki_vagy()
    {
        return "Pistike";
    }
    Megoldás

    Teljesen jó. "Pistike" típusa const char[]; a globális memóriaterületen létrejött konstans tömb. Megmarad a hívás után, sőt létezett már előtte is.

  5. int* fv()
    {
        int tarolo[30] = {1, 2, 3, 9};
        static int* statptr;
        statptr = tarolo;
        return statptr;
    }
    Megoldás

    A pointer ugyan statikus (globális memóriaterületen van), vagyis az értéke megmarad a hívások között... A tömb viszont, amire mutat, az meg fog szűnni a függvény végén, mert az meg a veremben van. Ezért ez hibás.

  6. class String {
        char* szo;
      public:
        String(const char* init) {
            szo = new char[strlen(init)];
            strcpy(szo, init);
        }
        ~String() { delete[] szo; }
    };
    Megoldás

    Az odaírt dolgokban a hiba, hogy a sztringnek eggyel több karakter kell, mint strlen(init); mert az strlen() csak az értékes karaktereket számolja, a lezáró nullát viszont nem. Az oda nem írt dolgok pedig: kell másoló konstruktor és értékadó operátor. (Ha a destruktor, másoló konstruktor, értékadó operátor közül bármelyik kell, akkor általában mind a három kell.)

  7. class String {
        char* szo;
      public:
        String() { szo = ""; }
        String(const char* init) {
            szo = new char[strlen(init)+1];
            strcpy(szo, init);
        }
        ~String() { delete[] szo; }
    };
    Megoldás

    Szinte teljesen jónak tűnik, de mégsem. Ha a paraméter nélküli konstruktort hívjuk, akkor a szo pointer egy globális memóriaterületen elhelyezett üres sztringre fog mutatni. Ha egy ilyen objektum destruktora fut, az fel akarja szabadítani ezt a memóriaterületet, de nem fog menni; a pointer ugyanis nem a new[] operátortól származik.

  8. class Tarolo {
        Adat** t;
        int db;
      public:
        Tarolo(int i) { db = 0; t = new Adat* [i]; }
        ~Tarolo() { delete[] t; }
        berak(Adat* a) { t[db++] = a; }
    };
    
    Adat m;
    Tarolo tar(10);
    tar.berak(new Adat);
    tar.berak(&m);
    Megoldás

    Az osztály kódja jónak néz ki, ahogy használjuk, az viszont tervezési hibára utalhat. Pointereket tárol el Adat objektumokra; adunk neki olyan pointert is, amelyik globális (vagy veremben lévő) objektumra mutat, meg olyat is, amelyik a heapen van (new Adat). Viszont ha nem tartjuk nyilván, hogy melyik melyik, akkor nem fogjuk tudni utólag, melyiket kell delete-elni, melyiket nem. (Egy pointeren nem látszik, hogy globális, verem, vagy heap területre mutat-e!) Vagy esetleg ha ez a két berak() hívás egy függvényen belül történt, akkor abból kikerülve az m objektum megszűnik, a tároló viszont még mindig tárolja a pointerét, ami előbb-utóbb hibához vezet majd. Ha már fix a tömb mérete, nem ártana azt sem ellenőrizni a berak() hívásnál.

  9. char* ptr = new char(80);
    strcpy(ptr, "Nederlandse Spoorwegen");
    delete[] ptr;
    Megoldás

    A puskázók és egymásról másolók tipikus hibája. :) A 80 karakterből álló tömb a heapen ugyanis nem new(80), hanem new[80]. A kerek zárójel azt jelenti, hogy egyetlen egy karaktert foglalunk le, aminek 80-as értéket adunk (amely a nagy P betű ASCII kódja).

  10. class Tarolo {
        Adat** t;
        int db;
      public:
        Tarolo(int i) { db = 0; t = new Adat* [i]; }
        ~Tarolo() { for (int i = 0; i < db; ++i) delete t[i]; }
        berak(Adat* a) { t[db++] = a; }
    };
    
    Adat m;
    Tarolo tar(10);
    tar.berak(new Adat);
    tar.berak(&m);
    Megoldás

    Hasonló a 8-as feladat tárolójához, de ez úgymond örökbe fogadja az objektumokat, vagyis delete-eli őket, ha ő maga is megszűnik. A delete-elés módja rendben van; de a tömböt, amelyik az egyes Adat-okra mutató pointereket tartalmazza (amit a konstruktorban hozunk létre), azt nem szabadítja föl. Vagyis tartalmaz az osztály egy memóriaszivárgást.

    Az sincsen rendben, ahogyan használjuk; ennek a tárolónak a delete-elés miatt csak dinamikusan lefoglalt objektumokat adhatunk, ezért berak(&m) is hibás.

    Kényes kérdés egyébként itt a másoló konstruktor. Ha lemásoljuk a tárolót, akkor le kell másolnunk a benne lévő objektumokat is (mert a destruktora deletel). Nem elég csak átmásolni az Adat** pointert, hanem új pointereket tároló tömb kell; és nem elég az azon belül tárolt pointereket sem átmásolni, hanem mindegyik objektumról másolatot kell készíteni egyesével.

  11. /* mit ír ki? */
    const char* egyik = "hello";
    const char* masik = "hello";
    
    if (egyik == masik)
        printf("egyforma");
    else
        printf("nem egyforma");
    Megoldás

    Nem lehet megmondani, mit ír ki. Nem a két sztringet hasonlítjuk össze, hanem a rájuk mutató pointereket. Két pointert deklaráltunk csak, nem pedig két tömböt. Ráállítottuk őket az egyik, illetve a másik „hello” sztringre, amelyeket a globális memóriaterületen helyeztünk el. Mivel a sztring literálisok ("hello") típusa const char[], az általuk tárolt karakterek konstansok. Ha pedig konstansok, akkor úgysem fognak megváltozni. Ezért a fordító, ha felfigyel rá, hogy egyforma a két sztring, megteheti azt, hogy csak egy másolatot tárol el belőle. Így kisebb lehet a program.

    GCC-vel kipróbálva, ha egy fájlban van definiálva a két sztring, akkor egyformák a pointerek. Ha két külön .c fájlban vannak, akkor linkeléskor már nem keresi, hogy egyformák-e; és különbözőek lesznek a pointerek.

  12. class String {
        char* ptr;
      public:
        String(char* init) {
            ptr = new char[strlen(init)+1];
            strcpy(ptr, init);
            delete[] init;
        }
    };
    Megoldás

    Nem biztos, hogy hibás; igazából ez nem kódolási, hanem tervezési hiba lehet. A konstruktorban delete[]-eljük azt a karakter tömböt, aminek a másolatát tárolja a String objektum. Annyiban rossz gondolat ez, hogy a tömbért, amit a String-nek csak le kell másolnia, nem a String a felelős, hanem a hívó. Ha valamiért mégis így döntünk, az nem csak logikátlan felépítését jelenti a programnak: onnantól kezdve ezt a String-et csak dinamikusan lefoglalt karakter tömbből lehet inicializálni. Még olyat se írhatunk, hogy String s("hello"); mert itt a paraméterben megadott sztring globális területen van.

  13. class String {
        char* ptr;
      public:
        String() {
            /* Üres string: egy szem lezáró nulla. Inicializáljuk is. */
            ptr = new char('\0');
        }
        String(const char* init) {
            ptr = new char[strlen(init)+1];
            strcpy(ptr, init);
        }
        ~String() { delete[] ptr; }
    };
    Megoldás

    Nem indul rosszul, lefoglalunk egyetlen karaktert, mert úgyis csak a lezáró nulla kell, és egyből be is másoljuk azt a nullát. De amit new operátorral foglaltunk le, azt később nem lehet delete[] operátorral felszabadítani. Szóval mégis rossz.

  14. /* kétdimenziós, négyzetes mátrix determinánsát számolja */
    double determinans(double **matrix, int meret);
    
    double matr[3][3];
    printf("%g", determinans(matr, 3));
    Megoldás

    Nem jó típust adunk át paraméternek. A matr[3][3] egy kétdimenziós tömb, amely igazából 3*3=9 darab szám sorfolytonosan elhelyezve a memóriában. A paraméterben lévő matrix pedig: double** matrix, vagyis double* matrix[], vagyis double számokra mutató pointerek tömbje. Ez nem lehet kompatibilis az elsővel (ott nincsenek pointerek).

  15. char s[30] = "hello";
    printf("%s", &s);
    Megoldás

    Nem jó, printf("%s", s) a helyes. A típus nem egyezik; s típusa 30 karakterből álló tömb, amely kezdőcímével adódik át a függvénynek, amely egy karakterre mutató pointert vár. Más kérdés, hogy ott több karakter is lesz. &s pedig nem karakterre mutató pointer, hanem 30 elemű karakter tömbre mutató pointer. És megint csak más kérdés, hogy annak az értéke történetesen ugyanaz, mint s-nek. Változó hosszúságú paraméterlista esetén nincsen típusellenőrzés. Aki nem hiszi, járjon utána: strlen(&s) nem működik; printf("%s", (&s)+1) sem írja ki, hogy „ello”.

  16. int* t = new int[300];
    …
    free(t);
    Megoldás

    Tipikus „otthon még működött” hiba. Nem csak az a baj, hogy ha a lefoglalt objektumoknak lenne destruktora (az int-nek mondjuk nincs), az nem fog meghívódni a free() esetén; hanem hogy a new[]-delete[] lehet, hogy különböző területről ad memóriát, vagy más nyilvántartást vezet a lefoglalt területekről, mint a malloc-free. Nagyon gyakran van az, hogy a new és a new[] a háttérben a malloc() hívást használja, némelyik fordítót így írták meg, némelyiket meg nem. Ezért néhol „véletlenül” jól fut ez a programrész, máshol meg memóriakezelési hibaüzenettel leáll.

  17. void nagybetus(char* t)
    {
        for (int i = 0; t[i] != '\0'; t++)
            t[i] = toupper(t[i]);
    }
    
    const char str1[] = "hello";
    
    int main()
    {
        char* str2 = "hello";
        const char str3[] = "hello";
        nagybetus(str1);
        nagybetus(str2);
        nagybetus(str3);
    }
    Megoldás

    Egyik függvényhívás sem jó. str1 esetén konstans tömböt deklarálunk a globális memóriaterület konstans részén. A fordító a függvényhívásra jelezni fogja, hogy nem kellene. Ha egy casttal ráerőltetjük, akkor meg futás közben a processzor fogja jelezni, hogy nem lehet írni a csak olvashatóként megjelölt memóriaterületre. A második esetben (str2) egy pointert deklarálunk csak, amelyet a konstans globális területen létrehozott tömbre állítunk. A fordító a deklarációnál fogja jelezni, hogy nincs rendben a dolog; a hívásnál nem. A futás közben ugyanúgy memóriahibát fog jelezni a processzor (segmentation fault). A harmadik esetben (str3) a fordító jelez, hogy konstans tömbre hívjuk meg a függvényt. Ha egy casttal meggyőzzük – akkor a program lefut, a sztring valóban nagybetűs lesz. Azért, mert az egy tömb a veremben; a verem pedig nem helyezkedhet el csak olvasható memóriaterületen. Akkor nem lehetne használni semmire.

    A globális memóriaterületből igazából kettő van, egy csak olvasható, és egy írható-olvasható. A read-only (csak olvasható) memóriaterület, mint lehetőség, csak a globális változók esetén működik; a legtöbb mai processzor szerencsére támogat ilyet. Valami múzeumi gépen, régi fordítóval, a megfelelő castokkal mind a három hívás működésre bírható.

  18. class Vektor {
        int meret;
        double* adat;
      public:
        Vektor (int meret = 3)
          : meret(meret), adat(new double[meret])
        {
            for (int i = 0; i < meret; ++i)
                adat[i] = 0;
        }
        friend Vektor operator*(double d, const Vektor& v);
    };
    
    Vektor operator*(double d, const Vektor& v)
    {
        Vektor temp;
        temp.meret = v.meret;
        temp.adat = new double[temp.meret];
        for (int i = 0; i < temp.meret; ++i)
            temp.adat[i] = d*v.adat[i];
        return temp;
    }
    Megoldás

    (Destruktor, másoló konstruktor és értékadó operátor kell, természetesen. De azon kívül.) Memóriaszivárgást csinál az operator* függvény. Ez ugyanis létrehoz egy vektort, temp néven. Ez a vektor az alapértelmezett konstruktorával jön létre, hármas mérettel. Vagyis a temp.adat lefoglalt memóriaterületre mutat; amelyre a pointert a temp.adat = new double[temp.meret]; értékadás felülír, így az elveszik.

    Lehet javítani pl. úgy, hogy az új memóriaterület foglalása előtt felszabadítjuk a konstruktor által lefoglaltat, de az se nem szép, se nem gyors megoldás. Jobb megoldás az, ha a konstruktornak megadjuk paraméterben a v vektor méretét (Vektor temp(v.meret)), mert akkor eleve akkora terület jön létre, amekkora a szorzatnak kell, és nem kell felszabadítani és újra foglalni sem. Harmadik megoldás, ha a másoló konstruktort használjuk, ugyanerre a célra, hiszen az is pont akkora vektort hoz létre, mint v, amely pedig pont akkora, amekkorának a szorzatnak is kell lennie.