Az előadáson szerepelt a Noisy osztály, amelynek
konstruktoraival láthatóvá vált az std::vector
memóriakezelése. Volt egy ehhez hasonló kódrészlet:
std::vector<Noisy> v(3);
Noisy n{5};
v.push_back(n);
Ebben a push_back()
-es sor hatása a következőképpen jelent meg:
Noisy copy ctor 5 !
Noisy copy ctor 0
Noisy copy ctor 0
Noisy copy ctor 0
Noisy dtor 0
Noisy dtor 0
Noisy dtor 0
A lényeg a felkiáltójellel jelzett résznél van. Látjuk, hogy a vektor átméretezi a dinamikus tömböt (ezért volt
szükség a három régi objektum másolására, és az eredeti tömb felszabadításakor a destruktoruk hívására). De
vajon az új tömbbe miért a push_back()
-elt objektum kerül be előbb, miért nem a régiek?
Máshogy is föltehetnénk a kérdést. Tekintsük a push_back()
függvény alábbi megvalósítását.
(Ez nem tartalmazza azt a haladó memóriakezelési technikát,
amit az előadás mutatott be.)
template <typename T>
void vector<T>::push_back(T const & val) {
/* helyet kell csinálni: eggyel nagyobb tömb kell */
T* newdata = new T[size + 1];
for (size_t i = 0; i < size; ++i)
newdata[i] = data[i];
delete[] data;
data = newdata;
/* új adat bemásolása */
data[size] = val;
size++;
}
A kérdés: hol a hiba? Mert tényleg van benne hiba.
A kérdést harmadikféleképp is föl lehet tenni. Ez a verzió a következőképpen hangzik:
mi az a teszteset, amire a push_back()
fenti megvalósításának a hibája előjön?
A hiba a referencia típusú paraméter miatt jöhet elő. Adott a következő kódrészlet:
std::vector<std::string> v = { "alma", "körte", "barack" };
v.push_back(v[0]);
Mit várunk ettől a kódtól? Azt, hogy a vektor elején lévő sztringet, az almát, a vektor végéhez fűzze. Tehát az „alma, körte, barack, alma” szavakat tartalmazó vektort kellene kapnunk. Ezzel szemben a kódunk könnyen lehet, hogy csúfosan el fog szállni.
Mi történik ebben az esetben? Nagyon fontos a műveletek sorrendje:
- Meghívódik az indexelő operátor. Ez visszatér a vektor elején lévő objektum referenciájával.
- Ezek után a
push_back()
függvénybe kerülünk. Ennek szintén referencia típusú a paramétere; aval
referencia ugyanahhoz az objektumhoz kötődik, mint amivel av[0]
visszatért, tehát a tömb elején lévő elemhez. - A
push_back()
nekiáll átméretezni a vektort. Ehhez létrehoz egy nagyobb tömböt, és átmásolja az adatokat. - Ezek után pedig felszabadítja a régi tömböt. A baj itt van: ezzel megszűntek azok az objektumok, tehát a paraméterként kapott referencia is érvénytelenné válik, „invalidálódik”. Az a régi tömbbeli objektumhoz kötődött! A referencia nem objektum, csak egy hivatkozás az objektumra, amely most eltűnt.
Hogy oldható meg ez a probléma könnyen? A megoldás egyszerű: a műveletek átsorrendezésével. Az új tömbbe előbb kell bemásolni a paraméterként kapott objektumot, vagy legalábbis a felszabadítást annak bemásolása után szabad csak megtenni. Ugyanis nem lehet tudni, hogy a referenciával átvett objektum nem valamelyik tömbelem-e véletlenül.
A klasszikus memóriakezeléssel így kell kinéznie a push_back()
függvénynek:
template <typename T>
void vector<T>::push_back(T const & val) {
/* helyet kell csinálni: eggyel nagyobb tömb kell */
T* newdata = new T[size + 1];
/* régi adatok átmásolása, új adat bemásolása */
for (size_t i = 0; i < size; ++i)
newdata[i] = data[i];
newdata[size] = val;
/* most már mehet a delete[] */
delete[] data;
size++;
data = newdata;
}
Ezzel nem magyaráztuk meg teljesen, miért kell előbb másolni az új adatot. De arról majd egy kicsit később, egy másik írásban: push_back, 2. rész.