Jeśli utworzę zmienną w nowym zestawie nawiasów klamrowych, to czy ta zmienna wyskoczyła ze stosu w nawiasie zamykającym, czy też zawiesza się do końca funkcji? Na przykład:
void foo() {
int c[100];
{
int d[200];
}
//code that takes a while
return;
}
Będzie d
zajmować pamięć podczas code that takes a while
sekcji?
Odpowiedzi:
Nie, klamry nie działają jak ramka stosu. W języku C nawiasy klamrowe oznaczają tylko zakres nazewnictwa, ale nic nie zostaje zniszczone ani nic nie jest wyskakujące ze stosu, gdy kontrola przechodzi poza niego.
Jako programista piszący kod, często możesz myśleć o nim jak o ramce stosu. Identyfikatory zadeklarowane w nawiasach są dostępne tylko w nawiasach, więc z punktu widzenia programisty wygląda to tak, jakby były umieszczane na stosie, gdy są zadeklarowane, a następnie usuwane po wyjściu z zakresu. Jednak kompilatory nie muszą generować kodu, który wypycha / wyskakuje cokolwiek przy wejściu / wyjściu (i generalnie tego nie robi).
Należy również zauważyć, że zmienne lokalne mogą w ogóle nie zajmować miejsca na stosie: mogą być przechowywane w rejestrach procesora lub w innej lokalizacji pamięci dyskowej lub całkowicie zoptymalizowane.
A zatem
d
tablica teoretycznie może zużywać pamięć dla całej funkcji. Jednak kompilator może go zoptymalizować lub udostępnić pamięć innym zmiennym lokalnym, których okresy użytkowania nie pokrywają się.źródło
Czas, w którym zmienna faktycznie zajmuje pamięć, jest oczywiście zależny od kompilatora (a wiele kompilatorów nie dostosowuje wskaźnika stosu, gdy wewnętrzne bloki są wprowadzane i zamykane w funkcjach).
Jednak ściśle powiązanym, ale być może bardziej interesującym pytaniem jest to, czy program może uzyskać dostęp do tego wewnętrznego obiektu poza wewnętrznym zakresem (ale w ramach funkcji zawierającej), tj .:
(Innymi słowy: czy kompilator może zwolnić przydział
d
, nawet jeśli w praktyce większość tego nie robi?).Odpowiedź jest, że kompilator jest dozwolone na zwalnianie
d
i dostępup[0]
gdzie wskazuje komentarz jest niezdefiniowane zachowanie (program nie może uzyskać dostęp do wewnętrznej zewnątrz obiektu zakresu wewnętrznego). Odpowiednia część normy C to 6.2.4p5:źródło
Twoje pytanie nie jest na tyle jasne, aby uzyskać jednoznaczną odpowiedź.
Z jednej strony kompilatory normalnie nie wykonują żadnej lokalnej alokacji / zwalniania alokacji pamięci dla zagnieżdżonych zakresów bloków. Pamięć lokalna jest zwykle przydzielana tylko raz przy wejściu do funkcji i zwalniana przy jej wyjściu.
Z drugiej strony, gdy kończy się okres istnienia obiektu lokalnego, pamięć zajmowaną przez ten obiekt można później ponownie wykorzystać dla innego obiektu lokalnego. Na przykład w tym kodzie
obie tablice zwykle zajmują ten sam obszar pamięci, co oznacza, że całkowita ilość lokalnej pamięci potrzebnej do funkcji
foo
jest taka, jaka jest potrzebna dla największej z dwóch tablic, a nie dla obu jednocześnie.To, czy to ostatnie kwalifikuje się jako
d
dalsze zajmowanie pamięci do końca funkcji w kontekście twojego pytania, zależy od ciebie.źródło
To zależy od implementacji. Napisałem krótki program, aby sprawdzić, co robi gcc 4.3.4, i przydziela on całą przestrzeń stosu naraz na początku funkcji. Możesz zbadać zestaw, który tworzy gcc, używając opcji -S.
źródło
Nie, d [] nie będzie na stosie do końca procedury. Ale throwa () jest inaczej.
Edycja: Kristopher Johnson (oraz simon i Daniel) mają rację , a moja początkowa odpowiedź była błędna . W gcc 4.3.4. Na CYGWIN, kod:
daje:
Żyj i ucz się! Szybki test wydaje się wykazywać, że AndreyT ma również rację co do wielu alokacji.
Dodano dużo później : Powyższy test pokazuje, że dokumentacja gcc nie jest całkiem poprawna. Od lat mówi (podkreślenie dodane):
źródło
alloca
funkcji. Jestem naprawdę zaskoczony, że cygwin gcc to zrobił. To nawet nie jest tablica o zmiennej długości, więc IDK, dlaczego o tym wspominasz.Mogą. Może nie. Odpowiedź, której myślę, że naprawdę potrzebujesz, brzmi: nigdy niczego nie zakładaj. Nowoczesne kompilatory wykonują wszystkie rodzaje architektury i magii specyficznej dla implementacji. Napisz swój kod prosto i czytelnie dla ludzi i pozwól kompilatorowi zrobić dobre rzeczy. Jeśli próbujesz kodować wokół kompilatora, prosisz o kłopoty - a kłopoty, które zwykle pojawiają się w takich sytuacjach, są zwykle strasznie subtelne i trudne do zdiagnozowania.
źródło
Twoja zmienna
d
zazwyczaj nie jest usuwana ze stosu. Nawiasy klamrowe nie oznaczają ramki stosu. W przeciwnym razie nie byłbyś w stanie zrobić czegoś takiego:Jeśli nawiasy klamrowe spowodowałyby prawdziwy stos push / pop (jak wywołanie funkcji), powyższy kod nie zostałby skompilowany, ponieważ kod wewnątrz nawiasów nie byłby w stanie uzyskać dostępu do zmiennej,
var
która znajduje się poza nawiasami klamrowymi (tak jak pod- funkcja nie ma bezpośredniego dostępu do zmiennych w funkcji wywołującej). Wiemy, że tak nie jest.Szelki kręcone są po prostu używane do określania zakresu. Kompilator potraktuje każdy dostęp do zmiennej "wewnętrznej" spoza otaczających nawiasów jako nieprawidłowy i może ponownie wykorzystać tę pamięć do czegoś innego (jest to zależne od implementacji). Jednak nie można go zdjąć ze stosu, dopóki funkcja otaczająca nie powróci.
Aktualizacja: Oto, co ma do powiedzenia specyfikacja C. Odnośnie obiektów z automatycznym czasem przechowywania (sekcja 6.4.2):
Ta sama sekcja definiuje termin „żywotność” jako (wyróżnienie moje):
Kluczowym słowem jest tutaj oczywiście „gwarantowana”. Po wyjściu z zakresu wewnętrznego zestawu nawiasów klamrowych okres istnienia tablicy dobiega końca. Pamięć może być dla niego przydzielona lub nie (Twój kompilator może ponownie wykorzystać to miejsce na coś innego), ale wszelkie próby uzyskania dostępu do tablicy wywołują niezdefiniowane zachowanie i powodują nieprzewidywalne rezultaty.
Specyfikacja C nie zawiera pojęcia ramek stosu. Mówi tylko o tym, jak zachowa się wynikowy program i pozostawia szczegóły implementacji kompilatorowi (w końcu implementacja wyglądałaby zupełnie inaczej na procesorze bez stosu niż na procesorze ze stosem sprzętowym). W specyfikacji C nie ma nic, co określa, gdzie ramka stosu będzie się kończyć lub nie. Jedyny prawdziwy sposobem, aby to wiedzieć, jest skompilowanie kodu na konkretnym kompilatorze / platformie i zbadanie wynikowego zestawu. Obecny zestaw opcji optymalizacyjnych Twojego kompilatora prawdopodobnie również odegra w tym rolę.
Jeśli chcesz mieć pewność, że tablica
d
nie zajmuje już pamięci podczas działania kodu, możesz albo przekonwertować kod w nawiasach klamrowych na osobną funkcję, albo jawniemalloc
ifree
pamięć, zamiast korzystać z automatycznego przechowywania.źródło
Uważam, że wykracza poza zakres, ale nie jest wyskakiwany ze stosu, dopóki funkcja nie powróci. Tak więc nadal będzie zajmować pamięć na stosie, dopóki funkcja nie zostanie zakończona, ale nie będzie dostępna za pierwszym zamykającym nawiasem klamrowym.
źródło
Podano już wiele informacji na temat normy, wskazujących, że jest ona rzeczywiście specyficzna dla implementacji.
Tak więc jeden eksperyment może być interesujący. Jeśli spróbujemy poniższego kodu:
Używając gcc otrzymujemy tutaj dwa razy ten sam adres: Coliro
Ale jeśli spróbujemy następującego kodu:
Używając gcc otrzymujemy tutaj dwa różne adresy: Coliro
Więc nie możesz być naprawdę pewien, co się dzieje.
źródło