Domanda

Ho sempre sentito dire che in C devi davvero guardare come gestisci la memoria.E sto ancora iniziando a imparare il C, ma finora non ho dovuto svolgere alcuna attività correlata alla gestione della memoria.Ho sempre immaginato di dover rilasciare variabili e fare ogni sorta di cose brutte.Ma questo non sembra essere il caso.

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

È stato utile?

Soluzione

Esistono due posti in cui è possibile inserire le variabili in memoria.Quando crei una variabile come questa:

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

Le variabili vengono create nel "pila".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 utilizzeranno solo variabili di stack.

Lo stack è carino perché è automatico, ma presenta anche due inconvenienti:(1) Il compilatore deve sapere in anticipo quanto sono grandi le variabili e (b) lo spazio dello stack è alquanto limitato.Per esempio:in Windows, nelle impostazioni predefinite per il 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 array o di una struttura grande, hai bisogno del "piano B".

Il piano B si chiama "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, anche se ci sono altri modi:

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

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

Una volta creata una variabile 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 di rilasciarla in modo che la memoria possa essere utilizzata per altri scopi.Per il caso di cui sopra, con:

free(p);

Ciò che rende questa seconda opzione una "brutta faccenda" è che non è sempre facile sapere quando la variabile non è più necessaria.Dimenticare di rilasciare una variabile quando non ti serve farà sì che il tuo programma consumi più memoria di quella necessaria.Questa situazione è chiamata "perdita".La memoria "persa" non può essere utilizzata per nulla finché il programma non termina e il sistema operativo non recupera tutte le sue risorse.Problemi ancora più gravi sono possibili se si rilascia per errore una variabile heap Prima in realtà hai finito.

In C e C++, sei responsabile di ripulire le variabili heap come mostrato sopra.Tuttavia, esistono linguaggi e ambienti come Java e linguaggi .NET come C# che utilizzano un approccio diverso, in cui l'heap viene ripulito autonomamente.Questo secondo metodo, chiamato "garbage collection", è molto più semplice per lo sviluppatore ma paga una penalità in termini di costi generali e prestazioni.È un equilibrio.

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

Altri suggerimenti

Ecco un esempio.Supponiamo 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 memoria (tramite malloc) senza liberarla.Hai perso il puntatore al primo blocco di memoria quando hai chiamato strdup la seconda volta.

Questo 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 giga di memoria (forse di più, a seconda del tuo gestore di memoria) e se non hai subito arresti anomali, il tuo processo probabilmente sta funzionando piuttosto lentamente.

Per risolvere il problema, devi 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 aiuti!

Devi eseguire la "gestione della memoria" quando vuoi utilizzare la memoria sull'heap anziché sullo stack.Se non sai quanto grande deve essere creato un array fino al momento dell'esecuzione, devi utilizzare l'heap.Ad esempio, potresti voler memorizzare qualcosa in una stringa, ma non sapere quanto sarà grande il suo contenuto finché il programma non verrà eseguito.In tal caso scriveresti 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

Penso che il modo più conciso per rispondere alla domanda sia 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 darti la zappa sui piedi.

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 un linguaggio C efficace.

Le risposte pubblicate fino ad oggi si concentrano sulle allocazioni 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 limite, in alcuni scenari desktop puoi utilizzare tranquillamente i megabyte.

L'allocazione dell'heap è meno inerente alla lingua.Si tratta fondamentalmente di un insieme di chiamate alla libreria che ti garantiscono la proprietà di un blocco di memoria di una determinata dimensione finché non sei pronto a restituirlo ("liberarlo").Sembra semplice, ma è associato a un dolore indicibile del programmatore.I problemi sono semplici (liberare la stessa memoria due volte, o non liberarla affatto [perdite di memoria], non allocare memoria sufficiente [overflow del buffer], ecc.) ma difficili da evitare ed eseguire il debug.Un approccio altamente disciplinato è assolutamente obbligatorio nella pratica, ma ovviamente il linguaggio non lo impone.

Vorrei menzionare un altro tipo di allocazione di memoria che è stata ignorata da altri post.È possibile allocare staticamente le variabili dichiarandole all'esterno di qualsiasi funzione.Penso che in generale questo tipo di allocazione abbia una cattiva reputazione perché viene utilizzato da variabili globali.Tuttavia non c'è nulla che dica che l'unico modo per utilizzare la memoria allocata in questo modo sia 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 heap e automatica.Alcuni programmatori C sono sorpresi nell'apprendere che grandi e sofisticati programmi embedded e giochi C sono stati costruiti senza alcun utilizzo dell'allocazione heap.

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 viene fatto correttamente cosa finisci quello che stai facendo è il cugino di questo sito, un buffer overflow, e potresti sovrascrivere la memoria 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 l'allocazione della 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 gratuito, ma potrebbe essere utilizzato anche da un'altra applicazione - Questa è la parte critica della gestione della memoria, dove un errore avrà conseguenze imprevedibili (e talvolta irripetibili).

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

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é poi, hm...otterrai uno stack overflow.

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

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

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 allochi molto spazio nello stack contemporaneamente, questo è un bug.) In genere, la prima struttura di dati che dovrai imparare è una sorta di elenco.Eccone uno singolo collegato, in cima alla 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 ti piacerebbe avere qualche altra funzione, ma fondamentalmente questo è ciò per cui hai bisogno della gestione della memoria.Dovrei sottolineare che ci sono una serie di trucchi possibili con la gestione "manuale" della memoria, ad esempio,

  • Usando il fatto che malloc è garantito (dallo standard del linguaggio) per restituire un puntatore divisibile per 4,
  • allocare spazio extra per qualche tuo sinistro scopo,
  • creando pool di memoriaS..

Procurati un buon debugger... Buona fortuna!

@EuroMicelli

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

@Ted Percival:
...non è necessario eseguire il cast del valore restituito di malloc().

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

Non mi piacciono molte delle 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 il compilatore catturi un bug silenzioso.Tuttavia, non ho un'opinione forte al riguardo, in un modo o nell'altro.

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

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

In C, in realtà hai due scelte diverse.Uno, puoi lasciare che il sistema gestisca la memoria per te.In alternativa, puoi farlo da solo.In generale, vorresti 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;
   struct pair *next;
}

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

B.si desidera avere 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);
for(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 davvero disordinata):

struct data{ int data_type; long data_in_mem; }; struct animal{/*something*/}; struct person{/*some other thing*/}; struct animal* read_animal(); struct person* read_person(); /*In main*/ struct data sample; sampe.data_type = input_type; switch(input_type){ case DATA_PERSON: sample.data_in_mem = read_person(); break; case DATA_ANIMAL: sample.data_in_mem = read_animal(); default: printf("Oh hoh! I warn you, that again and I will seg fault your OS"); }

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

Tuttavia, in generale, dovresti stare lontano dai tuoi trucchi preferiti (T___T).Prima o poi romperai il tuo sistema operativo se lo usi troppo spesso.Finché non usi *alloc e free, puoi dire con certezza che sei ancora vergine e che il codice sembra ancora carino.

Sicuro.Se crei un oggetto che esiste al di fuori dell'ambito in cui lo usi.Ecco un esempio inventato (tieni presente che la mia sintassi sarà disattivata;il mio C è arrugginito, ma questo esempio illustrerà comunque 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 vita di MyClass.L'oggetto SomeOtherClass viene utilizzato in diverse funzioni, quindi ho allocato dinamicamente la memoria:l'oggetto SomeOtherClass viene creato quando viene creata MyClass, utilizzato più volte nel corso della vita dell'oggetto e quindi liberato una volta liberata MyClass.

Ovviamente se si trattasse di codice reale, non ci sarebbe motivo (a parte il possibile 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 risucchi 1 GB di RAM per tutta la sua vita, ad esempio), e in un ambiente con finestre, questo è praticamente obbligatorio, poiché gli oggetti che crei (pulsanti, diciamo) , devono esistere ben al di fuori dell'ambito di una particolare funzione (o anche di una classe).

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top