Pochodzę z środowiska Java i zacząłem pracować z obiektami w C ++. Ale jedną rzeczą, która przyszła mi do głowy, jest to, że ludzie często używają wskaźników do obiektów, a nie samych obiektów, na przykład ta deklaracja:
Object *myObject = new Object;
zamiast:
Object myObject;
Lub zamiast używać funkcji, powiedzmy testFunc()
, tak:
myObject.testFunc();
musimy napisać:
myObject->testFunc();
Ale nie mogę zrozumieć, dlaczego powinniśmy to zrobić w ten sposób. Zakładam, że ma to związek z wydajnością i szybkością, ponieważ uzyskujemy bezpośredni dostęp do adresu pamięci. Czy mam rację?
Odpowiedzi:
To bardzo niefortunne, że tak często widujesz alokację dynamiczną. To pokazuje, ilu jest złych programistów C ++.
W pewnym sensie masz dwa pytania w jedno. Po pierwsze, kiedy powinniśmy zastosować dynamiczną alokację (używanie
new
)? Po drugie, kiedy powinniśmy używać wskaźników?Ważnym przesłaniem jest to, że zawsze powinieneś używać odpowiedniego narzędzia do pracy . W prawie wszystkich sytuacjach istnieje coś bardziej odpowiedniego i bezpieczniejszego niż ręczne ręczne przydzielanie dynamiczne i / lub używanie surowych wskaźników.
Alokacja dynamiczna
W swoim pytaniu pokazałeś dwa sposoby tworzenia obiektu. Główną różnicą jest czas przechowywania obiektu. Podczas wykonywania
Object myObject;
w obrębie bloku obiekt jest tworzony z automatycznym czasem przechowywania, co oznacza, że zostanie automatycznie zniszczony, gdy przekroczy zakres. Kiedy to zrobisznew Object()
, obiekt ma dynamiczny czas przechowywania, co oznacza, że pozostaje przy życiu, dopóki go nie wyraziszdelete
. Dynamicznego czasu przechowywania należy używać tylko wtedy, gdy jest to potrzebne. Oznacza to, że zawsze powinieneś preferować tworzenie obiektów z automatycznym czasem przechowywania, kiedy możesz .Dwie główne sytuacje, w których możesz potrzebować dynamicznej alokacji:
Kiedy absolutnie potrzebujesz dynamicznej alokacji, powinieneś umieścić ją w inteligentnym wskaźniku lub innym typie, który wykonuje RAII (podobnie jak standardowe kontenery). Inteligentne wskaźniki zapewniają semantykę własności dynamicznie przydzielanych obiektów. Przyjrzeć się
std::unique_ptr
istd::shared_ptr
, na przykład. Jeśli użyjesz ich odpowiednio, możesz prawie całkowicie uniknąć własnego zarządzania pamięcią (patrz Zasada zero ).Wskaźniki
Istnieją jednak inne bardziej ogólne zastosowania wskaźników surowych poza alokacją dynamiczną, ale większość ma alternatywy, które powinieneś preferować. Tak jak poprzednio, zawsze preferuj alternatywy, chyba że naprawdę potrzebujesz wskaźników .
Potrzebujesz semantyki odniesienia . Czasami chcesz przekazać obiekt za pomocą wskaźnika (niezależnie od tego, w jaki sposób został przydzielony), ponieważ chcesz, aby funkcja, do której go przekazujesz, miała dostęp do tego konkretnego obiektu (a nie jego kopii). Jednak w większości sytuacji powinieneś preferować typy odwołań zamiast wskaźników, ponieważ właśnie do tego są przeznaczone. Zauważ, że nie musi to koniecznie przedłużać żywotności obiektu poza obecny zakres, jak w sytuacji 1 powyżej. Tak jak poprzednio, jeśli jesteś w stanie przekazać kopię obiektu, nie potrzebujesz semantyki odniesienia.
Potrzebujesz polimorfizmu . Możesz wywoływać funkcje tylko polimorficznie (to znaczy zgodnie z dynamicznym typem obiektu) za pomocą wskaźnika lub odwołania do obiektu. Jeśli potrzebujesz takiego zachowania, musisz użyć wskaźników lub referencji. Ponownie, referencje powinny być preferowane.
Chcesz reprezentować, że obiekt jest opcjonalny , umożliwiając
nullptr
przekazanie go, gdy obiekt jest pomijany. Jeśli jest to argument, powinieneś używać domyślnych argumentów lub przeciążeń funkcji. W przeciwnym razie powinieneś raczej użyć typu, który opisuje to zachowanie, np.std::optional
(Wprowadzony w C ++ 17 - we wcześniejszych standardach C ++, użyjboost::optional
).Chcesz odłączyć jednostki kompilacyjne, aby skrócić czas kompilacji . Przydatną właściwością wskaźnika jest to, że potrzebujesz tylko deklaracji forward typu wskazanego na (aby faktycznie użyć obiektu, potrzebujesz definicji). Pozwala to rozdzielić części procesu kompilacji, co może znacznie skrócić czas kompilacji. Zobacz idiom Pimpl .
Musisz połączyć się z biblioteką C lub biblioteką w stylu C. W tym momencie musisz użyć surowych wskaźników. Najlepsze, co możesz zrobić, to dopilnować, aby swoje nieprzetworzone wskaźniki straciły tylko w ostatniej możliwej chwili. Surowy wskaźnik można uzyskać z inteligentnego wskaźnika, na przykład za pomocą jego
get
funkcji składowej. Jeśli biblioteka wykonuje dla ciebie pewne alokacje, które, jak się spodziewa, zwolnisz za pomocą uchwytu, często możesz zawinąć uchwyt w inteligentny wskaźnik za pomocą niestandardowego narzędzia usuwającego, który odpowiednio zwolni obiekt.źródło
Object myObject(param1, etc...)
Istnieje wiele przypadków użycia wskaźników.
Zachowanie polimorficzne . W przypadku typów polimorficznych stosuje się wskaźniki (lub odniesienia), aby uniknąć krojenia:
Odwołaj się do semantyki i unikaj kopiowania . W przypadku typów niepolimorficznych wskaźnik (lub odniesienie) pozwoli uniknąć kopiowania potencjalnie drogiego obiektu
Zauważ, że C ++ 11 ma semantykę przenoszenia, która pozwala uniknąć wielu kopii drogich obiektów na argument funkcji i jako wartości zwracane. Ale użycie wskaźnika na pewno ich uniknie i pozwoli na wiele wskaźników na tym samym obiekcie (podczas gdy obiekt można przesunąć tylko raz).
Pozyskiwanie zasobów . Tworzenie wskaźnika do zasobu za pomocą
new
operatora jest anty-wzorcem we współczesnym C ++. Użyj specjalnej klasy zasobów (jeden ze standardowych kontenerów) lub inteligentnego wskaźnika (std::unique_ptr<>
lubstd::shared_ptr<>
). Rozważać:vs.
Surowy wskaźnik powinien być używany tylko jako „widok” i nie może być w żaden sposób związany z własnością, czy to poprzez bezpośrednie tworzenie, czy też pośrednio poprzez wartości zwracane. Zobacz także te pytania i odpowiedzi w sekcji C ++ FAQ .
Bardziej szczegółowa kontrola czasu życia Za każdym razem, gdy kopiowany jest wspólny wskaźnik (np. Jako argument funkcji), zasób, na który wskazuje, jest utrzymywany przy życiu. Zwykłe obiekty (nie tworzone
new
ani bezpośrednio przez ciebie, ani wewnątrz klasy zasobów) są niszczone, gdy wykraczają poza zakres.źródło
unique_ptr
semantyka / movehun(b)
wymaga również znajomości podpisu, chyba że nic ci nie jest, że nie wiedziałeś, że podałeś niewłaściwy typ przed kompilacją. Podczas gdy problem z referencją zwykle nie zostaje złapany w czasie kompilacji i wymaga większego wysiłku do debugowania, jeśli sprawdzasz podpis, aby upewnić się, że argumenty są prawidłowe, będziesz również w stanie sprawdzić, czy którykolwiek z argumentów jest odwołaniem tak więc bit referencyjny staje się czymś bezproblemowym (szczególnie przy użyciu IDE lub edytorów tekstowych, które pokazują podpis wybranych funkcji). Równieżconst&
.Istnieje wiele doskonałych odpowiedzi na to pytanie, w tym ważne przypadki użycia deklaracji forward, polimorfizm itp., Ale czuję, że część „duszy” twojego pytania nie została wyjaśniona - mianowicie co oznaczają różne składnie w Javie i C ++.
Przeanalizujmy sytuację porównując dwa języki:
Jawa:
Najbliższym odpowiednikiem tego jest:
C ++:
Zobaczmy alternatywny sposób C ++:
Najlepszym sposobem, aby o tym pomyśleć jest to, że - mniej więcej - Java (domyślnie) obsługuje wskaźniki do obiektów, podczas gdy C ++ może obsługiwać albo wskaźniki do obiektów, albo same obiekty. Istnieją wyjątki od tego - na przykład, jeśli zadeklarujesz „prymitywne” typy Java, są to rzeczywiste wartości, które są kopiowane, a nie wskaźniki. Więc,
Jawa:
To powiedziawszy, użycie wskaźników niekoniecznie musi być poprawnym lub niewłaściwym sposobem postępowania; jednak inne odpowiedzi zadowalająco to ujęły. Ogólna idea jest taka, że w C ++ masz znacznie większą kontrolę nad czasem życia obiektów i miejscem ich zamieszkania.
Weźmy do domu -
Object * object = new Object()
konstrukcja jest w rzeczywistości najbardziej zbliżona do typowej semantyki Java (lub C #).źródło
Object2 is now "dead"
: Myślę, że masz na myślimyObject1
lub ściślejthe object pointed to by myObject1
.Object object1 = new Object(); Object object2 = new Object();
jest bardzo złym kodem. Drugi nowy lub drugi konstruktor obiektów może rzucić, a teraz obiekt1 wyciekł. Jeśli używasz surowychnew
plików, powinieneś zawinąćnew
obiekty jak najszybciej w opakowania RAII.Innym dobrym powodem do użycia wskaźników byłyby deklaracje przesyłania dalej . W wystarczająco dużym projekcie mogą naprawdę przyspieszyć czas kompilacji.
źródło
std::unique_ptr<T>
działa z deklaracjami przesyłania dalejT
. Musisz tylko upewnić się, że gdystd::unique_ptr<T>
wywoływany jest destruktor ,T
jest to kompletny typ. Zazwyczaj oznacza to, że klasa, która zawierastd::unique_ptr<T>
deklarator destruktora w pliku nagłówkowym i implementuje go w pliku cpp (nawet jeśli implementacja jest pusta).Przedmowa
Java nie przypomina C ++, w przeciwieństwie do hype. Maszyna do hype Java chciałaby, abyś wierzył, że ponieważ Java ma składnię podobną do C ++, języki są podobne. Nic nie może być dalsze od prawdy. Ta dezinformacja jest jednym z powodów, dla których programiści Java przechodzą do C ++ i używają składni podobnej do języka Java, nie rozumiejąc konsekwencji swojego kodu.
Idziemy dalej
Wręcz przeciwnie. Stos jest znacznie wolniejszy niż stos, ponieważ stos jest bardzo prosty w porównaniu do stosu. Automatyczne zmienne pamięci (inaczej zmienne stosu) mają swoje wywoływacze, gdy wychodzą poza zakres. Na przykład:
Z drugiej strony, jeśli użyjesz wskaźnika przydzielanego dynamicznie, jego destruktor musi zostać wywołany ręcznie.
delete
nazywa to Destruktorem.Nie ma to nic wspólnego ze
new
składnią rozpowszechnioną w C # i Javie. Są używane do zupełnie innych celów.Korzyści z dynamicznej alokacji
Jednym z pierwszych problemów, na jakie napotyka wielu programistów C ++, jest to, że kiedy akceptują dowolne dane wejściowe od użytkowników, można przydzielić tylko stały rozmiar zmiennej stosu. Nie można również zmienić rozmiaru tablic. Na przykład:
Oczywiście, jeśli użyłeś
std::string
zamiast tego,std::string
rozmiar wewnętrzny sam się zmienia, więc nie powinno to stanowić problemu. Ale zasadniczo rozwiązaniem tego problemu jest alokacja dynamiczna. Możesz przydzielić pamięć dynamiczną na podstawie danych wejściowych użytkownika, na przykład:Ponieważ stos jest znacznie większy niż stos, można dowolnie przydzielić / realokować tyle pamięci, ile on / ona potrzebuje, podczas gdy stos ma ograniczenia.
Jaka jest korzyść, o którą prosisz? Odpowiedź stanie się jasna, gdy zrozumiesz zamieszanie / mit kryjący się za tablicami i wskaźnikami. Powszechnie przyjmuje się, że są takie same, ale nie są. Mit ten wynika z faktu, że wskaźniki można subskrybować podobnie jak tablice, a ponieważ tablice rozpadają się na wskaźniki na najwyższym poziomie w deklaracji funkcji. Jednak gdy tablica rozpadnie się na wskaźnik, wskaźnik traci
sizeof
informacje. Podaje więcsizeof(pointer)
rozmiar wskaźnika w bajtach, który zwykle wynosi 8 bajtów w systemie 64-bitowym.Nie możesz przypisywać tablic, tylko je inicjuj. Na przykład:
Z drugiej strony możesz robić, co chcesz, korzystając ze wskaźników. Niestety, ponieważ różnice między wskaźnikami i tablicami są ręcznie pomachane w Javie i C #, początkujący nie rozumieją różnicy.
Java i C # mają funkcje, które pozwalają traktować obiekty jak inne, na przykład używając
as
słowa kluczowego. Więc jeśli ktoś chciałby traktowaćEntity
obiekt jakoPlayer
obiekt, można to zrobić.Player player = Entity as Player;
Jest to bardzo przydatne, jeśli zamierzasz wywoływać funkcje na jednorodnym kontenerze, które powinny mieć zastosowanie tylko do określonego typu. Funkcjonalność można osiągnąć w podobny sposób poniżej:Powiedzmy, że gdyby tylko trójkąty miały funkcję obracania, byłby to błąd kompilatora, gdybyś próbował wywołać ją na wszystkich obiektach klasy. Za pomocą
dynamic_cast
możesz symulowaćas
słowo kluczowe. Aby wyjaśnić, jeśli rzutowanie nie powiedzie się, zwraca nieprawidłowy wskaźnik.!test
Jest to zatem w skrócie sprawdzenie, czytest
NULL lub nieprawidłowy wskaźnik, co oznacza, że rzutowanie nie powiodło się.Korzyści ze zmiennych automatycznych
Po zobaczeniu wszystkich wspaniałych rzeczy, które może zrobić dynamiczna alokacja, prawdopodobnie zastanawiasz się, dlaczego nikt NIE powinien używać dynamicznej alokacji przez cały czas? Powiedziałem ci już jeden powód, że stos jest wolny. A jeśli nie potrzebujesz całej tej pamięci, nie powinieneś jej nadużywać. Oto kilka wad w nie określonej kolejności:
Jest podatny na błędy. Ręczne przydzielanie pamięci jest niebezpieczne i masz skłonność do wycieków. Jeśli nie jesteś biegły w używaniu debugera lub
valgrind
(narzędzia do wycieku pamięci), możesz wyciągnąć włosy z głowy. Na szczęście idiomy RAII i inteligentne wskaźniki nieco to łagodzą, ale musisz znać takie praktyki, jak Zasada Trzech i Reguła Pięciu. Jest wiele informacji do przyjęcia, a początkujący, którzy albo nie wiedzą, albo nie przejmują się, wpadną w tę pułapkę.To nie jest konieczne. W przeciwieństwie do Java i C #, gdzie idiomatyczne jest używanie
new
słowa kluczowego wszędzie, w C ++ powinieneś go używać tylko wtedy, gdy jest to konieczne. Mówi się, że jeśli masz młotek, wszystko wygląda jak gwóźdź. Podczas gdy początkujący, którzy zaczynają od C ++, boją się wskaźników i uczą się używania zmiennych stosu według przyzwyczajenia, programiści Java i C # zaczynają od używania wskaźników bez ich zrozumienia! To dosłownie stąpanie po niewłaściwej stopie. Musisz porzucić wszystko, co wiesz, ponieważ składnia to jedno, uczenie się języka to drugie.Jedną z optymalizacji wielu kompilatorów są tzw . Optymalizacja elision i return . Te rzeczy mogą wyeliminować niepotrzebne kopie, co jest przydatne w przypadku bardzo dużych obiektów, takich jak wektor zawierający wiele elementów. Zwykle powszechną praktyką jest używanie wskaźników do przenoszenia własności, a nie kopiowanie dużych obiektów w celu ich przenoszenia . Doprowadziło to do powstania semantyki ruchów i inteligentnych wskaźników .
Jeśli używasz wskaźników, (N) RVO NIE występuje. Bardziej korzystne i mniej podatne na błędy jest skorzystanie z (N) RVO zamiast zwracania lub przekazywania wskaźników, jeśli martwisz się optymalizacją. Wycieki błędów mogą się zdarzyć, jeśli wywoływacz funkcji jest odpowiedzialny za wywołanie
delete
dynamicznie przydzielanego obiektu i tym podobne. Śledzenie własności obiektu może być trudne, jeśli wskaźniki są przekazywane jak gorący ziemniak. Wystarczy użyć zmiennych stosu, ponieważ jest to prostsze i lepsze.źródło
{ std::string* s = new std::string; } delete s; // destructor called
... na pewno todelete
nie zadziała, ponieważ kompilator nie będzie już wiedział, cos
jest?C ++ oferuje trzy sposoby przekazywania obiektu: przez wskaźnik, przez referencję i wartość. Java ogranicza Cię do tego drugiego (jedynym wyjątkiem są prymitywne typy, takie jak int, boolean itp.). Jeśli chcesz używać C ++ nie tylko jako dziwnej zabawki, lepiej poznaj różnicę między tymi trzema sposobami.
Java udaje, że nie ma takiego problemu jak „kto i kiedy powinien to zniszczyć?”. Odpowiedź brzmi: śmieciarz, wielki i okropny. Niemniej jednak nie może zapewnić 100% ochrony przed wyciekiem pamięci (tak, Java może wyciekać pamięć ). W rzeczywistości GC daje fałszywe poczucie bezpieczeństwa. Im większy Twój SUV, tym dłuższa droga do ewakuatora.
C ++ pozostawia Cię twarzą w twarz z zarządzaniem cyklem życia obiektu. Cóż, istnieją sposoby, aby sobie z tym poradzić ( rodzina inteligentnych wskaźników , QObject w Qt i tak dalej), ale żadnego z nich nie można używać w trybie „odpal i zapomnij” jak GC: zawsze powinieneś pamiętać o obsłudze pamięci. Nie tylko powinieneś dbać o zniszczenie obiektu, ale także musisz unikać niszczenia tego samego obiektu więcej niż jeden raz.
Jeszcze się nie boisz? Ok: cykliczne odniesienia - zajmij się nimi sam, człowieku. I pamiętaj: zabij każdy obiekt dokładnie raz, my środowiska wykonawcze C ++ nie lubimy tych, którzy bawią się zwłokami, zostawiając martwych w spokoju.
Wracając do twojego pytania.
Gdy przekazujesz swój obiekt według wartości, a nie wskaźnika lub odwołania, kopiujesz obiekt (cały obiekt, czy to kilka bajtów, czy wielki zrzut bazy danych - jesteś wystarczająco inteligentny, aby unikać tego drugiego, nie są t you?) za każdym razem, gdy to robisz '='. Aby uzyskać dostęp do elementów obiektu, użyj „.” (kropka).
Gdy przekazujesz swój obiekt wskaźnikiem, kopiujesz tylko kilka bajtów (4 w systemach 32-bitowych, 8 w systemach 64-bitowych), a mianowicie - adres tego obiektu. Aby pokazać to wszystkim, używasz tego fantazyjnego operatora „->”, gdy uzyskujesz dostęp do członków. Lub możesz użyć kombinacji „*” i „.”.
Kiedy używasz referencji, otrzymujesz wskaźnik, który udaje wartość. Jest to wskaźnik, ale dostęp do członków można uzyskać za pomocą „.”.
I jeszcze raz oszołomić: kiedy deklarujesz kilka zmiennych oddzielonych przecinkami, to (patrz na ręce):
Przykład:
źródło
std::auto_ptr
jest przestarzałe, nie używaj go.W C ++ obiekty przydzielone na stosie (przy użyciu
Object object;
instrukcji w bloku) będą istnieć tylko w zakresie, w którym zostały zadeklarowane. Gdy blok kodu zakończy wykonywanie, zadeklarowany obiekt zostaje zniszczony. Natomiast jeśli przydzielisz pamięć na stercie, używającObject* obj = new Object()
, będą one nadal żyć na stercie, dopóki nie zadzwoniszdelete obj
.Chciałbym utworzyć obiekt na stercie, gdy chcę użyć obiektu nie tylko w bloku kodu, który go zadeklarował / przydzielił.
źródło
Object obj
nie zawsze znajduje się na stosie - na przykład globalne lub zmienne składowe.Porównam, jak to działa w ciele funkcji, jeśli użyjesz:
Wewnątrz funkcji
myObject
zostaniesz zniszczony, gdy ta funkcja powróci. Jest to więc przydatne, jeśli nie potrzebujesz obiektu poza funkcją. Ten obiekt zostanie umieszczony na bieżącym stosie wątków.Jeśli piszesz w treści funkcji:
wskazana instancja klasy Object
myObject
nie zostanie zniszczona po zakończeniu funkcji, a przydział będzie na stercie.Teraz, jeśli jesteś programistą Java, drugi przykład jest bliżej tego, jak działa alokacja obiektów w java. Ta linia:
Object *myObject = new Object;
jest równoważne Java:Object myObject = new Object();
. Różnica polega na tym, że w java myObject zostanie zebrane śmieci, podczas gdy w c ++ nie zostanie uwolnione, musisz gdzieś wyraźnie nazwać `delete myObject; ' w przeciwnym razie wprowadzisz wycieki pamięci.Od wersji c ++ 11 możesz używać bezpiecznych sposobów dynamicznej alokacji:
new Object
przechowując wartości w shared_ptr / unique_ptr.również obiekty są bardzo często przechowywane w pojemnikach, takich jak mapy lub wektory, automatycznie zarządzają czasem życia twoich obiektów.
źródło
then myObject will not get destroyed once function ends
To absolutnie będzie.myObject
nadal będzie zniszczony, tak jak każda inna zmienna lokalna. Różnica polega na tym, że jego wartością jest wskaźnik do obiektu, a nie sam obiekt, a zniszczenie głupiego wskaźnika nie wpływa na jego pointe. Więc obiekt przetrwa wspomniane zniszczenie.Technicznie jest to kwestia alokacji pamięci, ale tutaj są dwa inne praktyczne aspekty tego. Ma to związek z dwiema rzeczami: 1) Zakres, kiedy definiujesz obiekt bez wskaźnika, nie będziesz już mógł uzyskać do niego dostępu po bloku kodu, w którym jest zdefiniowany, natomiast jeśli zdefiniujesz wskaźnik za pomocą „nowego”, to może uzyskać do niego dostęp z dowolnego miejsca, w którym znajduje się wskaźnik do tej pamięci, dopóki nie wywołasz „usuń” na tym samym wskaźniku. 2) Jeśli chcesz przekazać argumenty do funkcji, chcesz przekazać wskaźnik lub odwołanie, aby być bardziej wydajnym. Gdy przekazujesz Obiekt, obiekt jest kopiowany, jeśli jest to obiekt, który zużywa dużo pamięci, może to pochłaniać procesor (np. Kopiujesz wektor pełen danych). Gdy przekazujesz wskaźnik, wszystko, co przekazujesz, to jedna int (w zależności od implementacji, ale większość z nich to jedna int).
Poza tym musisz zrozumieć, że „nowy” przydziela pamięć na stercie, która w pewnym momencie musi zostać zwolniona. Gdy nie musisz używać „nowego”, sugeruję użycie zwykłej definicji obiektu „na stosie”.
źródło
Cóż, główne pytanie brzmi: dlaczego powinienem używać wskaźnika zamiast samego obiektu? I moja odpowiedź: nie powinieneś (prawie) nigdy używać wskaźnika zamiast obiektu, ponieważ C ++ ma odwołania , jest bezpieczniejszy niż wskaźniki i gwarantuje taką samą wydajność jak wskaźniki.
Kolejna rzecz, o której wspomniałeś w swoim pytaniu:
Jak to działa? Tworzy wskaźnik
Object
typu, przydziela pamięć na jeden obiekt i wywołuje domyślnego konstruktora, brzmi dobrze, prawda? Ale tak naprawdę nie jest tak dobrze, jeśli dynamicznie przydzielasz pamięć (używane słowo kluczowenew
), musisz również ręcznie zwolnić pamięć, co oznacza, że w kodzie powinieneś mieć:To wywołuje destruktor i zwalnia pamięć, wygląda na łatwe, jednak w dużych projektach może być trudne do wykrycia, czy pamięć zwolniła jeden wątek, czy nie, ale w tym celu możesz wypróbować wspólne wskaźniki , co nieco zmniejsza wydajność, ale znacznie łatwiej jest pracować z im.
A teraz trochę wstępu i wróć do pytania.
Możesz użyć wskaźników zamiast obiektów, aby uzyskać lepszą wydajność podczas przesyłania danych między funkcjami.
Spójrz, masz
std::string
(to także obiekt) i zawiera naprawdę dużo danych, na przykład duży XML, teraz musisz go przeanalizować, ale do tego masz funkcję,void foo(...)
którą można zadeklarować na różne sposoby:void foo(std::string xml);
W takim przypadku skopiujesz wszystkie dane ze zmiennej do stosu funkcji, zajmuje to trochę czasu, więc Twoja wydajność będzie niska.void foo(std::string* xml);
W takim przypadku przekażesz wskaźnik do obiektu z tą samą prędkością co przekazywaniesize_t
zmiennej, jednak deklaracja ta jest podatna na błędy, ponieważ możesz przekazaćNULL
wskaźnik lub nieprawidłowy wskaźnik. Wskaźniki zwykle używane w,C
ponieważ nie ma referencji.void foo(std::string& xml);
Tutaj przekazujesz referencję, w zasadzie jest to to samo, co przekazywanie wskaźnika, ale kompilator wykonuje pewne czynności i nie możesz przekazać nieprawidłowej referencji (w rzeczywistości możliwe jest stworzenie sytuacji z nieprawidłową referencją, ale to oszukiwanie kompilatora).void foo(const std::string* xml);
Tutaj jest to samo co drugi, tylko wartości wskaźnika nie można zmienić.void foo(const std::string& xml);
Tutaj jest to samo co trzecie, ale wartości obiektu nie można zmienić.Co więcej chcę wspomnieć, możesz użyć tych 5 sposobów przekazywania danych bez względu na wybrany sposób alokacji (z
new
lub zwykły ).Inną rzeczą, o której należy wspomnieć, gdy tworzysz obiekt w zwykły sposób, alokujesz pamięć na stosie, ale podczas tworzenia go
new
przydzielasz stertę. Przydzielanie stosu jest znacznie szybsze, ale jest trochę małe dla naprawdę dużych tablic danych, więc jeśli potrzebujesz dużego obiektu, powinieneś użyć sterty, ponieważ możesz dostać przepełnienie stosu, ale zwykle ten problem rozwiązuje się za pomocą kontenerów STL i pamiętajstd::string
jest też kontener, niektórzy faceci zapomnieli :)źródło
Powiedzmy, że masz to
class A
,class B
co chcesz. Kiedy chcesz wywołać jakąś funkcjęclass B
zewnętrznąclass A
, po prostu uzyskasz wskaźnik do tej klasy i możesz zrobić, co chcesz, a także zmieni kontekstclass B
w twoimclass A
Uważaj jednak na dynamiczny obiekt
źródło
Istnieje wiele zalet używania wskaźników do obiektów -
źródło
Jest to omówione szczegółowo, ale w Javie wszystko jest wskaźnikiem. Nie rozróżnia alokacji stosu i sterty (wszystkie obiekty są przydzielane na sterty), więc nie zdajesz sobie sprawy, że używasz wskaźników. W C ++ możesz je mieszać, w zależności od wymagań dotyczących pamięci. Wydajność i wykorzystanie pamięci są bardziej deterministyczne w C ++ (duh).
źródło
Spowoduje to utworzenie odwołania do obiektu (na stercie), który należy jawnie usunąć, aby uniknąć wycieku pamięci .
Spowoduje to utworzenie obiektu (myObject) typu automatycznego (na stosie), który zostanie automatycznie usunięty, gdy obiekt (myObject) wyjdzie poza zakres.
źródło
Wskaźnik bezpośrednio odwołuje się do lokalizacji pamięci obiektu. Java nie ma nic takiego. Java ma odwołania, które odwołują się do lokalizacji obiektu poprzez tabele skrótów. Za pomocą tych odniesień nie można robić niczego takiego jak arytmetyka wskaźników w Javie.
Aby odpowiedzieć na twoje pytanie, to tylko twoje preferencje. Wolę używać składni podobnej do Java.
źródło
Ze wskaźnikami ,
może bezpośrednio rozmawiać z pamięcią.
może zapobiec wyciekom pamięci z programu przez manipulowanie wskaźnikami.
źródło
Jednym z powodów używania wskaźników jest interfejs z funkcjami C. Innym powodem jest oszczędność pamięci; na przykład: zamiast przekazywać do obiektu obiekt, który zawiera dużo danych i ma konstruktor intensywnie kopiujący do funkcji, wystarczy przekazać wskaźnik do obiektu, oszczędzając pamięć i szybkość, szczególnie jeśli jesteś w pętli, jednak odniesienie byłoby lepsze w takim przypadku, chyba że używasz tablicy w stylu C.
źródło
W obszarach, w których wykorzystanie pamięci jest na wagę złota, przydatne są wskaźniki. Na przykład rozważmy algorytm minimax, w którym tysiące węzłów zostaną wygenerowane przy użyciu procedury rekurencyjnej, a następnie wykorzystamy je do oceny następnego najlepszego ruchu w grze, możliwość cofnięcia przydziału lub zresetowania (jak w inteligentnych wskaźnikach) znacznie zmniejsza zużycie pamięci. Podczas gdy zmienna nieinterpretacyjna nadal zajmuje miejsce, dopóki jej rekurencyjne wywołanie nie zwróci wartości.
źródło
Dołączę jeden ważny przypadek użycia wskaźnika. Kiedy przechowujesz jakiś obiekt w klasie bazowej, ale może on być polimorficzny.
W takim przypadku nie możesz zadeklarować bObj jako obiektu bezpośredniego, musisz mieć wskaźnik.
źródło
"Potrzeba jest matka wynalazku." Najważniejszą różnicą, na którą chciałbym zwrócić uwagę, jest wynik własnego doświadczenia w kodowaniu. Czasami musisz przekazać obiekty do funkcji. W takim przypadku, jeśli twój obiekt należy do bardzo dużej klasy, wówczas przekazanie go jako obiektu spowoduje skopiowanie jego stanu (czego możesz nie chcieć .. I MOŻNA BYĆ WIELKI PRZEKRÓJ), w wyniku czego powstanie narzut związany z kopiowaniem obiektu. Podczas gdy wskaźnik jest naprawiony Rozmiar 4-bajtowy (przy założeniu 32 bitów). Inne powody są już wspomniane powyżej ...
źródło
std::string test;
którą mamy,void func(const std::string &) {}
ale chyba, że funkcja musi zmienić dane wejściowe, w takim przypadku zalecam użycie wskaźników (aby każdy, kto czyta kod, zauważył&
i zrozumiał, że funkcja może zmienić dane wejściowe)Istnieje już wiele doskonałych odpowiedzi, ale dam wam jeden przykład:
Mam prostą klasę przedmiotów:
Tworzę wektor, aby pomieścić kilka z nich.
std::vector<Item> inventory;
Tworzę milion obiektów Przedmiotów i pcham je z powrotem na wektor. Sortuję wektor według nazwy, a następnie wykonuję proste iteracyjne wyszukiwanie binarne konkretnej nazwy elementu. Testuję program, a jego ukończenie zajmuje ponad 8 minut. Następnie zmieniam wektor ekwipunku tak:
std::vector<Item *> inventory;
... i stwórz mój milion przedmiotów Przedmiotów za pomocą nowego. JEDYNIE zmiany, które wprowadzam w kodzie, to użycie wskaźników do pozycji, z wyjątkiem pętli, którą dodaję do czyszczenia pamięci na końcu. Ten program działa w czasie krótszym niż 40 sekund lub lepszym niż 10-krotny wzrost prędkości. EDYCJA: Kod znajduje się na stronie http://pastebin.com/DK24SPeW Z optymalizacjami kompilatora pokazuje tylko 3,4-krotny wzrost na komputerze, na którym właśnie go testowałem, co jest nadal znaczące.
źródło
push_back
. Oczywiście te kopie. Powinieneś byćemplace
w miejscu podczas tworzenia swoich obiektów (chyba że potrzebujesz ich do buforowania w innym miejscu).