Szukaj…


Wprowadzenie

Do zarządzania dynamicznie przydzielaną pamięcią standardowa biblioteka C udostępnia funkcje malloc() , calloc() , realloc() i free() . W wersji C99 i nowszych istnieje również aligned_alloc() . Niektóre systemy udostępniają także alloca() .

Składnia

  • void * wyrównane_alloc (wyrównanie size_t, size_t rozmiar); / * Tylko od C11 * /
  • void * calloc (size_t nelements, size_t size);
  • void free (void * ptr);
  • void * malloc (rozmiar_t rozmiar);
  • void * realloc (void * ptr, size_t size);
  • void * przydzielenie (rozmiar_t rozmiar); / * z przydzielania.h, niestandardowe, nieprzenośne, niebezpieczne. * /

Parametry

Nazwa opis
rozmiar ( malloc , realloc i aligned_alloc ) całkowity rozmiar pamięci w bajtach. W przypadku aligned_alloc rozmiar musi być całkowitą wielokrotnością wyrównania.
rozmiar ( calloc ) rozmiar każdego elementu
elementy liczba elementów
ptr wskaźnik do przydzielonej pamięci poprzednio zwracany przez malloc , calloc , realloc lub aligned_alloc
wyrównanie wyrównanie przydzielonej pamięci

Uwagi

C11

Zauważ, że aligned_alloc() jest zdefiniowana tylko dla C11 lub nowszej.

Systemy takie jak oparte na POSIX zapewniają inne sposoby przydzielania wyrównanej pamięci (np. posix_memalign() ), a także mają inne opcje zarządzania pamięcią (np. posix_memalign() mmap() ).

Uwalnianie pamięci

Możliwe jest zwolnienie pamięci przydzielanej dynamicznie poprzez wywołanie free () .

int *p = malloc(10 * sizeof *p); /* allocation of memory */
if (p == NULL) 
{
    perror("malloc failed");
    return -1;
}

free(p); /* release of memory */
/* note that after free(p), even using the *value* of the pointer p
   has undefined behavior, until a new value is stored into it. */

/* reusing/re-purposing the pointer itself */
int i = 42;
p = &i; /* This is valid, has defined behaviour */

Pamięć wskazywana przez p jest odzyskiwana (albo przez implementację libc, albo przez podstawowy system operacyjny) po wywołaniu free() , więc dostęp do zwolnionego bloku pamięci przez p doprowadzi do nieokreślonego zachowania . Wskaźniki odwołujące się do uwolnionych elementów pamięci są zwykle nazywane zwisającymi wskaźnikami i stanowią zagrożenie bezpieczeństwa. Ponadto standard C stwierdza, że nawet uzyskanie dostępu do wartości wiszącego wskaźnika ma niezdefiniowane zachowanie. Zauważ, że sam wskaźnik p może być ponownie wykorzystany, jak pokazano powyżej.

Pamiętaj, że możesz wywoływać free() w przypadku wskaźników, które zostały zwrócone bezpośrednio z funkcji malloc() , calloc() , realloc() i aligned_alloc() , lub gdy dokumentacja mówi, że pamięć została przydzielona w ten sposób (funkcje podobnie jak strdup () to godne uwagi przykłady). Uwolnienie wskaźnika, który jest

  • uzyskane za pomocą operatora & na zmiennej, lub
  • w środku przydzielonego bloku

jest zabronione. Taki błąd zwykle nie zostanie zdiagnozowany przez kompilator, ale spowoduje wykonanie programu w nieokreślonym stanie.

Istnieją dwie popularne strategie zapobiegania takim przypadkom niezdefiniowanego zachowania.

Pierwszy i preferowany jest prosty - sam p przestaje istnieć, gdy nie jest już potrzebny, na przykład:

if (something_is_needed())
{

    int *p = malloc(10 * sizeof *p);
    if (p == NULL) 
    {
        perror("malloc failed");
        return -1;
    }

    /* do whatever is needed with p */

    free(p);
}

Wywołując free() bezpośrednio przed końcem bloku zawierającego (tj. } ), Samo p przestaje istnieć. Kompilator wyświetli błąd kompilacji przy każdej próbie użycia p po tym.

Drugim podejściem jest również unieważnienie samego wskaźnika po zwolnieniu pamięci, na którą wskazuje:

free(p);
p = NULL;     // you may also use 0 instead of NULL

Argumenty za tym podejściem:

  • Na wielu platformach próba wyzerowania wskaźnika zerowego spowoduje natychmiastową awarię: błąd segmentacji. Tutaj otrzymujemy co najmniej ślad stosu wskazujący na zmienną, która została użyta po uwolnieniu.

    Bez ustawiania wskaźnika na NULL mamy wiszący wskaźnik. Program najprawdopodobniej nadal ulegnie awarii, ale później, ponieważ pamięć, na którą wskazuje wskaźnik, zostanie po cichu uszkodzona. Takie błędy są trudne do wyśledzenia, ponieważ mogą powodować stos wywołań, który jest całkowicie niezwiązany z początkowym problemem.

    Podejście to jest zatem zgodne z koncepcją szybkiego działania .

  • Uwolnienie wskaźnika zerowego jest bezpieczne. Norma C określa, że free(NULL) nie ma wpływu:

    Funkcja swobodna powoduje zwolnienie przestrzeni wskazywanej przez ptr, czyli udostępnienie jej do dalszego przydzielenia. Jeśli ptr jest wskaźnikiem zerowym, nie występuje żadna akcja. W przeciwnym razie, jeśli argument nie pasuje do wskaźnika wcześniej zwróconego przez funkcję calloc , malloc lub realloc , lub jeśli miejsce zostało zwolnione przez wywołanie free lub realloc , zachowanie jest niezdefiniowane.

  • Czasami nie można zastosować pierwszego podejścia (np. Pamięć jest alokowana w jednej funkcji, a zwolniona znacznie później w zupełnie innej funkcji)

Przydzielanie pamięci

Standardowy przydział

Funkcje dynamicznego przydzielania pamięci C są zdefiniowane w nagłówku <stdlib.h> . Jeśli chcemy dynamicznie przydzielać przestrzeń pamięci dla obiektu, można użyć następującego kodu:

int *p = malloc(10 * sizeof *p);
if (p == NULL) 
{
    perror("malloc() failed");
    return -1;
}

To oblicza liczbę bajtów zajętych w pamięci przez dziesięć int , a następnie żąda tylu bajtów od malloc i przypisuje wynik (tj. Adres początkowy fragmentu pamięci, który właśnie utworzono za pomocą malloc ) do wskaźnika o nazwie p .

Dobrą praktyką jest używanie sizeof do obliczania ilości pamięci, która ma zostać zażądana, ponieważ wynik sizeof jest zdefiniowany w implementacji (z wyjątkiem typów znaków , które są char , signed char i unsigned char , dla których sizeof ma zawsze wartość 1 ).

Ponieważ malloc może nie być w stanie obsłużyć żądania, może zwrócić wskaźnik zerowy. Ważne jest, aby to sprawdzić, aby zapobiec późniejszym próbom wyrejestrowania wskaźnika zerowego.

Pamięć przydzielana dynamicznie za pomocą malloc() może być zmieniana za pomocą realloc() lub, gdy nie jest już potrzebna, zwalniana za pomocą free() .

Alternatywnie, deklarowanie int array[10]; przydzieli tę samą ilość pamięci. Jeśli jednak zostanie zadeklarowany w funkcji bez słowa kluczowego static , będzie można go używać tylko w funkcji, w której jest zadeklarowany i w funkcjach, które wywołuje (ponieważ tablica zostanie przydzielona na stosie, a miejsce zostanie zwolnione do ponownego użycia, gdy funkcja powraca). Alternatywnie, jeśli jest zdefiniowany static w funkcji lub jeśli jest zdefiniowany poza jakąkolwiek funkcją, wówczas jego żywotność jest czasem życia programu. Wskaźniki mogą być również zwracane z funkcji, jednak funkcja w C nie może zwrócić tablicy.

Zerowana pamięć

Pamięć zwrócona przez malloc może nie zostać zainicjowana do rozsądnej wartości i należy zadbać o zerowanie pamięci za pomocą memset pamięci lub natychmiastowe skopiowanie do niej odpowiedniej wartości. Alternatywnie, calloc zwraca blok o pożądanym rozmiarze, w którym wszystkie bity są inicjowane na 0 . Nie musi to być to samo, co reprezentacja zmiennoprzecinkowego zera lub stałej wskaźnika zerowego.

int *p = calloc(10, sizeof *p);
if (p == NULL) 
{
    perror("calloc() failed");
    return -1;
}

Uwaga na temat calloc : Większość (najczęściej używanych) implementacji zoptymalizuje calloc() kątem wydajności, więc będzie szybsze niż wywołanie malloc() , a następnie memset() , nawet jeśli efekt netto jest identyczny.

Wyrównana pamięć

C11

C11 wprowadził nową funkcję aligned_alloc() która przydziela miejsce z danym wyrównaniem. Można go użyć, jeśli pamięć, która ma zostać przydzielona, musi zostać wyrównana na określonych granicach, których nie można spełnić przy pomocy malloc() lub calloc() . Funkcje malloc() i calloc() przydzielają pamięć, która jest odpowiednio wyrównana dla dowolnego typu obiektu (tj. wyrównanie to alignof(max_align_t) ). Ale dzięki aligned_alloc() można aligned_alloc() większych wyrównań.

/* Allocates 1024 bytes with 256 bytes alignment. */
char *ptr = aligned_alloc(256, 1024);
if (ptr) {
    perror("aligned_alloc()");
    return -1;
}
free(ptr);

Standard C11 nakłada dwa ograniczenia: 1) żądany rozmiar (drugi argument) musi być całkowitą wielokrotnością wyrównania (pierwszy argument) i 2) wartość wyrównania powinna być poprawnym wyrównaniem obsługiwanym przez implementację. Niespełnienie któregokolwiek z nich powoduje niezdefiniowane zachowanie .

Ponowne przydzielanie pamięci

Po przydzieleniu pamięci może być konieczne zwiększenie lub zmniejszenie przestrzeni pamięci wskaźnika. Funkcja void *realloc(void *ptr, size_t size) zwalnia stary obiekt wskazywany przez ptr i zwraca wskaźnik do obiektu o rozmiarze określonym przez size . ptr jest wskaźnikiem do bloku pamięci uprzednio przypisanego do malloc , calloc lub realloc (lub wskaźnika zerowego), który ma zostać ponownie przydzielony. Zachowana jest maksymalna możliwa zawartość oryginalnej pamięci. Jeśli nowy rozmiar jest większy, wszelka dodatkowa pamięć poza starym rozmiarem nie jest inicjowana. Jeśli nowy rozmiar jest krótszy, zawartość skurczonej części zostanie utracona. Jeśli ptr ma wartość NULL, przydzielany jest nowy blok, a funkcja zwraca do niego wskaźnik.

#include <stdio.h>
#include <stdlib.h>
 
int main(void)
{
    int *p = malloc(10 * sizeof *p);
    if (NULL == p) 
    {
        perror("malloc() failed");
        return EXIT_FAILURE;
    }
 
    p[0] = 42;
    p[9] = 15;

    /* Reallocate array to a larger size, storing the result into a
     * temporary pointer in case realloc() fails. */
    {
        int *temporary = realloc(p, 1000000 * sizeof *temporary);

        /* realloc() failed, the original allocation was not free'd yet. */
        if (NULL == temporary)
        {
            perror("realloc() failed");
            free(p); /* Clean up. */
            return EXIT_FAILURE;
        }      

        p = temporary;
    }

    /* From here on, array can be used with the new size it was 
     * realloc'ed to, until it is free'd. */

    /* The values of p[0] to p[9] are preserved, so this will print:
       42 15
    */
    printf("%d %d\n", p[0], p[9]);

    free(p);

    return EXIT_SUCCESS;
}

Ponownie przydzielony obiekt może, ale nie musi mieć taki sam adres jak *p . Dlatego ważne jest, aby przechwycić wartość realloc z realloc która zawiera nowy adres, jeśli połączenie się powiedzie.

Upewnij się, że przypisujesz wartość realloc przez realloc do temporary zamiast oryginalnego p . realloc zwróci null w przypadku jakiejkolwiek awarii, która nadpisze wskaźnik. Spowoduje to utratę danych i wyciek pamięci.

Wielowymiarowe tablice o zmiennej wielkości

C99

Od C99 C ma tablice o zmiennej długości, VLA, które modelują tablice z granicami, które są znane tylko w czasie inicjalizacji. Chociaż musisz uważać, aby nie przydzielić zbyt dużej VLA (mogą zniszczyć twój stos), używanie wskaźników do VLA i używanie ich w wyrażeniach sizeof jest w porządku.

double sumAll(size_t n, size_t m, double A[n][m]) {
    double ret = 0.0;
    for (size_t i = 0; i < n; ++i)
       for (size_t j = 0; j < m; ++j)
          ret += A[i][j]
    return ret;
}

int main(int argc, char *argv[argc+1]) {
   size_t n = argc*10;
   size_t m = argc*8;
   double (*matrix)[m] = malloc(sizeof(double[n][m]));
   // initialize matrix somehow
   double res = sumAll(n, m, matrix);
   printf("result is %g\n", res);
   free(matrix);
}

Powyżej matrix jest wskaźnikiem do elementów typu double[m] , a sizeof ekspresji z double[n][m] , zapewnia to, że zawiera przestrzeń dla n tych elementów.

Cała ta przestrzeń jest przydzielana w sposób ciągły, a zatem można ją free za pomocą pojedynczego wezwania do free .

Obecność VLA w języku wpływa również na możliwe deklaracje tablic i wskaźników w nagłówkach funkcji. Teraz ogólne wyrażenie liczb całkowitych jest dozwolone w [] parametrów tablicy. Dla obu funkcji wyrażenia w [] używają parametrów zadeklarowanych wcześniej na liście parametrów. W sumAll są długościami, których kod użytkownika oczekuje dla macierzy. Tak jak w przypadku wszystkich parametrów funkcji tablicy w C, najbardziej wewnętrzny wymiar jest przepisany na typ wskaźnika, więc jest to równoważne z deklaracją

  double sumAll(size_t n, size_t m, double (*A)[m]);

Oznacza to, że n nie jest tak naprawdę częścią interfejsu funkcji, ale informacje mogą być przydatne w dokumentacji i mogą być również wykorzystane przez kompilatory sprawdzające granice, aby ostrzec o dostępie poza granicami.

Podobnie w przypadku słowa main wyrażenie argc+1 to minimalna długość, jaką standard C określa dla argumentu argv .

Pamiętaj, że oficjalnie obsługa VLA jest opcjonalna w C11, ale nie znamy żadnego kompilatora, który implementuje C11 i nie ma ich. W razie __STDC_NO_VLA__ możesz przetestować za pomocą makra __STDC_NO_VLA__ .

realloc (ptr, 0) nie jest równoważny free (ptr)

realloc jest koncepcyjnie równoważne malloc + memcpy + free na drugim wskaźniku.

Jeśli wielkość żądanego miejsca wynosi zero, zachowanie realloc jest zdefiniowane w implementacji. Jest to podobne dla wszystkich funkcji alokacji pamięci, które otrzymują parametr size o wartości 0 . Takie funkcje mogą w rzeczywistości zwracać niezerowy wskaźnik, ale nie można tego nigdy zmieniać.

Zatem realloc(ptr,0) nie jest równoważny free(ptr) . Może

  • bądź „leniwą” implementacją i po prostu zwróć ptr
  • free(ptr) , przydziel element fikcyjny i zwróć go
  • free(ptr) i return 0
  • po prostu zwróć 0 dla niepowodzenia i nie rób nic więcej.

W szczególności te dwa ostatnie przypadki są nierozróżnialne według kodu aplikacji.

Oznacza to, że realloc(ptr,0) może tak naprawdę nie zwolnić / realloc(ptr,0) pamięci, a zatem nigdy nie powinien być używany jako zamiennik za free .

Zdefiniowane przez użytkownika zarządzanie pamięcią

malloc() często wywołuje podstawowe funkcje systemu operacyjnego w celu uzyskania stron pamięci. Ale nie ma nic specjalnego w tej funkcji i można ją zaimplementować w prostym C, deklarując dużą macierz statyczną i przydzielając z niej (istnieje niewielka trudność w zapewnieniu prawidłowego wyrównania, w praktyce wyrównanie do 8 bajtów jest prawie zawsze wystarczające).

Aby wdrożyć prosty schemat, blok kontrolny jest przechowywany w obszarze pamięci bezpośrednio przed wskaźnikiem, który ma zostać zwrócony z wywołania. Oznacza to, że free() można zaimplementować odejmując od zwróconego wskaźnika i odczytując informacje kontrolne, które zazwyczaj mają rozmiar bloku plus pewne informacje, które pozwalają na powrót do wolnej listy - połączonej listy nieprzydzielonych bloków.

Gdy użytkownik zażąda alokacji, bezpłatna lista jest przeszukiwana do momentu znalezienia bloku o identycznym lub większym rozmiarze w stosunku do żądanej kwoty, a następnie w razie potrzeby jest dzielona. Może to prowadzić do fragmentacji pamięci, jeśli użytkownik stale dokonuje wielu przydziałów i uwolnień o nieprzewidzianych rozmiarach i w nieprzewidywalnych odstępach czasu (nie wszystkie takie programy zachowują się w ten sposób, prosty schemat jest często odpowiedni dla małych programów).

/* typical control block */
struct block
{
   size_t size;         /* size of block */
   struct block *next;  /* next block in free list */ 
   struct block *prev;  /* back pointer to previous block in memory */
   void *padding;       /* need 16 bytes to make multiple of 8 */
}

static struct block arena[10000]; /* allocate from here */
static struct block *firstfree;

Wiele programów wymaga dużej liczby przydziałów małych obiektów o tym samym rozmiarze. Jest to bardzo łatwe do wdrożenia. Wystarczy użyć bloku z następnym wskaźnikiem. Więc jeśli wymagany jest blok 32 bajtów:

union block
{
   union block * next;
   unsigned char payload[32];
}  

static union block arena[100];
static union block * head; 
void init(void)
{
    int i;
    for (i = 0; i < 100 - 1; i++)
        arena[i].next = &arena[i + 1];
    arena[i].next = 0; /* last one, null */
    head = &block[0];
}
 
void *block_alloc()
{
    void *answer = head;
    if (answer)
        head = head->next;
    return answer;
}

void block_free(void *ptr)
{
    union block *block = ptr;
    block->next = head;
    head - block;
}

Ten schemat jest niezwykle szybki i skuteczny i można go uogólnić z pewną utratą przejrzystości.

przydzielanie: przydzielanie pamięci na stosie

Zastrzeżenie: alloca jest wymieniony tutaj tylko ze względu na kompletność. Jest całkowicie nieprzenośny (nie objęty żadnymi powszechnymi standardami) i ma wiele potencjalnie niebezpiecznych funkcji, które czynią go niebezpiecznym dla nieświadomych. Nowoczesny kod C powinien zastąpić go tablicami o zmiennej długości (VLA).

Strona podręcznika

#include <alloca.h>
// glibc version of stdlib.h include alloca.h by default

void foo(int size) {
    char *data = alloca(size);
    /*
      function body;
    */
    // data is automatically freed
}

Przydzielić pamięci na ramce stosu rozmówcy, przestrzeń odwołuje zwrócony wskaźnik jest automatycznie wolny „d Po zakończeniu czynności rozmówca.

Podczas gdy funkcja ta jest wygodna dla automatycznego zarządzania pamięcią, należy pamiętać, że zainteresowanie dużej alokacji może spowodować przepełnienie stosu, i że nie można korzystać free z pamięci przydzielonej z alloca (co może spowodować więcej problemu z przepełnienia stosu).

Z tego powodu nie zaleca się używania alloca wewnątrz pętli ani funkcji rekurencyjnej.

A ponieważ pamięć jest free po zwróceniu funkcji, nie można zwrócić wskaźnika jako wyniku funkcji ( zachowanie byłoby niezdefiniowane ).

streszczenie

  • połączenie identyczne jak malloc
  • automatycznie zwolniony po powrocie funkcji
  • niekompatybilny z free funkcjami realloc ( niezdefiniowane zachowanie )
  • wskaźnik nie może zostać zwrócony jako wynik funkcji ( niezdefiniowane zachowanie )
  • wielkość alokacji ograniczona przestrzenią stosu, która (na większości komputerów) jest znacznie mniejsza niż przestrzeń sterty dostępna do użycia przez malloc()
  • unikaj korzystania z alloca() replacea alloca() i VLA (tablic o zmiennej długości) w jednej funkcji
  • alloca() nie jest tak przenośna jak malloc() i in

Rekomendacje

  • Nie używaj funkcji alloca() w nowym kodzie
C99

Nowoczesna alternatywa.

void foo(int size) {
    char data[size];
    /*
      function body;
    */
    // data is automatically freed
}

Działa to gdzie alloca() robi, a prace w miejscach gdzie alloca() nie (wewnątrz pętli, na przykład). Zakłada implementację C99 lub implementację C11, która nie definiuje __STDC_NO_VLA__ .



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow