Dlaczego powinienem używać wskaźnika zamiast samego obiektu?

1599

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ę?

gEdringer
źródło
403
Uznanie dla ciebie za kwestionowanie tej praktyki, a nie tylko jej przestrzeganie. Przez większość czasu wskaźniki są przeładowane.
Luchian Grigore
118
Jeśli nie widzisz powodu, aby używać wskaźników, nie rób tego. Wolę przedmioty. Preferuj obiekty przed Unique_ptr przed shared_ptr przed surowymi wskaźnikami.
Stefan
111
Uwaga: w java wszystko (oprócz podstawowych typów) jest wskaźnikiem. więc powinieneś raczej zapytać przeciwnie: dlaczego potrzebuję prostych przedmiotów?
Karoly Horvath
117
Zauważ, że w Javie wskaźniki są ukryte przez składnię. W C ++ różnica między wskaźnikiem a wskaźnikiem innym niż wskaźnik jest wyraźnie zaznaczona w kodzie. Java używa wskaźników wszędzie.
Daniel Martín
214
Zamknięty jako zbyt szeroki ? Poważnie? Proszę zauważyć, że ten sposób programowania w języku Java ++ jest bardzo powszechny i ​​stanowi jeden z najważniejszych problemów w społeczności C ++ . Należy to potraktować poważnie.
Manu343726,

Odpowiedzi:

1570

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 zrobisz new Object(), obiekt ma dynamiczny czas przechowywania, co oznacza, że ​​pozostaje przy życiu, dopóki go nie wyrazisz delete. 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:

  1. Potrzebujesz obiektu, aby przeżyć bieżący zakres - ten konkretny obiekt w tej konkretnej lokalizacji pamięci, a nie jego kopię. Jeśli nie masz nic przeciwko kopiowaniu / przenoszeniu obiektu (przez większość czasu powinieneś), powinieneś wybrać obiekt automatyczny.
  2. Musisz przydzielić dużo pamięci , co może łatwo wypełnić stos. Byłoby miło, gdybyśmy nie musieli się tym przejmować (przez większość czasu nie powinieneś tego robić), ponieważ tak naprawdę to nie leży w zakresie C ++, ale niestety musimy poradzić sobie z rzeczywistością systemów rozwijamy się dla.

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_ptri std::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 .

  1. 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.

  2. 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.

  3. Chcesz reprezentować, że obiekt jest opcjonalny , umożliwiając nullptrprzekazanie 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żyj boost::optional).

  4. 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 .

  5. 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 getfunkcji 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.

Joseph Mansfield
źródło
82
„Potrzebujesz obiektu, aby przeżyć bieżący zakres”. - Dodatkowa uwaga na ten temat: są przypadki, w których wydaje się, że potrzebujesz obiektu, aby przeżyć obecny zakres, ale tak naprawdę nie jest. Na przykład, jeśli umieścisz swój obiekt w wektorze, obiekt zostanie skopiowany (lub przeniesiony) do wektora, a oryginalny obiekt można bezpiecznie zniszczyć po zakończeniu jego zakresu.
25
Pamiętaj, że teraz s / copy / move / w wielu miejscach. Zwrócenie obiektu zdecydowanie nie oznacza ruchu. Należy również pamiętać, że dostęp do obiektu za pomocą wskaźnika jest prostopadły do ​​sposobu jego utworzenia.
Szczeniak
15
Brakuje wyraźnego odniesienia do RAII w tej odpowiedzi. C ++ jest wszystkim (prawie wszystkim) na temat zarządzania zasobami, a RAII jest sposobem, aby to zrobić w C ++ (I główny problem generowany przez surowe wskaźniki: Breaking RAII)
Manu343726,
11
Inteligentne wskaźniki istniały przed C ++ 11, np. Boost :: shared_ptr i boost :: scoped_ptr. Inne projekty mają swój własny odpowiednik. Nie można uzyskać semantyki ruchu, a przypisanie std :: auto_ptr jest wadliwe, więc C ++ 11 poprawia rzeczy, ale porady są nadal dobre. (I smutny nitpick, to nie wystarczy, aby mieć dostęp do w C ++ 11 kompilatora, jest konieczne, aby wszystkie kompilatory można chcieć swój kod do pracy z pomocy c ++ 11. Tak, Oracle Solaris Studio, jestem patrzy na ciebie.)
armb
7
@ MDMoore313 Możesz pisaćObject myObject(param1, etc...)
użytkownik000001
171

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:

class Base { ... };
class Derived : public Base { ... };

void fun(Base b) { ... }
void gun(Base* b) { ... }
void hun(Base& b) { ... }

Derived d;
fun(d);    // oops, all Derived parts silently "sliced" off
gun(&d);   // OK, a Derived object IS-A Base object
hun(d);    // also OK, reference also doesn't slice

Odwołaj się do semantyki i unikaj kopiowania . W przypadku typów niepolimorficznych wskaźnik (lub odniesienie) pozwoli uniknąć kopiowania potencjalnie drogiego obiektu

Base b;
fun(b);  // copies b, potentially expensive 
gun(&b); // takes a pointer to b, no copying
hun(b);  // regular syntax, behaves as a pointer

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ą newoperatora 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<>lub std::shared_ptr<>). Rozważać:

{
    auto b = new Base;
    ...       // oops, if an exception is thrown, destructor not called!
    delete b;
}

vs.

{
    auto b = std::make_unique<Base>();
    ...       // OK, now exception safe
}

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 newani bezpośrednio przez ciebie, ani wewnątrz klasy zasobów) są niszczone, gdy wykraczają poza zakres.

TemplateRex
źródło
17
„Tworzenie wskaźnika do zasobu za pomocą nowego operatora jest anty-wzorcem”. Myślę, że możesz nawet ulepszyć to, że posiadanie surowego wskaźnika posiadającego coś jest anty-wzorcem . Nie tylko tworzenie, ale przekazywanie surowych wskaźników jako argumentów lub zwracanych wartości sugerujących przeniesienie własności IMHO jest przestarzałe, ponieważ unique_ptrsemantyka / move
dyp
1
@dyp tnx, zaktualizowano i odniesiono do pytań i odpowiedzi dotyczących C ++ FAQ na ten temat.
TemplateRex,
4
Używanie inteligentnych wskaźników wszędzie jest anty-wzorem. Jest kilka specjalnych przypadków, w których ma to zastosowanie, ale przez większość czasu ten sam powód, który przemawia za alokacją dynamiczną (dowolne życie), przemawia również przeciwko zwykłym inteligentnym wskaźnikom.
James Kanze
2
@JamesKanze Nie chciałem sugerować, że inteligentne wskaźniki powinny być używane wszędzie, tylko do własności, a także, że surowe wskaźniki nie powinny być używane do własności, ale tylko do widoków.
TemplateRex
2
@TemplateRex To wydaje się nieco głupie, biorąc pod uwagę, że hun(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&.
JAB
130

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:

Object object1 = new Object(); //A new object is allocated by Java
Object object2 = new Object(); //Another new object is allocated by Java

object1 = 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

Najbliższym odpowiednikiem tego jest:

C ++:

Object * object1 = new Object(); //A new object is allocated on the heap
Object * object2 = new Object(); //Another new object is allocated on the heap
delete 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.

Zobaczmy alternatywny sposób C ++:

Object object1; //A new object is allocated on the STACK
Object object2; //Another new object is allocated on the STACK
object1 = 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...

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:

int object1; //An integer is allocated on the stack.
int object2; //Another integer is allocated on the stack.
object1 = object2; //The value of object2 is copied to object1.

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 #).

Gerasimos R.
źródło
7
Object2 is now "dead": Myślę, że masz na myśli myObject1lub ściślej the object pointed to by myObject1.
Clément,
2
W rzeczy samej! Przeformułowałem trochę.
Gerasimos R
2
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 surowych newplików, powinieneś zawinąć newobiekty jak najszybciej w opakowania RAII.
PSkocik,
8
Rzeczywiście, byłoby tak, gdyby był to program i nic innego się wokół niego nie działo. Na szczęście jest to tylko fragment wyjaśniający pokazujący, jak zachowuje się wskaźnik w C ++ - i jedno z niewielu miejsc, w których obiekt RAII nie może być zastąpiony surowym wskaźnikiem, studiuje i uczy się o surowych wskaźnikach ...
Gerasimos R
80

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.

Spalony Toast
źródło
7
to naprawdę dodaje do użytecznych informacji, więc cieszę się, że udzieliłeś odpowiedzi!
TemplateRex,
3
std :: shared_ptr <T> działa również z przednimi deklaracjami T. (std :: unique_ptr <T> nie działa )
berkus
13
@berkus: std::unique_ptr<T>działa z deklaracjami przesyłania dalej T. Musisz tylko upewnić się, że gdy std::unique_ptr<T>wywoływany jest destruktor , Tjest to kompletny typ. Zazwyczaj oznacza to, że klasa, która zawiera std::unique_ptr<T>deklarator destruktora w pliku nagłówkowym i implementuje go w pliku cpp (nawet jeśli implementacja jest pusta).
David Stone
Czy moduły to naprawią?
Trevor Hickey
@ TrevorHickey Stary komentarz Wiem, ale i tak na nie odpowiedzieć. Moduły nie usuwają zależności, ale powinny sprawić, że włączenie zależności będzie bardzo tanie, prawie bezpłatne pod względem kosztów wydajności. Ponadto, jeśli ogólne przyspieszenie z modułów wystarczyłoby, aby uzyskać czasy kompilacji w akceptowalnym zakresie, nie stanowi to już problemu.
Aidiakapi
78

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

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ę?

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:

{
    std::string s;
}
// s is destroyed here

Z drugiej strony, jeśli użyjesz wskaźnika przydzielanego dynamicznie, jego destruktor musi zostać wywołany ręcznie. deletenazywa to Destruktorem.

{
    std::string* s = new std::string;
}
delete s; // destructor called

Nie ma to nic wspólnego ze newskładnią rozpowszechnioną w C # i Javie. Są używane do zupełnie innych celów.

Korzyści z dynamicznej alokacji

1. Nie musisz z góry znać rozmiaru tablicy

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:

char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow

Oczywiście, jeśli użyłeś std::stringzamiast tego, std::stringrozmiar 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:

int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];

Uwaga dodatkowa : Jednym z błędów wielu początkujących jest użycie tablic o zmiennej długości. Jest to rozszerzenie GNU, a także w Clang, ponieważ odzwierciedla wiele rozszerzeń GCC. Dlatego int arr[n]nie należy polegać na następujących kwestiach.

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.

2. Tablice nie są wskaźnikami

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 sizeofinformacje. Podaje więc sizeof(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:

int arr[5] = {1, 2, 3, 4, 5}; // initialization 
int arr[] = {1, 2, 3, 4, 5}; // The standard dictates that the size of the array
                             // be given by the amount of members in the initializer  
arr = { 1, 2, 3, 4, 5 }; // ERROR

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.

3. Polimorfizm

Java i C # mają funkcje, które pozwalają traktować obiekty jak inne, na przykład używając assłowa kluczowego. Więc jeśli ktoś chciałby traktować Entityobiekt jako Playerobiekt, 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:

std::vector<Base*> vector;
vector.push_back(&square);
vector.push_back(&triangle);
for (auto& e : vector)
{
     auto test = dynamic_cast<Triangle*>(e); // I only care about triangles
     if (!test) // not a triangle
        e.GenericFunction();
     else
        e.TriangleOnlyMagic();
}

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_castmożesz symulować assłowo kluczowe. Aby wyjaśnić, jeśli rzutowanie nie powiedzie się, zwraca nieprawidłowy wskaźnik. !testJest 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 newsł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.

1. (N) RVO - Aka, (nazwana) Optymalizacja wartości zwracanej

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 deletedynamicznie 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.

użytkownik3391320
źródło
„Więc! Test jest w skrócie skrótem do sprawdzania, czy test ma wartość NULL lub nieprawidłowy wskaźnik, co oznacza, że ​​rzutowanie nie powiodło się.” Myślę, że to zdanie musi zostać przepisane dla zachowania przejrzystości.
berkus
4
„Maszyna do hype Java chciałabyś w to uwierzyć” - może w 1997 roku, ale teraz jest to anachroniczne, nie ma już motywacji, aby porównywać Javę do C ++ w 2014 roku.
Matt R
15
Stare pytanie, ale w segmencie kodu { std::string* s = new std::string; } delete s; // destructor called... na pewno to deletenie zadziała, ponieważ kompilator nie będzie już wiedział, co sjest?
badger5000
2
NIE daję -1, ale nie zgadzam się z oświadczeniami otwierającymi, jak napisano. Po pierwsze, nie zgadzam się z tym, że istnieje jakikolwiek „szum” - mógł być w okolicach Y2K, ale teraz oba języki są dobrze rozumiane. Po drugie, twierdzę, że są dość podobne - C ++ jest dzieckiem C poślubionym z Simulą, Java dodaje Virtual Machine, Garbage Collector i HEAVILY ogranicza funkcje, a C # usprawnia i przywraca brakujące funkcje do Javy. Tak, to sprawia, że ​​wzorce i poprawne użycie są BARDZO różne, ale korzystne jest zrozumienie wspólnej infrastruktury / projektu, aby można było zobaczyć różnice.
Gerasimos R
1
@James Matta: Oczywiście masz rację, że pamięć jest pamięcią i obie są przydzielane z tej samej pamięci fizycznej, ale jedną rzeczą do rozważenia jest to, że bardzo często uzyskuje się lepszą charakterystykę wydajności podczas pracy z obiektami przydzielonymi do stosu, ponieważ stos - lub przynajmniej jego najwyższe poziomy - mają bardzo dużą szansę na bycie „gorącym” w pamięci podręcznej, gdy funkcje wchodzą i wychodzą, podczas gdy sterta nie ma takiej korzyści, więc jeśli ścigasz wskaźnik w stosie, możesz otrzymać wiele braków pamięci podręcznej, które prawdopodobnie nie będziesz na stosie. Ale cała ta „losowość” zwykle sprzyja stosowi.
Gerasimos R
23

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):

  • Rodzaj jest podawany wszystkim
  • Modyfikator wartości / wskaźnika / odniesienia jest indywidualny

Przykład:

struct MyStruct
{
    int* someIntPointer, someInt; //here comes the surprise
    MyStruct *somePointer;
    MyStruct &someReference;
};

MyStruct s1; //we allocated an object on stack, not in heap

s1.someInt = 1; //someInt is of type 'int', not 'int*' - value/pointer modifier is individual
s1.someIntPointer = &s1.someInt;
*s1.someIntPointer = 2; //now s1.someInt has value '2'
s1.somePointer = &s1;
s1.someReference = s1; //note there is no '&' operator: reference tries to look like value
s1.somePointer->someInt = 3; //now s1.someInt has value '3'
*(s1.somePointer).someInt = 3; //same as above line
*s1.somePointer->someIntPointer = 4; //now s1.someInt has value '4'

s1.someReference.someInt = 5; //now s1.someInt has value '5'
                              //although someReference is not value, it's members are accessed through '.'

MyStruct s2 = s1; //'NO WAY' the compiler will say. Go define your '=' operator and come back.

//OK, assume we have '=' defined in MyStruct

s2.someInt = 0; //s2.someInt == 0, but s1.someInt is still 5 - it's two completely different objects, not the references to the same one
Kirill Gamazkov
źródło
1
std::auto_ptrjest przestarzałe, nie używaj go.
Neil
2
Jestem całkiem pewien, że nie możesz mieć referencji jako członka bez dostarczenia konstruktorowi listy inicjalizacyjnej, która zawiera zmienną referencyjną. (Odwołanie należy natychmiast zainicjować. Nawet ciało konstruktora jest za późno, aby je ustawić, IIRC.)
cHao
20

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ąc Object* obj = new Object(), będą one nadal żyć na stercie, dopóki nie zadzwonisz delete obj.

Chciałbym utworzyć obiekt na stercie, gdy chcę użyć obiektu nie tylko w bloku kodu, który go zadeklarował / przydzielił.

Karthik Kalyanasundaram
źródło
6
Object objnie zawsze znajduje się na stosie - na przykład globalne lub zmienne składowe.
tenfour
2
@LightnessRacesinOrbit Wspomniałem tylko o obiektach przydzielonych w bloku, a nie o zmiennych globalnych i zmiennych członkowskich. Chodzi o to, że nie było to jasne, teraz to poprawiłem - dodano „w bloku” w odpowiedzi. Mam nadzieję, że teraz nie są to fałszywe informacje :)
Karthik Kalyanasundaram
20

Ale nie mogę zrozumieć, dlaczego powinniśmy tego tak używać?

Porównam, jak to działa w ciele funkcji, jeśli użyjesz:

Object myObject;

Wewnątrz funkcji myObjectzostaniesz 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:

 Object *myObject = new Object;

wskazana instancja klasy Object myObjectnie 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 Objectprzechowując wartości w shared_ptr / unique_ptr.

std::shared_ptr<std::string> safe_str = make_shared<std::string>("make_shared");

// since c++14
std::unique_ptr<std::string> safe_str = make_unique<std::string>("make_shared"); 

również obiekty są bardzo często przechowywane w pojemnikach, takich jak mapy lub wektory, automatycznie zarządzają czasem życia twoich obiektów.

marcinj
źródło
1
then myObject will not get destroyed once function endsTo absolutnie będzie.
Wyścigi lekkości na orbicie
6
W przypadku wskaźnika, myObjectnadal 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.
cHao
Naprawiono to, że zmienne lokalne (w tym wskaźnik) oczywiście zostaną zwolnione - są na stosie.
marcinj
13

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”.

potrzebuje pomocy
źródło
6

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:

Object *myObject = new Object;

Jak to działa? Tworzy wskaźnik Objecttypu, 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 kluczowe new), musisz również ręcznie zwolnić pamięć, co oznacza, że ​​w kodzie powinieneś mieć:

delete myObject;

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:

  1. 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.
  2. void foo(std::string* xml); W takim przypadku przekażesz wskaźnik do obiektu z tą samą prędkością co przekazywanie size_tzmiennej, jednak deklaracja ta jest podatna na błędy, ponieważ możesz przekazać NULLwskaźnik lub nieprawidłowy wskaźnik. Wskaźniki zwykle używane w, Cponieważ nie ma referencji.
  3. 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).
  4. void foo(const std::string* xml); Tutaj jest to samo co drugi, tylko wartości wskaźnika nie można zmienić.
  5. 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 newlub 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 newprzydzielasz 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ętaj std::stringjest też kontener, niektórzy faceci zapomnieli :)

ST3
źródło
5

Powiedzmy, że masz to class A, class Bco chcesz. Kiedy chcesz wywołać jakąś funkcję class Bzewnętrzną class A, po prostu uzyskasz wskaźnik do tej klasy i możesz zrobić, co chcesz, a także zmieni kontekst class Bw twoimclass A

Uważaj jednak na dynamiczny obiekt

Zadanie
źródło
5

Istnieje wiele zalet używania wskaźników do obiektów -

  1. Wydajność (jak już wskazałeś). Przekazywanie obiektów do funkcji oznacza tworzenie nowych kopii obiektu.
  2. Praca z obiektami z bibliotek stron trzecich. Jeśli twój obiekt należy do kodu strony trzeciej, a autorzy zamierzają używać swoich obiektów tylko za pomocą wskaźników (bez konstruktorów kopiowania itp.), Jedynym sposobem na obejście tego obiektu jest użycie wskaźników. Przekazywanie wartości może powodować problemy. (Problemy z kopiowaniem głębokim / płytkim).
  3. jeśli obiekt jest właścicielem zasobu i chcesz, aby własność nie była dzielona z innymi obiektami.
Rohit
źródło
3

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).

cmollis
źródło
3
Object *myObject = new Object;

Spowoduje to utworzenie odwołania do obiektu (na stercie), który należy jawnie usunąć, aby uniknąć wycieku pamięci .

Object myObject;

Spowoduje to utworzenie obiektu (myObject) typu automatycznego (na stosie), który zostanie automatycznie usunięty, gdy obiekt (myObject) wyjdzie poza zakres.

Palak Jain
źródło
1

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.

RioRicoRick
źródło
Stoły haszyszowe? Może w niektórych maszynach JVM, ale nie licz na to.
Zan Lynx,
Co z JVM dostarczaną z Javą? Oczywiście możesz zaimplementować WSZYSTKO, co możesz myśleć o JVM, który używa wskaźników bezpośrednio lub o metodzie, która wykonuje matematykę wskaźnika. To tak, jakby powiedzieć „ludzie nie umierają na przeziębienie” i otrzymać odpowiedź „Być może większość ludzi nie umie, ale na to nie liczy!” Ha ha.
RioRicoRick
2
@RioRicoRick HotSpot implementuje referencje Java jako natywne wskaźniki, patrz docs.oracle.com/javase/7/docs/technotes/guides/vm/... O ile widzę, JRockit robi to samo. Obie obsługują kompresję OOP, ale żadna z nich nigdy nie używa tabel skrótów. Konsekwencje wydajności prawdopodobnie byłyby katastrofalne. Ponadto „to tylko twoja preferencja” wydaje się sugerować, że obie są jedynie różnymi składniami równoważnego zachowania, co oczywiście nie jest.
Max Barraclough,
0

Ze wskaźnikami ,

  • może bezpośrednio rozmawiać z pamięcią.

  • może zapobiec wyciekom pamięci z programu przez manipulowanie wskaźnikami.

lasan
źródło
4
w C ++, korzystając ze wskaźników, możesz utworzyć niestandardowy moduł wyrzucania elementów bezużytecznych dla własnego programu ”, który brzmi jak okropny pomysł.
ilościowo
0

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
0

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.

seccpur
źródło
0

Dołączę jeden ważny przypadek użycia wskaźnika. Kiedy przechowujesz jakiś obiekt w klasie bazowej, ale może on być polimorficzny.

Class Base1 {
};

Class Derived1 : public Base1 {
};


Class Base2 {
  Base *bObj;
  virtual void createMemerObects() = 0;
};

Class Derived2 {
  virtual void createMemerObects() {
    bObj = new Derived1();
  }
};

W takim przypadku nie możesz zadeklarować bObj jako obiektu bezpośredniego, musisz mieć wskaźnik.

użytkownik18853
źródło
-5

"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 ...

sandeep bisht
źródło
14
powinieneś przejechać przez odniesienie
bolov
2
Polecam przekazywanie stałej referencji jak dla zmiennej, 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)
Top- Master
-7

Istnieje już wiele doskonałych odpowiedzi, ale dam wam jeden przykład:

Mam prostą klasę przedmiotów:

 class Item
    {
    public: 
      std::string name;
      int weight;
      int price;
    };

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.

Darren
źródło
2
Czy porównujesz wtedy wskaźniki, czy nadal porównujesz rzeczywiste obiekty? Bardzo wątpię, że inny poziom pośredni może poprawić wydajność. Proszę podać kod! Czy potem odpowiednio sprzątasz?
Stefan
1
@stefan Porównuję dane (w szczególności pole nazwy) obiektów dla sortowania i wyszukiwania. Sprzątam właściwie, jak już wspomniałem w poście. przyspieszenie jest prawdopodobnie spowodowane dwoma czynnikami: 1) std :: vector push_back () kopiuje obiekty, więc wersja wskaźnika potrzebuje tylko jednego wskaźnika na obiekt. Ma to wiele wpływów na wydajność, ponieważ nie tylko mniej danych jest kopiowanych, ale również alokator pamięci klasy wektorowej jest mniej obciążony.
Darren
2
Oto kod pokazujący praktycznie brak różnicy w twoim przykładzie: sortowanie. Kod wskaźnika jest o 6% szybszy niż kod bez wskaźnika dla samego sortowania, ale ogólnie jest o 10% wolniejszy niż kod bez wskaźnika. ideone.com/G0c7zw
stefan
3
Słowo kluczowe: push_back. Oczywiście te kopie. Powinieneś być emplacew miejscu podczas tworzenia swoich obiektów (chyba że potrzebujesz ich do buforowania w innym miejscu).
underscore_d
1
Wektory wskaźników prawie zawsze są błędne. Proszę nie polecać ich bez szczegółowego wyjaśnienia zastrzeżeń i zalet i wad. Wygląda na to, że znalazłeś jednego profesjonalistę, który jest jedynie konsekwencją źle zakodowanego kontrprzykładu i wprowadziłeś go w błąd
Lightness Races in Orbit