2013-05-09 9 views
7

Gdybym określić strukturę ...Dlaczego nadal mogę uzyskać dostęp do elementu struktury po zwolnieniu wskaźnika?

struct LinkNode 
{ 
    int node_val; 
    struct LinkNode *next_node; 
}; 

a następnie utworzyć wskaźnik do niego ...

struct LinkNode *mynode = malloc(sizeof(struct LinkNode)); 

... i wreszcie free() to ...

free(mynode); 

... Wciąż mogę uzyskać dostęp do elementu struktury "next_node".

mynode->next_node 

Moje pytanie jest takie: który kawałek mechaniki bazowych śledzi to, że ten blok pamięci ma reprezentować LinkNode struct? Jestem nowicjuszem w C i spodziewałem się, że po tym, jak użyłem free() na wskaźniku do mojego LinkNode, nie będę już mógł uzyskać dostępu do członków tej struktury. Spodziewałem się jakiegoś ostrzeżenia "już niedostępnego".

Chciałbym dowiedzieć się więcej o tym, jak działa ten proces.

+0

Czy spodziewałbyś się, że pamięć wyparuje podczas dzwonienia za darmo? Czy nadal tam jest (lub nie) lub może mieć inne znaczenie. – wildplasser

+0

@wildplasser jesteś tam snurkowaty? Próbuję zrozumieć nie tylko składnię języka, ale także dlaczego działa. Oczywiście pamięć nie wyparowuje. Ale kiedy piszę * wskaźnik -> członek, nie jestem pewien, jak działa to mechanika i dlaczego nadal działa, gdy wskaźnik jest wolny() d. Mam nadzieję, że ma to sens. – Ducain

+1

Wskaźnik ma nadal tę samą wartość. Po wywołaniu free() porzuciłeś pamięć: powiedziałeś malloc/free, że nie chcesz już jej używać. Porównaj to z numerem telefonu: po tym, jak opuściłem mój załącznik telefoniczny, mój numer nie jest już ważny. Ale nadal możesz spróbować go wybrać. To może nawet ja odpowiadam przez telefon. Lub Hałasu. Albo ktoś zupełnie inny. Numer (= adres) nadal istnieje, ale jego użycie jest już nieważne. Może wskazywać na sterowanie elektrownią jądrową ... – wildplasser

Odpowiedz

6

Skompilowany program nie ma już żadnej wiedzy na temat struct LinkedNode lub pola o nazwie next_node lub czegoś w tym stylu. Wszelkie nazwy są zupełnie nieobecne w skompilowanym programie. Skompilowany program działa pod względem wartości liczbowych, które mogą odgrywać role adresów pamięci, przesunięć, indeksów i tak dalej.

W przykładzie, kiedy czytasz mynode->next_node w kodzie źródłowym programu, jest kompilowany do kodu maszynowego, które po prostu odczytuje 4-bajtowy wartość liczbową od jakiegoś zarezerwowanego miejsca pamięci (znany jako zmiennej mynode w kodzie źródłowym) , dodaje 4 do niego (co jest przesunięciem pola next_node) i odczytuje 4-bajtową wartość w wynikowym adresie (który jest mynode->next_node). Ten kod, jak widać, działa w kategoriach wartości całkowitych - adresów, rozmiarów i przesunięć. Nie interesują go żadne nazwy, takie jak LinkedNode lub next_node. Nie obchodzi, czy pamięć jest przydzielona i/lub uwolniona. Nie obchodzi, czy którykolwiek z tych dostępów jest legalny, czy nie.

(Stała 4 I wielokrotnie używane w powyższym przykładzie jest specyficzne dla platformy 32-bitowych. Platform 64-bitowych to być zastąpione 8 w większości (lub wszystkich) przypadkach).

Jeżeli próba jest przeznaczony do odczytu pamięci, która została uwolniona, te dostępy mogą spowodować awarię twojego programu. A może nie. To kwestia czystego szczęścia. Jeśli chodzi o język, zachowanie jest niezdefiniowane.

+0

Rockin '. Dokładnie rodzaju informacji, które miałem. Dzięki tobie również. – Ducain

4

Nie ma i nie można. Jest to klasyczny przypadek niezdefiniowanego zachowania.

Kiedy masz niezdefiniowane zachowanie, wszystko może się zdarzyć. Może się nawet wydawać, że działa, ale losowo rozbija się rok później.

+1

To tak naprawdę nie odpowiada na pytanie, które zostało zadane, zwłaszcza dlaczego względne pozycjonowanie elementów struktury jest nadal poprawne, nawet jeśli wynik dostępu do nich jest niezdefiniowany. –

+0

@ChrisStratton: Jeśli usuniesz blokadę z publicznej szafki, ale zostawisz swoje rzeczy w środku, a później po pewnym czasie wrócisz i zajrzysz do środka, możliwe, że twoje rzeczy nadal tam będą. Lub może to być coś, co wygląda jak twoje, ale w rzeczywistości należy do kogoś, kto ma podobny gust.Jeśli zdarzy ci się znaleźć twoje rzeczy w szafce dokładnie tak, jak ostatnio to ułożyłeś, jedynym "powodem" będzie to zorganizowane tak, że nikt jeszcze nie zrobił nic więcej z tą przestrzenią. – supercat

+1

Aspektem, który to pomija jest to, że szafka 3 na prawo od twojej wciąż jest szafką 3 na prawo od tej, którą miałeś, nawet jeśli nie jest już twoja. Ponadto w uwagach wskazał, że zachowanie jest faktycznie ** nie zawsze ** nieokreślone. –

2

Żadna część podstawowej pamięci nie śledzi tego. To tylko semantyka, którą język programowania nadaje kawałkowi pamięci. Możesz np. rzucić go do czegoś zupełnie innego i nadal może uzyskać dostęp do tego samego regionu pamięci. Jednak tutaj jest haczyk, który z większym prawdopodobieństwem może prowadzić do błędów. Zwłaszcza bezpieczeństwo typu zniknie. W twoim przypadku tylko dlatego, że zadzwoniłeś pod numer free, nie oznacza to, że pamięć bazowa w ogóle działa. W systemie operacyjnym znajduje się tylko flaga oznaczająca ten region jako wolny ponownie.

Pomyśl o tym w ten sposób: funkcja free jest czymś w rodzaju "minimalnego" systemu zarządzania pamięcią. Jeśli twoje zgłoszenie wymagałoby czegoś więcej niż ustawienie flagi, wprowadziłoby to niepotrzebnie narzut. Również wtedy, gdy uzyskujesz dostęp do elementu, który użytkownik (tj. Twój system operacyjny) może sprawdzić, czy flaga dla tego regionu pamięci jest ustawiona na "wolny" lub "w użyciu". Ale to znowu jest w górze.

Oczywiście nie oznacza to, że nie ma sensu robić takich rzeczy. Uniknęłoby to wielu luk w zabezpieczeniach i zostało wykonane na przykład w .Net i Java. Te środowiska wykonawcze są jednak znacznie młodsze od C, a obecnie mamy znacznie więcej zasobów.

4

Działa na zasadzie szczęścia, ponieważ uwolniona pamięć nie została jeszcze nadpisana przez coś innego. Po zwolnieniu pamięci należy pamiętać, aby nie używać go ponownie.

2

Kiedy twój kompilator przetłumaczy twój kod C na kod wykonywalny maszyny, wyrzuci wiele informacji, w tym informacje o typie. Gdzie piszesz:

int x = 42; 

wygenerowany kod kopiuje tylko pewien wzorzec bitowy w pewnym fragmencie pamięci (fragmentu, który może być typowo 4 bajty). Nie można stwierdzić poprzez sprawdzenie kodu maszynowego, że fragment pamięci jest obiektem typu int.

Podobnie, gdy piszesz:

if (mynode->next_node == NULL) { /* ... */ } 

wygenerowany kod pobiera wskaźnik wielkości kawałek pamięci przez dereferencji innego wskaźnika wielkości kawałek pamięci, i porównać wynik z reprezentacją systemu z zerowym wskaźnikiem (zwykle wszystkie-bity-zero). Wygenerowany kod nie odzwierciedla bezpośrednio faktu, że next_node jest członkiem struktury, ani nic o tym, jak struktura została przydzielona lub czy nadal istnieje.

Kompilator może sprawdzić wiele rzeczy w czasie kompilacji, ale niekoniecznie generuje kod do przeprowadzania kontroli w czasie wykonywania. Od Ciebie zależy, czy jako programista unikniesz błędów.

W tym konkretnym przypadku, po wywołaniu free, mynode ma nieokreśloną wartość. Nie wskazuje żadnego poprawnego obiektu, ale nie ma wymogu implementacji, aby cokolwiek zrobić z tą wiedzą. Wywołanie free nie niszczy przydzielonej pamięci, a jedynie udostępnia ją do alokacji przez przyszłe połączenia z numerem malloc.

Istnieje wiele sposobów, że implementacja mógłby przeprowadzają kontrole w ten sposób, i wywołać błąd run-time jeśli nieprawidłowego wskaźnika po free ing go. Ale takie kontrole nie są wymagane przez język C, i generalnie nie są implementowane, ponieważ (a) byłyby one dość drogie, dzięki czemu twój program działałby wolniej i (b) kontrole i tak nie są w stanie wychwycić wszystkich błędów.

C jest tak zdefiniowany, że przydział pamięci i manipulacja kursorem będą działały poprawnie, jeśli twój program robi wszystko dobrze. Jeśli popełnisz pewne błędy, które można wykryć podczas kompilacji, kompilator może je zdiagnozować. Na przykład przypisanie wartości wskaźnika do obiektu typu liczba całkowita wymaga co najmniej ostrzeżenia o czasie kompilacji.Ale inne błędy, takie jak dereferencja ze wskaźnikiem free powodują, że twój program ma niezdefiniowane zachowanie . To ty, jako programista, musisz przede wszystkim unikać wprowadzania tych błędów. Jeśli ci się nie uda, jesteś sam.

Oczywiście istnieją narzędzia, które mogą pomóc. Valgrind to jeden; sprytne kompilatory optymalizujące to kolejne. (Włączenie optymalizacji powoduje, że kompilator wykonuje więcej analiz kodu, co często pozwala mu diagnozować więcej błędów.) Ale ostatecznie C nie jest językiem, który trzyma rękę. Jest to ostre narzędzie - i może być używane do budowania bezpieczniejszych narzędzi, takich jak języki interpretowane, które wykonują więcej sprawdzania w czasie pracy.

+0

Optymalizator może również pomóc w wykryciu błędów w tym sensie, że może to spowodować uszkodzenie błędnego kodu. Kod, który wydaje się działać bez optymalizacji, może być w oczywisty sposób uszkodzony podczas kompilacji z optymalizacją. – Antimony

+0

@Antimony: Tak, jeśli kod ma niezdefiniowane zachowanie, kompilator może wygenerować dowolny kod, który mu się podoba. Optymalizacja może wpływać na podejmowane decyzje, zmieniając rzeczywiste zachowanie i ujawniając błąd. –

+0

W rzeczywistości w C zazwyczaj nie jest sprawdzane zerowanie podczas deereferencji wskaźnika. Zamiast tego jest to próba ślepa - choć na wielu nowoczesnych systemach spowoduje błąd w jednostce ochrony pamięci. I nie, wartość mynode jest ** nie ** nieokreślona. Wciąż wskazuje, gdzie go użył, może po prostu pamięć może być teraz używana do czegoś innego. –

0

Musisz przypisać NULL do mynode-> next_node:

mynode-> next_node = NULL;

po zwolnieniu pamięci, aby wskazać, że nie jest już używana przydzielona pamięć.

Bez przypisania wartości NULL, nadal wskazuje poprzednio zwolnioną lokalizację pamięci.

Powiązane problemy