C Gestione della memoria


90

Ho sempre sentito dire che in C devi davvero guardare come gestisci la memoria. E sto ancora iniziando a imparare il C, ma fino ad ora non ho dovuto fare alcuna memoria per gestire le attività correlate .. Ho sempre immaginato di dover rilasciare variabili e fare ogni sorta di cose brutte. Ma non sembra essere così.

Qualcuno può mostrarmi (con esempi di codice) un esempio di quando dovresti fare un po 'di "gestione della memoria"?


Buon posto per imparare G4G
EsmaeelE

Risposte:


230

Ci sono due posti in cui le variabili possono essere memorizzate. Quando crei una variabile come questa:

int  a;
char c;
char d[16];

Le variabili vengono create nello " stack ". Le variabili di stack vengono automaticamente liberate quando escono dall'ambito (ovvero, quando il codice non può più raggiungerle). Potresti sentirle chiamate variabili "automatiche", ma questo è passato di moda.

Molti esempi per principianti useranno solo variabili di stack.

Lo stack è carino perché è automatico, ma ha anche due svantaggi: (1) Il compilatore deve sapere in anticipo quanto sono grandi le variabili e (b) lo spazio dello stack è alquanto limitato. Ad esempio: in Windows, nelle impostazioni predefinite del linker Microsoft, lo stack è impostato su 1 MB e non tutto è disponibile per le variabili.

Se non sai in fase di compilazione quanto è grande il tuo array, o se hai bisogno di un grande array o struct, hai bisogno del "piano B".

Il piano B è chiamato " mucchio ". Di solito puoi creare variabili grandi quanto il sistema operativo ti consente, ma devi farlo da solo. I post precedenti ti hanno mostrato un modo per farlo, sebbene ci siano altri modi:

int size;
// ...
// Set size to some value, based on information available at run-time. Then:
// ...
char *p = (char *)malloc(size);

(Si noti che le variabili nell'heap non vengono manipolate direttamente, ma tramite puntatori)

Dopo aver creato una variabile di heap, il problema è che il compilatore non può dire quando hai finito, quindi perdi il rilascio automatico. È qui che entra in gioco il "rilascio manuale" a cui ti riferivi. Il tuo codice ora è responsabile di decidere quando la variabile non è più necessaria e rilasciala in modo che la memoria possa essere utilizzata per altri scopi. Per il caso precedente, con:

free(p);

Ciò che rende questa seconda opzione un "brutto affare" è che non è sempre facile sapere quando la variabile non è più necessaria. Dimenticare di rilasciare una variabile quando non ne hai bisogno farà sì che il tuo programma consumi più memoria di cui ha bisogno. Questa situazione è chiamata "perdita". La memoria "trapelata" non può essere utilizzata per nulla finché il programma non termina e il sistema operativo non recupera tutte le sue risorse. Sono possibili problemi anche più fastidiosi se rilasci una variabile di heap per errore prima di averla effettivamente terminata.

In C e C ++, sei responsabile della pulizia delle variabili di heap come mostrato sopra. Tuttavia, ci sono linguaggi e ambienti come Java e .NET come C # che utilizzano un approccio diverso, in cui l'heap viene ripulito da solo. Questo secondo metodo, chiamato "garbage collection", è molto più semplice per lo sviluppatore ma si paga una penalità in termini di overhead e prestazioni. È un equilibrio.

(Ho sorvolato su molti dettagli per dare una risposta più semplice, ma spero più livellata)


3
Se vuoi mettere qualcosa in pila ma non sai quanto è grande in fase di compilazione, alloca () può ingrandire lo stack frame per fare spazio. Non c'è freea (), l'intero stack frame viene estratto quando la funzione ritorna. L'uso di alloca () per allocazioni di grandi dimensioni è irto di pericoli.
DGentry

1
Forse potresti aggiungere una o due frasi sulla posizione di memoria delle variabili globali
Michael Käfer

In C non ritorno gettato di malloc(), la sua causa UB, (char *)malloc(size);vedi stackoverflow.com/questions/605845/...
EsmaeelE

17

Ecco un esempio. Supponi di avere una funzione strdup () che duplica una stringa:

char *strdup(char *src)
{
    char * dest;
    dest = malloc(strlen(src) + 1);
    if (dest == NULL)
        abort();
    strcpy(dest, src);
    return dest;
}

E lo chiami così:

main()
{
    char *s;
    s = strdup("hello");
    printf("%s\n", s);
    s = strdup("world");
    printf("%s\n", s);
}

Puoi vedere che il programma funziona, ma hai allocato la memoria (tramite malloc) senza liberarla. Hai perso il puntatore al primo blocco di memoria quando hai chiamato strdup la seconda volta.

Non è un grosso problema per questa piccola quantità di memoria, ma considera il caso:

for (i = 0; i < 1000000000; ++i)  /* billion times */
    s = strdup("hello world");    /* 11 bytes */

Ora hai esaurito 11 GB di memoria (forse di più, a seconda del gestore della memoria) e se non hai bloccato il tuo processo probabilmente sta funzionando piuttosto lentamente.

Per risolvere il problema, è necessario chiamare free () per tutto ciò che si ottiene con malloc () dopo aver finito di usarlo:

s = strdup("hello");
free(s);  /* now not leaking memory! */
s = strdup("world");
...

Spero che questo esempio sia d'aiuto!


Mi piace di più questa risposta. Ma ho una piccola domanda secondaria. Mi aspetto che qualcosa di simile venga risolto con le librerie, non esiste una libreria che imiti fedelmente i tipi di dati di base e aggiunga loro funzionalità di liberazione della memoria in modo che quando le variabili vengono utilizzate vengano anche liberate automaticamente?
Lorenzo

Nessuno che faccia parte dello standard. Se vai in C ++ ottieni stringhe e contenitori che fanno la gestione automatica della memoria.
Mark Harrison

Capisco, quindi ci sono alcune librerie di terze parti? Potresti nominarli?
Lorenzo

9

È necessario eseguire la "gestione della memoria" quando si desidera utilizzare la memoria sull'heap anziché sullo stack. Se non sai quanto grande creare un array fino al runtime, devi usare l'heap. Ad esempio, potresti voler memorizzare qualcosa in una stringa, ma non sapere quanto sarà grande il suo contenuto finché il programma non viene eseguito. In tal caso dovresti scrivere qualcosa del genere:

 char *string = malloc(stringlength); // stringlength is the number of bytes to allocate

 // Do something with the string...

 free(string); // Free the allocated memory

5

Penso che il modo più conciso per rispondere alla domanda nel considerare il ruolo del puntatore in C. Il puntatore è un meccanismo leggero ma potente che ti dà un'immensa libertà a costo di un'immensa capacità di spararti sul piede.

In C la responsabilità di garantire che i tuoi puntatori puntino alla memoria che possiedi è tua e solo tua. Ciò richiede un approccio organizzato e disciplinato, a meno che non si abbandonino i puntatori, il che rende difficile scrivere C.

Le risposte pubblicate fino ad oggi si concentrano su allocazioni di variabili automatiche (stack) e heap. L'uso dell'allocazione dello stack rende la memoria gestita automaticamente e conveniente, ma in alcune circostanze (buffer di grandi dimensioni, algoritmi ricorsivi) può portare all'orrendo problema dell'overflow dello stack. Sapere esattamente quanta memoria è possibile allocare nello stack dipende molto dal sistema. In alcuni scenari incorporati alcune dozzine di byte potrebbero essere il tuo limite, in alcuni scenari desktop puoi tranquillamente utilizzare megabyte.

L'allocazione dell'heap è meno inerente alla lingua. È fondamentalmente un insieme di chiamate di libreria che ti garantisce la proprietà di un blocco di memoria di una determinata dimensione fino a quando non sei pronto per restituirlo ("libero"). Sembra semplice, ma è associato a un indicibile dolore del programmatore. I problemi sono semplici (liberare la stessa memoria due volte, o per niente [perdite di memoria], non allocare abbastanza memoria [buffer overflow], ecc.) Ma difficili da evitare ed eseguire il debug. Un approccio altamente disciplinato è assolutamente obbligatorio nella pratica, ma ovviamente la lingua in realtà non lo impone.

Vorrei menzionare un altro tipo di allocazione della memoria che è stato ignorato da altri post. È possibile allocare staticamente le variabili dichiarandole al di fuori di qualsiasi funzione. Penso che in generale questo tipo di allocazione abbia una cattiva reputazione perché viene utilizzato dalle variabili globali. Tuttavia non c'è nulla che dica che l'unico modo per utilizzare la memoria allocata in questo modo è come una variabile globale indisciplinata in un pasticcio di codice spaghetti. Il metodo di allocazione statica può essere utilizzato semplicemente per evitare alcune delle insidie ​​dei metodi di allocazione automatica e heap. Alcuni programmatori in C sono sorpresi di apprendere che i programmi incorporati e per giochi C ampi e sofisticati sono stati costruiti senza alcun uso dell'allocazione di heap.


4

Ci sono alcune ottime risposte qui su come allocare e liberare memoria, e secondo me il lato più impegnativo dell'uso di C è garantire che l'unica memoria che usi sia la memoria che hai allocato - se questo non è fatto correttamente cosa finisci up with è il cugino di questo sito - un buffer overflow - e potresti sovrascrivere la memoria che viene utilizzata da un'altra applicazione, con risultati molto imprevedibili.

Un esempio:

int main() {
    char* myString = (char*)malloc(5*sizeof(char));
    myString = "abcd";
}

A questo punto hai allocato 5 byte per myString e lo hai riempito con "abcd \ 0" (le stringhe terminano con un null - \ 0). Se la tua allocazione di stringa era

myString = "abcde";

Assegneresti "abcde" nei 5 byte che hai assegnato al tuo programma, e il carattere nullo finale verrebbe messo alla fine di questo - una parte di memoria che non è stata allocata per il tuo uso e potrebbe essere libero, ma potrebbe essere ugualmente utilizzato da un'altra applicazione - Questa è la parte critica della gestione della memoria, dove un errore avrà conseguenze imprevedibili (e talvolta irripetibili).


Qui assegni 5 byte. Perderlo assegnando un puntatore. Qualsiasi tentativo di liberare questo puntatore porta a un comportamento indefinito. Nota Le stringhe C non sovraccaricano l'operatore = non c'è copia.
Martin York,

Tuttavia, dipende davvero dal malloc che stai utilizzando. Molti operatori malloc si allineano a 8 byte. Quindi, se questo malloc utilizza un sistema di intestazione / piè di pagina, malloc riserverà 5 + 4 * 2 (4 byte sia per l'intestazione che per il piè di pagina). Sarebbero 13 byte e malloc ti darebbe solo 3 byte extra per l'allineamento. Non sto dicendo che sia una buona idea usarlo, perché funzionerà solo su sistemi il cui malloc funziona in questo modo, ma è almeno importante sapere perché fare qualcosa di sbagliato potrebbe funzionare.
kodai

Loki: Ho modificato la risposta da usare al strcpy()posto di =; Presumo che fosse l'intenzione di Chris BC.
echristopherson

Credo che nelle piattaforme moderne la protezione della memoria hardware impedisca ai processi dello spazio utente di sovrascrivere gli spazi degli indirizzi di altri processi; avresti invece un errore di segmentazione. Ma questo non fa parte di C di per sé.
echristopherson

4

Una cosa da ricordare è di inizializzare sempre i puntatori a NULL, poiché un puntatore non inizializzato può contenere un indirizzo di memoria valido pseudocasuale che può far procedere silenziosamente gli errori del puntatore. Imponendo che un puntatore venga inizializzato con NULL, puoi sempre capire se stai usando questo puntatore senza inizializzarlo. Il motivo è che i sistemi operativi "cablano" l'indirizzo virtuale 0x00000000 alle eccezioni di protezione generale per intercettare l'utilizzo del puntatore nullo.


2

Inoltre potresti voler utilizzare l'allocazione dinamica della memoria quando devi definire un array enorme, ad esempio int [10000]. Non puoi semplicemente metterlo in pila perché allora, hm ... otterrai uno stack overflow.

Un altro buon esempio potrebbe essere l'implementazione di una struttura dati, ad esempio un elenco collegato o un albero binario. Non ho un codice di esempio da incollare qui ma puoi cercarlo facilmente su Google.


2

(Sto scrivendo perché sento che le risposte finora non sono del tutto esatte.)

Il motivo per cui è necessario menzionare la gestione della memoria è quando si ha un problema / soluzione che richiede la creazione di strutture complesse. (Se i tuoi programmi si bloccano se assegni molto spazio nello stack in una volta, è un bug.) In genere, la prima struttura di dati che devi imparare è una sorta di elenco . Eccone uno solo collegato, fuori dalla mia testa:

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;
}

Naturalmente, vorresti alcune altre funzioni, ma fondamentalmente, questo è ciò per cui hai bisogno della gestione della memoria. Vorrei sottolineare che ci sono molti trucchi possibili con la gestione della memoria "manuale", ad es.

  • Utilizzando il fatto che malloc è garantito (dallo standard del linguaggio) per restituire un puntatore divisibile per 4,
  • assegnando spazio extra per qualche tuo scopo sinistro,
  • creazione di pool di memoria ..

Procurati un buon debugger ... Buona fortuna!


L'apprendimento delle strutture dati è il prossimo passo chiave nella comprensione della gestione della memoria. Imparare gli algoritmi per eseguire in modo appropriato queste strutture ti mostrerà i metodi appropriati per superare questi ostacoli. Questo è il motivo per cui troverai strutture dati e algoritmi insegnati negli stessi corsi.
aj.toulan

0

@ Euro Micelli

Un aspetto negativo da aggiungere è che i puntatori allo stack non sono più validi quando la funzione ritorna, quindi non è possibile restituire un puntatore a una variabile dello stack da una funzione. Questo è un errore comune e uno dei motivi principali per cui non puoi cavartela con le sole variabili di stack. Se la tua funzione deve restituire un puntatore, devi eseguire il malloc e occuparti della gestione della memoria.


0

@ Ted Percival :
... non è necessario lanciare il valore di ritorno di malloc ().

Hai ragione, ovviamente. Credo che sia sempre stato vero, anche se non ho una copia di K&R da controllare.

Non mi piacciono molte le conversioni implicite in C, quindi tendo a usare i cast per rendere la "magia" più visibile. A volte aiuta la leggibilità, a volte no, e talvolta fa sì che un bug silenzioso venga catturato dal compilatore. Tuttavia, non ho una forte opinione su questo, in un modo o nell'altro.

Ciò è particolarmente probabile se il compilatore comprende i commenti in stile C ++.

Sì ... mi hai beccato lì. Trascorro molto più tempo in C ++ che in C. Grazie per averlo notato.


@echristopherson, grazie. Hai ragione, ma tieni presente che questa domanda / risposta risale all'agosto 2008, prima che Stack Overflow fosse anche in versione beta pubblica. Allora stavamo ancora cercando di capire come avrebbe dovuto funzionare il sito. Il formato di questa domanda / risposta non deve essere necessariamente visto come un modello per come utilizzare SO. Grazie!
Euro Micelli

Ah, grazie per averlo fatto notare - non mi ero reso conto che l'aspetto del sito fosse ancora in evoluzione allora.
echristopherson

0

In C, hai effettivamente due scelte diverse. Uno, puoi lasciare che il sistema gestisca la memoria per te. In alternativa, puoi farlo da solo. In generale, dovresti attenersi al primo il più a lungo possibile. Tuttavia, la memoria gestita automaticamente in C è estremamente limitata e in molti casi sarà necessario gestire manualmente la memoria, ad esempio:

un. Vuoi che la variabile sopravviva alle funzioni e non vuoi avere una variabile globale. ex:

struct pair {
   int val;
   coppia di strutture * successiva;
}

coppia di strutture * new_pair (int val) {
   coppia di strutture * np = malloc (sizeof (coppia di strutture));
   np-> val = val;
   np-> successivo = NULL;
   return np;
}

b. si desidera disporre di memoria allocata dinamicamente. L'esempio più comune è l'array senza lunghezza fissa:

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

c. Vuoi fare qualcosa di VERAMENTE sporco. Ad esempio, vorrei che una struttura rappresentasse molti tipi di dati e non mi piace l'unione (l'unione sembra così disordinata):

struct data { int data_type; long data_in_mem; }; struct animal {/ * qualcosa * /}; struct persona {/ * qualche altra cosa * /}; struct animal * read_animal (); struct persona * read_person (); / * In principale * / struttura dati campione; sampe.data_type = input_type; switch (input_type) { caso DATA_PERSON: sample.data_in_mem = read_person (); rompere; caso DATA_ANIMAL: sample.data_in_mem = read_animal (); predefinito: printf ("Oh hoh! Ti avverto, di nuovo e segnerò il tuo sistema operativo"); }

Vedi, un valore lungo è sufficiente per contenere QUALSIASI COSA. Ricorda solo di liberarlo, o te ne pentirai. Questo è uno dei miei trucchi preferiti per divertirmi in C: D.

Tuttavia, in genere, dovresti stare lontano dai tuoi trucchi preferiti (T___T). Romperai il tuo sistema operativo, prima o poi, se li usi troppo spesso. Finché non usi * alloc e free, è sicuro che sei ancora vergine e che il codice sembra ancora carino.


"Vedi, un valore lungo è sufficiente per contenere QUALSIASI COSA" -: / di cosa stai parlando, sulla maggior parte dei sistemi un valore lungo è di 4 byte, esattamente come un int. L'unico motivo per cui si inseriscono i puntatori qui è perché la dimensione di long sembra essere la stessa della dimensione del puntatore. Tuttavia, dovresti davvero usare void *.
Score_Under

-2

Sicuro. Se crei un oggetto che esiste al di fuori dell'ambito in cui lo usi. Ecco un esempio artificioso (tieni presente che la mia sintassi sarà disattivata; il mio C è arrugginito, ma questo esempio illustrerà ancora il concetto):

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();
   }


};

In questo esempio, sto utilizzando un oggetto di tipo SomeOtherClass durante la durata di MyClass. L'oggetto SomeOtherClass viene utilizzato in diverse funzioni, quindi ho allocato dinamicamente la memoria: l'oggetto SomeOtherClass viene creato quando MyClass viene creato, utilizzato più volte durante la vita dell'oggetto e quindi liberato una volta che MyClass viene liberato.

Ovviamente se questo fosse codice reale, non ci sarebbe motivo (a parte forse il consumo di memoria dello stack) per creare myObject in questo modo, ma questo tipo di creazione / distruzione di oggetti diventa utile quando si hanno molti oggetti e si desidera controllare con precisione quando vengono creati e distrutti (in modo che la tua applicazione non assorba 1 GB di RAM per tutta la sua vita, ad esempio), e in un ambiente a finestre, questo è praticamente obbligatorio, poiché gli oggetti che crei (pulsanti, diciamo) , devono esistere ben al di fuori dell'ambito di una funzione particolare (o anche di una classe).


1
Eh, sì, quello è C ++ non è vero? Incredibile che ci siano voluti cinque mesi prima che qualcuno mi chiamasse.
TheSmurf
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.