Zawsze słyszałem, że w C trzeba naprawdę uważać, jak zarządza się pamięcią. I wciąż zaczynam uczyć się C, ale jak dotąd nie musiałem w ogóle wykonywać żadnych czynności związanych z zarządzaniem pamięcią. Zawsze wyobrażałem sobie, że muszę zwolnić zmienne i robić różne brzydkie rzeczy. Ale wydaje się, że tak nie jest.
Czy ktoś może mi pokazać (z przykładami kodu) przykład, kiedy musiałbyś zrobić jakieś „zarządzanie pamięcią”?
Odpowiedzi:
Istnieją dwa miejsca, w których można umieścić zmienne w pamięci. Podczas tworzenia takiej zmiennej:
int a; char c; char d[16];
Zmienne tworzone są w „ stosie ”. Zmienne stosu są automatycznie zwalniane, gdy wyjdą poza zakres (to znaczy, gdy kod nie może już do nich dotrzeć). Możesz usłyszeć, że to zmienne „automatyczne”, ale to wyszło z mody.
Wiele przykładów dla początkujących wykorzystuje tylko zmienne stosu.
Stos jest fajny, ponieważ jest automatyczny, ale ma również dwie wady: (1) kompilator musi wiedzieć z wyprzedzeniem, jak duże są zmienne, oraz (b) przestrzeń stosu jest nieco ograniczona. Na przykład: w systemie Windows, przy domyślnych ustawieniach konsolidatora Microsoft, stos jest ustawiony na 1 MB, a nie całość jest dostępna dla zmiennych.
Jeśli w czasie kompilacji nie wiesz, jak duża jest twoja tablica lub jeśli potrzebujesz dużej tablicy lub struktury, potrzebujesz „planu B”.
Plan B nazywany jest „ stertą ”. Zwykle możesz tworzyć zmienne tak duże, jak pozwala na to system operacyjny, ale musisz to zrobić sam. Wcześniejsze posty pokazały, że możesz to zrobić, chociaż są też inne sposoby:
int size; // ... // Set size to some value, based on information available at run-time. Then: // ... char *p = (char *)malloc(size);
(Zwróć uwagę, że zmiennymi w stercie nie można manipulować bezpośrednio, ale za pomocą wskaźników)
Po utworzeniu zmiennej sterty problem polega na tym, że kompilator nie może powiedzieć, kiedy z nią skończysz, więc tracisz automatyczne zwalnianie. W tym miejscu pojawia się „ręczne zwalnianie”, o którym mówiłaś. Twój kod jest teraz odpowiedzialny za podjęcie decyzji, kiedy zmienna nie jest już potrzebna, i zwolnienie jej, aby pamięć mogła zostać wykorzystana do innych celów. W powyższym przypadku z:
free(p);
To, co sprawia, że ta druga opcja jest „paskudną sprawą”, to fakt, że nie zawsze łatwo jest stwierdzić, kiedy zmienna nie jest już potrzebna. Zapomnienie o zwolnieniu zmiennej, gdy jej nie potrzebujesz, spowoduje, że program zużyje więcej pamięci niż potrzebuje. Taka sytuacja nazywana jest „wyciekiem”. „Wyciekła” pamięć nie może być użyta do niczego, dopóki program się nie zakończy, a system operacyjny nie odzyska wszystkich swoich zasobów. Jeszcze bardziej nieprzyjemne problemy są możliwe, jeśli przez pomyłkę zwolnisz zmienną sterty, zanim faktycznie skończysz z nią.
W C i C ++ jesteś odpowiedzialny za wyczyszczenie zmiennych sterty, jak pokazano powyżej. Istnieją jednak języki i środowiska, takie jak języki Java i .NET, takie jak C #, które wykorzystują inne podejście, w którym sterta jest czyszczona samodzielnie. Ta druga metoda, zwana „wyrzucaniem elementów bezużytecznych”, jest znacznie łatwiejsza dla dewelopera, ale wiąże się to z obniżeniem kosztów i wydajności. To równowaga.
(Przeanalizowałem wiele szczegółów, aby dać prostszą, ale miejmy nadzieję, bardziej wyrównaną odpowiedź)
źródło
malloc()
(char *)malloc(size);
Oto przykład. Załóżmy, że masz funkcję strdup (), która powiela ciąg:
char *strdup(char *src) { char * dest; dest = malloc(strlen(src) + 1); if (dest == NULL) abort(); strcpy(dest, src); return dest; }
I nazywasz to tak:
main() { char *s; s = strdup("hello"); printf("%s\n", s); s = strdup("world"); printf("%s\n", s); }
Możesz zobaczyć, że program działa, ale przydzieliłeś pamięć (przez malloc) bez jej zwalniania. Straciłeś wskaźnik do pierwszego bloku pamięci, gdy po raz drugi wywołałeś strdup.
To nic wielkiego przy tak małej ilości pamięci, ale rozważ przypadek:
for (i = 0; i < 1000000000; ++i) /* billion times */ s = strdup("hello world"); /* 11 bytes */
Zużyłeś teraz 11 gigabajtów pamięci (prawdopodobnie więcej, w zależności od menedżera pamięci) i jeśli nie zawiesiłeś pracy, proces prawdopodobnie działa dość wolno.
Aby to naprawić, musisz wywołać free () dla wszystkiego, co jest uzyskiwane za pomocą malloc () po zakończeniu używania:
s = strdup("hello"); free(s); /* now not leaking memory! */ s = strdup("world"); ...
Mam nadzieję, że ten przykład pomoże!
źródło
Musisz zrobić „zarządzanie pamięcią”, jeśli chcesz używać pamięci na stercie, a nie na stosie. Jeśli nie wiesz, jak duża jest tablica przed uruchomieniem, musisz użyć sterty. Na przykład możesz chcieć zapisać coś w ciągu, ale nie wiesz, jak duża będzie jego zawartość, dopóki program nie zostanie uruchomiony. W takim przypadku napisałbyś coś takiego:
char *string = malloc(stringlength); // stringlength is the number of bytes to allocate // Do something with the string... free(string); // Free the allocated memory
źródło
Myślę, że najbardziej zwięzłym sposobem odpowiedzi na to pytanie jest rozważenie roli wskaźnika w C. Wskaźnik to lekki, ale potężny mechanizm, który daje ogromną swobodę kosztem ogromnej zdolności do strzelenia sobie w stopę.
W C odpowiedzialność za zapewnienie, że wskazówki wskazują na pamięć, którą posiadasz, należy do Ciebie i tylko do Ciebie. Wymaga to zorganizowanego i zdyscyplinowanego podejścia, chyba że porzucisz wskaźniki, co utrudnia pisanie efektywnego C.
Opublikowane dotychczas odpowiedzi koncentrują się na automatycznym (stosowym) alokowaniu zmiennych w stosie i na stertach. Korzystanie z alokacji stosu zapewnia automatycznie zarządzaną i wygodną pamięć, ale w niektórych okolicznościach (duże bufory, algorytmy rekurencyjne) może prowadzić do horrendalnego problemu przepełnienia stosu. Wiedza o tym, ile dokładnie pamięci można przydzielić na stosie, zależy w dużym stopniu od systemu. W niektórych scenariuszach osadzonych kilkadziesiąt bajtów może być twoim limitem, w niektórych scenariuszach dla komputerów stacjonarnych możesz bezpiecznie używać megabajtów.
Alokacja sterty jest mniej związana z językiem. Jest to po prostu zestaw wywołań biblioteki, które przyznaje Ci prawo własności do bloku pamięci o danym rozmiarze, dopóki nie będziesz gotowy go zwrócić („zwolnić”). Brzmi prosto, ale wiąże się z niewypowiedzianym żalem programisty. Problemy są proste (zwolnienie tej samej pamięci dwukrotnie lub wcale [wycieki pamięci], niewystarczająca alokacja pamięci [przepełnienie bufora] itp.), Ale trudne do uniknięcia i debugowania. Wysoce zdyscyplinowane podejście jest absolutnie obowiązkowe w praktyce, ale oczywiście język tak naprawdę tego nie nakazuje.
Chciałbym wspomnieć o innym typie alokacji pamięci, który został zignorowany w innych postach. Możliwe jest statyczne przydzielanie zmiennych, deklarując je poza jakąkolwiek funkcją. Myślę, że ogólnie ten typ alokacji jest źle oceniany, ponieważ jest używany przez zmienne globalne. Jednak nic nie mówi, że jedynym sposobem wykorzystania pamięci przydzielonej w ten sposób jest niezdyscyplinowana zmienna globalna w bałaganie kodu spaghetti. Statycznej metody alokacji można użyć po prostu w celu uniknięcia niektórych pułapek sterty i automatycznych metod alokacji. Niektórzy programiści C są zaskoczeni, gdy dowiadują się, że duże i wyrafinowane programy wbudowane w C i gry zostały skonstruowane bez użycia alokacji sterty.
źródło
Jest tutaj kilka świetnych odpowiedzi na temat przydzielania i zwalniania pamięci, a moim zdaniem trudniejszą stroną korzystania z C jest upewnienie się, że jedyną pamięcią, której używasz, jest pamięć, którą przydzieliłeś - jeśli nie zostanie to zrobione poprawnie, co skończysz jest kuzyn tej strony - przepełnienie bufora - i możesz nadpisywać pamięć używaną przez inną aplikację, z bardzo nieprzewidywalnymi skutkami.
Przykład:
int main() { char* myString = (char*)malloc(5*sizeof(char)); myString = "abcd"; }
W tym momencie przydzieliłeś 5 bajtów dla myString i wypełniłeś go "abcd \ 0" (ciągi kończą się na null - \ 0). Jeśli twój przydział ciągów był
myString = "abcde";
Przypisałbyś "abcde" do 5 bajtów, które przydzieliłeś swojemu programowi, a końcowy znak null zostałby wstawiony na końcu tego - część pamięci, która nie została przydzielona do twojego użytku i mogłaby być wolne, ale może być również używane przez inną aplikację - jest to krytyczna część zarządzania pamięcią, w której błąd będzie miał nieprzewidywalne (a czasem niepowtarzalne) konsekwencje.
źródło
strcpy()
zamiast=
; Zakładam, że taki był zamiar Chrisa BC.Należy pamiętać, aby zawsze inicjalizować wskaźniki na NULL, ponieważ niezainicjowany wskaźnik może zawierać pseudolosowy prawidłowy adres pamięci, który może spowodować ciche błędy wskaźnika. Wymuszając zainicjowanie wskaźnika z wartością NULL, zawsze możesz złapać, czy używasz tego wskaźnika bez inicjalizacji. Powodem jest to, że systemy operacyjne „łączą” adres wirtualny 0x00000000 z ogólnymi wyjątkami ochrony w celu przechwycenia użycia wskaźnika zerowego.
źródło
Możesz także chcieć użyć dynamicznej alokacji pamięci, gdy musisz zdefiniować ogromną tablicę, powiedzmy int [10000]. Nie możesz po prostu umieścić go w stosie, ponieważ wtedy hm ... otrzymasz przepełnienie stosu.
Innym dobrym przykładem może być implementacja struktury danych, powiedzmy połączonej listy lub drzewa binarnego. Nie mam tutaj przykładowego kodu do wklejenia, ale możesz go łatwo wygooglować.
źródło
(Piszę, bo czuję, że dotychczasowe odpowiedzi nie są trafne).
Powodem, dla którego warto wspomnieć o zarządzaniu pamięcią, jest problem / rozwiązanie wymagające tworzenia złożonych struktur. (Jeśli twoje programy ulegną awarii, jeśli przydzielisz za dużo miejsca na stosie naraz, jest to błąd). Zazwyczaj pierwszą strukturą danych, której musisz się nauczyć, jest pewnego rodzaju lista . Oto pojedynczy link, z góry mojej głowy:
typedef struct listelem { struct listelem *next; void *data;} listelem; listelem * create(void * data) { listelem *p = calloc(1, sizeof(listelem)); if(p) p->data = data; return p; } listelem * delete(listelem * p) { listelem next = p->next; free(p); return next; } void deleteall(listelem * p) { while(p) p = delete(p); } void foreach(listelem * p, void (*fun)(void *data) ) { for( ; p != NULL; p = p->next) fun(p->data); } listelem * merge(listelem *p, listelem *q) { while(p != NULL && p->next != NULL) p = p->next; if(p) { p->next = q; return p; } else return q; }
Oczywiście chciałbyś mieć kilka innych funkcji, ale w zasadzie do tego potrzebujesz zarządzania pamięcią. Powinienem zaznaczyć, że jest kilka sztuczek, które są możliwe przy "ręcznym" zarządzaniu pamięcią, np.
Zdobądź dobry debugger ... Powodzenia!
źródło
@ Euro Micelli
Jedynym minusem do dodania jest to, że wskaźniki do stosu nie są już prawidłowe, gdy funkcja zwraca, więc nie można zwrócić wskaźnika do zmiennej stosu z funkcji. Jest to częsty błąd i główny powód, dla którego nie da się obejść za pomocą samych zmiennych stosu. Jeśli twoja funkcja musi zwrócić wskaźnik, musisz wykonać malloc i zająć się zarządzaniem pamięcią.
źródło
Oczywiście masz rację. Uważam, że zawsze była to prawda, chociaż nie mam kopii K&R do sprawdzenia.
Nie lubię wielu niejawnych konwersji w C, więc staram się używać rzutów, aby uczynić „magię” bardziej widoczną. Czasami poprawia czytelność, czasami nie, a czasami powoduje, że kompilator wychwytuje cichy błąd. Mimo to nie mam na ten temat zdecydowanej opinii, w taki czy inny sposób.
Tak ... złapałeś mnie tam. Spędzam dużo więcej czasu w C ++ niż C. Dzięki, że to zauważyłeś.
źródło
W C faktycznie masz dwie różne możliwości. Po pierwsze, możesz pozwolić systemowi zarządzać pamięcią za Ciebie. Alternatywnie możesz to zrobić samodzielnie. Ogólnie rzecz biorąc, chciałbyś jak najdłużej trzymać się tego pierwszego. Jednak pamięć zarządzana automatycznie w języku C jest bardzo ograniczona i w wielu przypadkach konieczne będzie ręczne zarządzanie pamięcią, na przykład:
za. Chcesz, aby zmienna przetrwała funkcje i nie chcesz mieć zmiennej globalnej. dawny:
b. chcesz mieć dynamicznie przydzielaną pamięć. Najczęstszym przykładem jest tablica bez stałej długości:
Widzisz, długa wartość wystarczy, aby przechowywać WSZYSTKO. Pamiętaj tylko, aby go uwolnić, inaczej będziesz żałować. To jedna z moich ulubionych sztuczek do zabawy w C: D.
Jednak generalnie wolałbyś trzymać się z daleka od swoich ulubionych sztuczek (T___T). Wcześniej czy później Zepsujesz swój system operacyjny, jeśli będziesz ich używać zbyt często. Dopóki nie używasz * przydzielania i bezpłatnego, można śmiało powiedzieć, że nadal jesteś dziewicą, a kod nadal wygląda ładnie.
źródło
Pewnie. Jeśli utworzysz obiekt, który istnieje poza zakresem, w którym go używasz. Oto sztuczny przykład (pamiętaj, że moja składnia będzie wyłączona; moje C jest zardzewiałe, ale ten przykład nadal ilustruje koncepcję):
class MyClass { SomeOtherClass *myObject; public MyClass() { //The object is created when the class is constructed myObject = (SomeOtherClass*)malloc(sizeof(myObject)); } public ~MyClass() { //The class is destructed //If you don't free the object here, you leak memory free(myObject); } public void SomeMemberFunction() { //Some use of the object myObject->SomeOperation(); } };
W tym przykładzie używam obiektu typu SomeOtherClass w okresie istnienia MyClass. Obiekt SomeOtherClass jest używany w kilku funkcjach, więc dynamicznie alokowałem pamięć: obiekt SomeOtherClass jest tworzony podczas tworzenia MyClass, używany kilka razy w ciągu życia obiektu, a następnie zwalniany po zwolnieniu MyClass.
Oczywiście, gdyby był to prawdziwy kod, nie byłoby powodu (poza możliwym zużyciem pamięci stosu), aby tworzyć myObject w ten sposób, ale ten rodzaj tworzenia / niszczenia obiektów staje się przydatny, gdy masz dużo obiektów i chcesz dokładnie kontrolować kiedy są tworzone i niszczone (na przykład, aby aplikacja nie zużywała 1 GB pamięci RAM przez cały okres życia), aw środowisku okienkowym jest to prawie obowiązkowe, ponieważ obiekty, które tworzysz (powiedzmy przyciski) , muszą dobrze istnieć poza zakresem określonej funkcji (lub nawet klasy).
źródło