Prawidłowe użycie stosu i sterty w C ++?

122

Od jakiegoś czasu zajmuję się programowaniem, ale to głównie Java i C #. Właściwie nigdy nie musiałem samodzielnie zarządzać pamięcią. Niedawno zacząłem programować w C ++ i jestem trochę zdezorientowany, kiedy powinienem przechowywać rzeczy na stosie, a kiedy przechowywać je na stercie.

Rozumiem, że zmienne, do których dostęp jest bardzo często, powinny być przechowywane na stosie i obiektach, rzadko używane zmienne, a duże struktury danych powinny być przechowywane na stercie. Czy to prawda, czy jestem nieprawidłowy?

Alexander
źródło

Odpowiedzi:

242

Nie, różnica między stosem a stertą nie polega na wydajności. Jest to żywotność: każda zmienna lokalna wewnątrz funkcji (wszystko, czego nie zrobisz malloc () lub new) żyje na stosie. Znika po powrocie z funkcji. Jeśli chcesz, aby coś żyło dłużej niż funkcja, która to zadeklarowała, musisz alokować to na stercie.

class Thingy;

Thingy* foo( ) 
{
  int a; // this int lives on the stack
  Thingy B; // this thingy lives on the stack and will be deleted when we return from foo
  Thingy *pointerToB = &B; // this points to an address on the stack
  Thingy *pointerToC = new Thingy(); // this makes a Thingy on the heap.
                                     // pointerToC contains its address.

  // this is safe: C lives on the heap and outlives foo().
  // Whoever you pass this to must remember to delete it!
  return pointerToC;

  // this is NOT SAFE: B lives on the stack and will be deleted when foo() returns. 
  // whoever uses this returned pointer will probably cause a crash!
  return pointerToB;
}

Aby lepiej zrozumieć, czym jest stos, podejdź do niego z drugiej strony - zamiast próbować zrozumieć, co robi stos w zakresie języka wysokiego poziomu, poszukaj „stosu wywołań” i „konwencji wywoływania” i zobacz, co maszyna naprawdę robi to, gdy wywołujesz funkcję. Pamięć komputera to tylko seria adresów; „heap” i „stack” to wynalazki kompilatora.

Crashworks
źródło
7
Można by było bezpiecznie dodać, że informacje o zmiennej wielkości zwykle trafiają na stos. Jedyne wyjątki, o których wiem, to VLA w C99 (który ma ograniczoną obsługę) i funkcja assigna (), która jest często źle rozumiana nawet przez programistów C.
Dan Olson
10
Dobre wyjaśnienie, chociaż w scenariuszu wielowątkowym z częstymi alokacjami i / lub cofnięciami alokacji, sterta jest punktem spornym, co wpływa na wydajność. Mimo to zakres jest prawie zawsze decydującym czynnikiem.
peterchen
18
Jasne, a new / malloc () samo w sobie jest powolną operacją, a stos jest bardziej prawdopodobny w dcache niż w dowolnym wierszu sterty. Są to prawdziwe względy, ale zwykle drugorzędne w stosunku do kwestii długości życia.
Crashworks
1
Czy to prawda "Pamięć komputera to tylko seria adresów;" sterta "i" stos "to wymysły kompilacji" ?? W wielu miejscach czytałem, że stos jest specjalnym obszarem pamięci naszego komputera.
Vineeth Chitteti
2
@kai To sposób na wizualizację tego, ale fizycznie niekoniecznie jest to prawda. System operacyjny jest odpowiedzialny za przydzielanie stosu i sterty aplikacji. Kompilator jest również odpowiedzialny, ale przede wszystkim opiera się na systemie operacyjnym. Stos jest ograniczony, a sterta nie. Wynika to ze sposobu, w jaki system operacyjny obsługuje sortowanie tych adresów pamięci w coś bardziej zorganizowanego, dzięki czemu wiele aplikacji może działać w tym samym systemie. Sterta i stos nie są jedynymi, ale zazwyczaj są jedynymi, o które martwi się większość programistów.
tsturzl
42

Powiedziałbym:

Przechowuj go na stosie, jeśli możesz.

Jeśli potrzebujesz, przechowuj go na stercie.

Dlatego preferuj stos od sterty. Oto kilka możliwych powodów, dla których nie możesz przechowywać czegoś na stosie:

  • Jest za duży - w programach wielowątkowych w 32-bitowym systemie operacyjnym stos ma mały i stały (przynajmniej w czasie tworzenia wątku) rozmiar (zwykle tylko kilka megabajtów. Dzięki temu można tworzyć wiele wątków bez wyczerpywania adresu) W przypadku programów 64-bitowych lub programów jednowątkowych (w każdym razie Linux) nie jest to poważny problem. W 32-bitowym Linuksie programy jednowątkowe zwykle używają dynamicznych stosów, które mogą rosnąć, aż osiągną szczyt stosu.
  • Musisz uzyskać do niego dostęp poza zakresem oryginalnej ramki stosu - to jest naprawdę główny powód.

Za pomocą rozsądnych kompilatorów można przydzielać na stercie obiekty o stałym rozmiarze (zwykle tablice, których rozmiar nie jest znany w czasie kompilacji).

MarkR
źródło
1
Coś więcej niż kilka KB jest zwykle najlepiej umieszczane na stosie. Nie znam szczegółów, ale nie przypominam sobie, żeby kiedykolwiek pracowałem ze stosem, który miał „kilka megapikseli”.
Dan Olson
2
To jest coś, czym nie przejmowałbym się na początku użytkownika. Dla użytkownika wektory i listy wydają się być przydzielone na stosie, nawet jeśli ta STL przechowuje zawartość na stercie. Pytanie wydawało się bardziej związane z podjęciem decyzji, kiedy wyraźnie nazwać nowy / usunąć.
David Rodríguez - dribeas
1
Dan: Położyłem 2 koncerty (tak, G jak w GIGS) na stosie pod 32-bitowym Linuksem. Limity stosu zależą od systemu operacyjnego.
Pan Ree
6
mrree: Stos Nintendo DS ma 16 kilobajtów. Niektóre limity stosu zależą od sprzętu.
Ant
Ant: Wszystkie stosy są zależne od sprzętu, systemu operacyjnego, a także od kompilatora.
Viliami,
24

Jest bardziej subtelny, niż sugerują inne odpowiedzi. Nie ma absolutnego podziału między danymi na stosie a danymi na stercie w zależności od tego, jak je zadeklarujesz. Na przykład:

std::vector<int> v(10);

W treści funkcji, która deklaruje vector(dynamiczną tablicę) dziesięciu liczb całkowitych na stosie. Ale magazyn zarządzany przez the vectornie znajduje się na stosie.

Ach, ale (inne odpowiedzi sugerują) czas życia tego magazynu jest ograniczony przez czas życia vectorsamego siebie, który tutaj jest oparty na stosie, więc nie ma znaczenia, jak jest zaimplementowany - możemy traktować go tylko jako obiekt oparty na stosie z semantyką wartości.

Skąd. Załóżmy, że funkcja była:

void GetSomeNumbers(std::vector<int> &result)
{
    std::vector<int> v(10);

    // fill v with numbers

    result.swap(v);
}

Tak więc wszystko, co ma swapfunkcję (i każdy złożony typ wartości powinien ją mieć) może służyć jako rodzaj możliwego do ponownego powiązania odniesienia do niektórych danych sterty w systemie, który gwarantuje jednego właściciela tych danych.

Dlatego nowoczesne podejście C ++ polega na tym, aby nigdy nie przechowywać adresu danych sterty w nagich lokalnych zmiennych wskaźnikowych. Wszystkie alokacje sterty muszą być ukryte w klasach.

Jeśli to zrobisz, możesz myśleć o wszystkich zmiennych w swoim programie tak, jakby były prostymi typami wartości i całkowicie zapomnieć o sterty (z wyjątkiem pisania nowej klasy opakowującej podobnej do wartości dla niektórych danych sterty, co powinno być niezwykłe) .

Musisz tylko zachować jedną specjalną wiedzę, która pomoże Ci zoptymalizować: tam, gdzie to możliwe, zamiast przypisywać jedną zmienną do drugiej w następujący sposób:

a = b;

zamień je w ten sposób:

a.swap(b);

ponieważ jest znacznie szybszy i nie generuje wyjątków. Jedynym wymaganiem jest to, że nie musisz bnadal utrzymywać tej samej wartości ( azamiast tego otrzyma wartość, która zostałaby skasowana a = b).

Wadą jest to, że takie podejście wymusza zwracanie wartości z funkcji za pośrednictwem parametrów wyjściowych zamiast rzeczywistej wartości zwracanej. Ale naprawiają to w C ++ 0x za pomocą odwołań do rvalue .

W najbardziej skomplikowanych sytuacjach możesz doprowadzić ten pomysł do skrajności i użyć inteligentnej klasy wskaźnika, takiej jak ta, shared_ptrktóra jest już w tr1. (Chociaż argumentowałbym, że jeśli wydaje ci się, że tego potrzebujesz, prawdopodobnie wyszedłeś poza słodkie miejsce zastosowania Standard C ++).

Daniel Earwicker
źródło
6

Możesz również przechowywać element na stercie, jeśli ma być używany poza zakresem funkcji, w której został utworzony. Jeden idiom używany z obiektami stosu nazywa się RAII - wiąże się to z użyciem obiektu opartego na stosie jako opakowania dla zasobu, gdy obiekt zostanie zniszczony, zasób zostanie wyczyszczony. Obiekty oparte na stosie są łatwiejsze do śledzenia, kiedy możesz rzucać wyjątki - nie musisz martwić się usuwaniem obiektu opartego na stosie w programie obsługi wyjątków. To dlatego surowe wskaźniki nie są normalnie używane we współczesnym C ++, można by użyć inteligentnego wskaźnika, który może być opakowaniem opartym na stosie dla surowego wskaźnika do obiektu opartego na stercie.

1800 INFORMACJE
źródło
5

Aby dodać do innych odpowiedzi, może to również dotyczyć wydajności, przynajmniej trochę. Nie znaczy to, że powinieneś się tym martwić, chyba że jest to istotne dla Ciebie, ale:

Alokowanie w stercie wymaga znalezienia śledzenia bloku pamięci, co nie jest operacją o stałym czasie (i zajmuje kilka cykli i narzut). Może to działać wolniej, gdy pamięć zostanie pofragmentowana i / lub zbliżasz się do wykorzystania 100% swojej przestrzeni adresowej. Z drugiej strony alokacje stosu są operacjami działającymi w czasie stałym, w zasadzie „darmowymi”.

Inną rzeczą do rozważenia (ponownie, naprawdę ważna tylko wtedy, gdy pojawi się problem) jest to, że zazwyczaj rozmiar stosu jest stały i może być znacznie mniejszy niż rozmiar sterty. Więc jeśli przydzielasz duże obiekty lub wiele małych obiektów, prawdopodobnie będziesz chciał użyć sterty; jeśli zabraknie miejsca na stosie, środowisko wykonawcze wyrzuci tytułowy wyjątek witryny. Zwykle nie jest to wielka sprawa, ale inna rzecz do rozważenia.

Nacięcie
źródło
Zarówno sterta, jak i stos są stronicowaną pamięcią wirtualną. Czas wyszukiwania sterty jest niesamowicie szybki w porównaniu z tym, co jest potrzebne do zmapowania w nowej pamięci. W 32-bitowym Linuksie mogę umieścić> 2gig na moim stosie. Myślę, że pod Macami stos jest mocno ograniczony do 65Meg.
Pan Ree
3

Stos jest bardziej wydajny i łatwiejszy do zarządzania danymi o określonym zakresie.

Ale sterta powinna być używana do wszystkiego, co jest większe niż kilka KB (w C ++ jest to łatwe, po prostu utwórz boost::scoped_ptrna stosie wskaźnik do przydzielonej pamięci).

Rozważmy rekurencyjny algorytm, który ciągle woła do siebie. Bardzo trudno jest ograniczyć i zgadnąć całkowite użycie stosu! Podczas gdy na stercie alokator ( malloc()lub new) może wskazać brak pamięci, zwracając NULLlub throwing.

Źródło : jądro Linuksa, którego stos nie jest większy niż 8 KB!

unixman83
źródło
Dla odniesienia dla innych czytelników: (A) „Powinien” jest tutaj czysto osobistą opinią użytkownika, zaczerpniętą z co najwyżej 1 cytatu i 1 scenariusza, z którym wielu użytkowników prawdopodobnie nie spotka się (rekurencja). Ponadto (B) zapewnia biblioteka standardowa std::unique_ptr, która powinna być preferowana w stosunku do dowolnej biblioteki zewnętrznej, takiej jak Boost (chociaż z czasem doprowadza to do standardu).
underscore_d
2

Dla kompletności możesz przeczytać artykuł Miro Samka o problemach związanych z używaniem sterty w kontekście oprogramowania wbudowanego .

Kupa problemów

Daniel Daranas
źródło
1

Wybór, czy alokować na stercie, czy na stosie, jest dokonywany za Ciebie, w zależności od sposobu przydzielenia zmiennej. Jeśli przydzielasz coś dynamicznie, używając „nowego” wywołania, alokujesz ze sterty. Jeśli przydzielisz coś jako zmienną globalną lub jako parametr w funkcji, jest to przydzielane na stosie.

Rob Lachlan
źródło
4
Podejrzewam, że pytał, kiedy położyć rzeczy na stosie, a nie jak.
Steve Rowe
0

Moim zdaniem decydują dwa czynniki

1) Scope of variable
2) Performance.

W większości przypadków wolałbym używać stosu, ale jeśli potrzebujesz dostępu do zmiennej poza zakresem, możesz użyć sterty.

Aby zwiększyć wydajność podczas korzystania ze stert, można również użyć funkcji tworzenia bloku sterty, co może pomóc w zwiększeniu wydajności zamiast przydzielania każdej zmiennej w innej lokalizacji pamięci.

anand
źródło
0

prawdopodobnie odpowiedź na to pytanie jest całkiem dobra. Chciałbym skierować Cię do poniższej serii artykułów, aby lepiej zrozumieć szczegóły niskiego poziomu. Alex Darby ma serię artykułów, w których przeprowadza cię przez debuger. Oto część 3 o stosie. http://www.altdevblogaday.com/2011/12/14/cc-low-level-curriculum-part-3-the-stack/

hAcKnRoCk
źródło
Wydaje się, że łącze jest martwe, ale sprawdzenie Internet Archive Wayback Machine wskazuje, że mówi on tylko o stosie i dlatego nie robi nic, aby odpowiedzieć na konkretne pytanie o stos i stos . -1
underscore_d