Ukazatele v C++: proč je potřebujeme, kdy je používáme, jak se liší od přístupu k samotnému objektu
Přestože většina programátorů chápe, jaký je rozdíl mezi objekty a ukazateli na ně, někdy není zcela jasné, který ze způsobů přístupu k objektu zvolit. Na tuto otázku jsme se pokusili odpovědět níže.
Dotaz
Přicházím z prostředí Javy a začal jsem pracovat s objekty v C++. Ale napadla mě jedna věc: lidé často používají ukazatele na objekty místo samotných objektů, například tuto deklaraci:
Object *myObject = new Object;
namísto:
Object myObject;
Nebo místo použití funkce, řekněme testFunc(), jako je tato:
myObject.testFunc();
máme napsat:
myObject->testFunc();
Ale nemohu přijít na to, proč bychom to měli dělat tímto způsobem. Předpokládal bych, že to souvisí s efektivitou a rychlostí, protože získáme přímý přístup k adrese paměti. Mám pravdu?
Odpověď
Měli bychom si mimochodem uvědomit, že ukazatele se v Javě nepoužívají explicitně, např. programátor nemůže v kódu přistupovat k objektu prostřednictvím ukazatele na něj. V Javě se však na všechny typy, kromě bázových, odkazuje: přístup k nim jde přes odkaz, i když nelze explicitně předat parametr pomocí odkazu. Kromě toho se new v C++ a v Javě nebo C# liší.
Abychom si udělali alespoň malou představu o ukazatelích v C++ , uvedeme dva podobné fragmenty kódu:
Java:
Object object1 = new Object(); //A new object is allocated by JavaObject object2 = new Object(); //Another new object is allocated by Javaobject1 = object2; //object1 now points to the object originally allocated for object2//The object originally allocated for object1 is now "dead" – //nothing points to it, so it//will be reclaimed by the Garbage Collector.//If either object1 or object2 is changed, //the change will be reflected to the other
Nejbližší ekvivalent tohoto, je:
C++:
Object * object1 = new Object(); //A new object is allocated on the heapObject * object2 = new Object(); //Another new object is allocated on the heapdelete object1;//Since C++ does not have a garbage collector, //if we don't do that, the next line would //cause a "memory leak", i.e. a piece of claimed memory that //the app cannot use //and that we have no way to reclaim...object1 = object2; //Same as Java, object1 points to object2.
Podívejme se na alternativní způsob v C++:
Object object1; //A new object is allocated on the STACKObject object2; //Another new object is allocated on the STACKobject1 = object2;//!!!! This is different! //The CONTENTS of object2 are COPIED onto object1,//using the "copy assignment operator", the definition of operator =.//But, the two objects are still different. //Change one, the other remains unchanged.//Also, the objects get automatically destroyed //once the function returns...
Dosáhneme zvýšení rychlosti, když budeme přistupovat přímo do paměti?
V podstatě vůbec ne. Pro přístup na haldu se obvykle používají ukazatele, zatímco objekty se nacházejí v zásobníku – to je jednodušší a rychlejší struktura. Pokud jste začátečník, máme pro vás materiál, ve kterém si podrobně řekneme, co je to zásobník a halda.
Přísně vzato tato otázka spojuje dvě různé záležitosti. Za prvé: kdy používáme dynamické přidělování paměti? Za druhé: kdy je lepší použít ukazatele? Jistě se neobejdeme bez obecných slov, že je třeba vždy zvolit nejvhodnější nástroj pro danou práci. Téměř vždy je lepší realizace než použití ruční dynamické alokace (dynamic allocation) a/nebo surových ukazatelů.
Je velmi nešťastné, že se s dynamickou alokací setkáváte tak často. To jen ukazuje, kolik je špatných programátorů v C++.
V jistém smyslu máte dvě otázky spojené do jedné. První je, kdy bychom měli použít dynamickou alokaci (pomocí new)? Druhá je, kdy bychom měli používat ukazatele?
Důležité poselství je, že byste vždy měli používat vhodný nástroj pro danou práci. Téměř ve všech situacích existuje něco vhodnějšího a bezpečnějšího než ruční dynamická alokace a/nebo použití surových ukazatelů.
Dynamická alokace
Ve své otázce jste demonstroval dva způsoby vytvoření objektu. Hlavním rozdílem je doba uložení objektu. Při provádění Object myObject; v rámci bloku je objekt vytvořen s automatickým trváním uložení, což znamená, že bude automaticky zničen, když se dostane mimo obor. Při provedení new Object() má objekt dynamické trvání uložení, což znamená, že zůstane naživu, dokud jej explicitně nesmažete. Dynamickou dobu uložení byste měli používat pouze tehdy, když ji potřebujete. To znamená, že byste vždy měli dávat přednost vytváření objektů s automatickým trváním uložení, pokud můžete.
Hlavní dvě situace, ve kterých můžete vyžadovat dynamické přidělování:
- Potřebujete, aby objekt přežil aktuální rozsah – tento konkrétní objekt na tomto konkrétním místě paměti, nikoli jeho kopie. Pokud vám kopírování/přesouvání objektu nevadí (většinou by mělo), měli byste dát přednost automatickému objektu.
- Potřebujete alokovat velké množství paměti, které může snadno zaplnit zásobník. Bylo by hezké, kdybychom se tím nemuseli zabývat (většinou byste neměli), protože to opravdu není v kompetenci C++, ale bohužel se musíme vyrovnat s realitou systémů, pro které vyvíjíme.
- Neznáte přesně velikost pole, které budete muset použít. Jak víte, v C++ mají pole pevnou velikost. To může způsobit problémy například při čtení uživatelského vstupu. Ukazatel definuje pouze tu část paměti, kam se zapíše začátek pole, neomezuje jeho velikost.
Pokud je použití dynamické alokace nutné, měl bys ji zapouzdřit pomocí inteligentního ukazatele nebo jiného typu, který podporuje idiom „Resource acquisition is initialization“ (standardní kontejnery ho podporují – je to idiom, podle kterého se prostředky: blok paměti, soubor, síťové připojení atd. – inicializují při získání v konstruktoru a pak se opatrně zničí destruktorem). Inteligentní ukazatele jsou například std::unique_ptr a std::shared_ptr
Ukazatele
Však existují i jiná obecnější použití surových ukazatelů než dynamická alokace, ale většina má alternativy, kterým byste měli dát přednost. Stejně jako dříve vždy dávejte přednost alternativám, pokud ukazatele opravdu nepotřebujete.
- Potřebujete referenční sémantiku. Někdy chcete předat objekt pomocí ukazatele (bez ohledu na to, jak byl alokován), protože chcete, aby funkce, které ho předáváte, měla přístup k tomuto konkrétnímu objektu (ne k jeho kopii). Ve většině situací byste však měli dát přednost referenčním typům před ukazateli, protože právě k tomu jsou určeny. Všimněte si, že nemusí jít nutně o prodloužení životnosti objektu mimo aktuální obor, jako v situaci 1 výše. Stejně jako dříve, pokud vám nevadí předávání kopie objektu, referenční sémantiku nepotřebujete.
- Potřebujete polymorfismus. Funkce můžete volat pouze polymorfně (tj. podle dynamického typu objektu) prostřednictvím ukazatele nebo reference na objekt. Pokud takové chování potřebujete, pak musíte používat ukazatele nebo reference. Opět by se měly upřednostňovat reference.
- Chcete reprezentovat, že objekt je nepovinný, tím, že umožníte předání nullptr při vynechání objektu. Pokud se jedná o argument, měli byste raději použít výchozí argumenty nebo přetížení funkce. V opačném případě byste měli raději použít typ, který toto chování zapouzdřuje, například std::optional (zavedeno v C++17 – u dřívějších standardů C++ používejte boost::optional).
- Chcete oddělit kompilační jednotky, abyste zlepšili čas kompilace. Užitečnou vlastností ukazatele je, že vyžadujete pouze dopřednou deklaraci typu, na který se ukazuje (pro skutečné použití objektu budete potřebovat definici). To vám umožní oddělit části kompilačního procesu, což může výrazně zlepšit čas kompilace. Viz idiom Pimpl.
- Potřebujete rozhraní s knihovnou jazyka C nebo s knihovnou ve stylu jazyka C. V případě, že se jedná o knihovnu ve stylu jazyka C, je nutné ji použít. V tomto okamžiku jste nuceni používat surové ukazatele. Nejlepší, co můžete udělat, je zajistit, abyste surové ukazatele uvolnili až v poslední možné chvíli. Surový ukazatel můžete získat například z inteligentního ukazatele pomocí jeho členské funkce get. Pokud pro vás knihovna provádí nějakou alokaci, od které očekává, že ji budete dealokovat prostřednictvím handle, můžete často handle zabalit do inteligentního ukazatele s vlastním deleterem, který objekt vhodně dealokuje.