C Zarządzanie pamięcią

90

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ą”?

The.Anti.9
źródło
Dobre miejsce do nauki G4G
EsmaeelE,

Odpowiedzi:

231

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

Euro Micelli
źródło
3
Jeśli chcesz umieścić coś na stosie, ale nie wiesz, jak duże jest to w czasie kompilacji, przydzielanie () może powiększyć ramkę stosu, aby zrobić miejsce. Nie ma freea (), cała ramka stosu jest usuwana, gdy funkcja zwraca. Korzystanie z funkcji przydzielania () do dużych przydziałów jest obarczone niebezpieczeństwem.
DGentry,
1
Może mógłbyś dodać jedno lub dwa zdania na temat lokalizacji pamięci zmiennych globalnych
Michael Käfer
W C nigdy nie malloc()(char *)malloc(size);
rzucano
17

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!

Mark Harrison
źródło
Ta odpowiedź podoba mi się bardziej. Ale mam małe pytanie poboczne. Spodziewałbym się, że coś takiego zostanie rozwiązane za pomocą bibliotek, czy nie ma biblioteki, która naśladowałaby dokładnie podstawowe typy danych i dodawałaby do nich funkcje zwalniające pamięć, aby po użyciu zmiennych były one również uwalniane automatycznie?
Lorenzo,
Żadne, które nie są częścią standardu. Jeśli przejdziesz do C ++, otrzymasz ciągi znaków i kontenery, które wykonują automatyczne zarządzanie pamięcią.
Mark Harrison,
Rozumiem, więc są jakieś biblioteki innych firm? Czy mógłbyś je nazwać?
Lorenzo,
9

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
Jeremy Ruten
źródło
5

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.

Bill Forster
źródło
4

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.

Chris BC
źródło
Tutaj przydzielasz 5 bajtów. Zwolnij go, przypisując wskaźnik. Każda próba zwolnienia tego wskaźnika prowadzi do nieokreślonego zachowania. Uwaga C-Strings nie przeciążają operatora =, nie ma kopii.
Martin York,
Chociaż to naprawdę zależy od używanego malloc. Wiele operatorów malloc dopasowuje się do 8 bajtów. Więc jeśli ten malloc używa systemu nagłówka / stopki, malloc zarezerwowałby 5 + 4 * 2 (4 bajty zarówno dla nagłówka, jak i stopki). To byłoby 13 bajtów, a malloc dałby po prostu dodatkowe 3 bajty na wyrównanie. Nie mówię, że dobrym pomysłem jest użycie tego, ponieważ będzie to tylko systemy, w których malloc działa w ten sposób, ale przynajmniej ważne jest, aby wiedzieć, dlaczego zrobienie czegoś złego może działać.
kodai
Loki: Zredagowałem odpowiedź, aby użyć strcpy()zamiast =; Zakładam, że taki był zamiar Chrisa BC.
echristopherson
Wierzę w sprzętową ochronę pamięci nowoczesnych platform, uniemożliwiającą procesom w przestrzeni użytkownika nadpisywanie przestrzeni adresowych innych procesów; zamiast tego pojawi się błąd segmentacji. Ale to nie jest część C per se.
echristopherson
4

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.

Hernán
źródło
2

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

Serge
źródło
2

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

  • Korzystając z faktu, że malloc jest gwarantowane (przez standard języka), aby zwrócić wskaźnik podzielny przez 4,
  • przeznaczenie dodatkowej przestrzeni na własny, złowieszczy cel,
  • tworzenie puli pamięci s ..

Zdobądź dobry debugger ... Powodzenia!

Anders Eurenius
źródło
Uczenie się struktur danych to kolejny kluczowy krok w zrozumieniu zarządzania pamięcią. Nauczenie się algorytmów, aby odpowiednio obsługiwać te struktury, pokaże Ci odpowiednie metody przezwyciężenia tych problemów. Dlatego na tych samych kursach można znaleźć struktury danych i algorytmy.
aj.toulan
0

@ 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ą.

Jonathan Branam
źródło
0

@ Ted Percival :
... nie musisz rzutować wartości zwracanej przez malloc ().

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.

Jest to szczególnie prawdopodobne, jeśli Twój kompilator obsługuje komentarze w stylu C ++.

Tak ... złapałeś mnie tam. Spędzam dużo więcej czasu w C ++ niż C. Dzięki, że to zauważyłeś.

Euro Micelli
źródło
@echristopherson, dzięki. Masz rację - ale pamiętaj, że to pytanie pochodziło z sierpnia 2008 r., Zanim Stack Overflow był nawet w publicznej wersji beta. Wtedy wciąż zastanawialiśmy się, jak powinna działać strona. Format tego pytania / odpowiedzi nie musi być koniecznie postrzegany jako model wykorzystania SO. Dzięki!
Euro Micelli
Ach, dziękuję za zwrócenie uwagi - nie zdawałem sobie sprawy, że ten aspekt strony wciąż się zmieniał.
echristopherson
0

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:

struct pair {
   int val;
   struct pair * next;
}

struct pair * new_pair (int val) {
   struct pair * np = malloc (sizeof (struct pair));
   np-> val = val;
   np-> next = NULL;
   powrót np;
}

b. chcesz mieć dynamicznie przydzielaną pamięć. Najczęstszym przykładem jest tablica bez stałej długości:

int * my_special_array;
my_special_array = malloc (sizeof (int) * number_of_element);
dla (i = 0; i

do. Chcesz zrobić coś NAPRAWDĘ brudnego. Na przykład chciałbym, aby struktura reprezentowała wiele rodzajów danych i nie lubię związku (związek wygląda bardzo niechlujnie):

struct data { int data_type; długi data_in_mem; }; struct animal {/ * coś * /}; struct person {/ * inna rzecz * /}; struct animal * read_animal (); struct person * read_person (); / * W głównym * / próbka danych strukturalnych; sampe.data_type = input_type; switch (input_type) { sprawa DATA_PERSON: sample.data_in_mem = read_person (); złamać; etui DATA_ANIMAL: sample.data_in_mem = read_animal (); domyślna: printf ("Oh hoh! Ostrzegam cię, że znowu i będę segregować twój system operacyjny"); }

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.

magia
źródło
„Widzisz, długa wartość wystarczy, by pomieścić WSZYSTKO” -: / o czym mówisz, w większości systemów długa wartość to 4 bajty, dokładnie to samo co int. Jedynym powodem, dla którego pasuje do wskaźników, jest to, że rozmiar long jest taki sam jak rozmiar wskaźnika. Jednak naprawdę powinieneś używać void *.
Score_Under
-2

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

Smerf
źródło
1
Heh, yeah, to jest C ++, prawda? Niesamowite, że ktoś zadzwonił do mnie po pięciu miesiącach.
TheSmurf