Ripasso dei concetti base di C - STB1019/SkullOfSummer GitHub Wiki

Introduzione

In questa sezione si andranno a ripassare tutti i concetti basilari di C. Il ripasso serve a rispolverare concetti che già dovreste sapere, giusto per essere sicuri di avere delle basi comuni.

Elenco dei concetti base

Funzione main

La "main function" è quella funzione da cui il computer parte ad eseguire il programma. In un eseguibile, ci può essere uno ed una sola funzione chiamata main. Normalmente main ha il seguente prototipo:

int main(const int argc, const char* args[]);

dove:

  • int è il tipo di ritorno del main e rappresenta il valore che verrà ritornato dall'esecuzione dell'eseguibile (se siete in bash questo valore verrà salvato in $?). Solitamente rappresenta l'exit status della vostra applicazione. 0 solitamente indica che il programma è terminato correttamente. Valori diversi da 0 indicano in generale un errore;
  • argc indica il numero di stringhe separate da almeno un " " nella riga di comando più 1;
  • args contiene le stringhe date in pasto al programma più il comando eseguito per lanciare il programma stesso;

Per esempio:

int main(int argc, const char* args[]) {
    printf("%d\n", argc);
    for (int i=0; i<argc; i++) {
        printf("%d %s\n", i, args[i]);
    }
    printf("DONE\n");
}

Eseguito tramite:

./a.out ciao bello 5

Darà il seguente output:

4
0 ./a.out
1 ciao
2 bello
3 5
DONE

Funzioni

In C una funzione è un insieme ordinato di istruzioni con degli input ed al più un output:

Ogni funzione ha un nome; ogni input e l'output possiedono un particolare tipo. Per esempio:

int sum(int a, int b) {
    return a + b;
}

la funzione ha come nome "sum", possiede 2 input, uno chiamato "a" di tipo int ed uno chiamato "b" di tipo int. "sum" ritorna un valore di tipo int.

E' possibile chiamare la funzione usando la seguente sintassi:

int five = 5;
int ten = 10;
int result = sum(five, ten); //result vale 15

Importante: Ogni parametri di input viene passato alla funzione per valore. Ciò significa che i valori delle variabili iniettate in "sum" ("five" e "ten") vengono copiate brutalmente nelle variabili temporanee "a" e "b":

  1. viene creata in memoria una variabile di tipo int chiamata "a";
  2. viene creata in memoria una variabile di tipo int chiamata "b";
  3. il contenuto della variabile "five" viene travasato nella variabile "a";
  4. il contenuto della variabile "ten" viene travasato nella variabile "b";
  5. viene eseguita la funzione "sum";

La modifica del valore delle variabili "a" e "b" quindi non ha effetti al di fuori della funzione. Quindi:

int fakeSum(int a, int b) {
    a = 100;
    return a + b;
}

"a" all'interno della funzione varrà 100 dopo l'assegnamento, ma "five" sarà sempre uguale a 5!

Interazione con l'utente

stampa su console

Per stampare informazioni sullo schermo si utilizza la funzione printf:

printf("Hello world!\n"); //stampa la stringa "Hello world!", quindi va a capo

printf è una variadic function, quindi accetta un numero indefinito di input (basta che il numero di input sia compatibile con il numero di "%" presenti nella prima stringa). Per esempio:

printf("Hello %s %s! The sum of %d and %d is %d!\n", "Marco", "Rossi", 2, 4, sum(2, 4));

Lettura da console

Molti potrebbero voler usare scanf, ma essa ha una potenziale pericolosità quando si effettuano conversioni stringa-intero automatiche. fgets è un'alternativa molto più sicura:

#include <stdio.h>
#include <stdlib.h>
#define BUFFERSIZE 100

int main(const int argc, const char* args[]) {
    char buffer[BUFFERSIZE];
    int yourAge;
    int yourWeight;
    if (fgets(buffer, BUFFERSIZE , stdin) == NULL) {
         fprintf(stderr, "Can't read from command line!\n");
    }
    //now buffer contains the line requested. If you want you can use sscanf to safely perform conversions (if you need them)
    if (sscanf(buffer, "%d %d", &yourAge, &yourWeight) != 2) { //2 perché gli argomenti da convertire sono 2
        fprintf(stderr, "Error while trying to convert age and weight from your line!");
    }

    printf("age: %d weight: %d\n", yourAge, yourWeight);
    
}

Puoi provare te stesso: grazie a questo meccanismo inserendo "12 13" tutto funziona perfettamente, mentre cose del tipo "1213" o "qw12 13" vengono identificate subito ed il buffer non viene sporcato!

Tipi

Abbiamo parlato di variabili. Ogni variabile ha un tipo. Un tipo permette al compilatore di capire quali sono i possibili valori che una variabile può avere. Per esempio una variabile int non può essere assegnata a "5.6" (in questo modo si possono intercettare molti più errori!). Una variabile può essere di uno ed un solo tipo. Una informazione importantissima che il tipo invia è che dal tipo (e solo dal tipo) è possibile ricavare quanti byte quella variabile occupa in memoria (puoi scoprirlo attraverso sizeof oppure l'header <stdint.h>): tieni a mente questo fatto per capire meglio i puntatori. I tipi possono essere primitivi o strutturati. Per esempio:

tipo tipologia lunghezza (B)* esempio
char primitivo 1 char a = 3;
short primitivo 2 short a = 5;
int primitivo 4 int a = 5;
long primitivo 8 long a = 6;
unsigned char primitivo 1 char a = 3;
unsigned short primitivo 2 short a = 5;
unsigned int primitivo 4 int a = 5;
unsigned long primitivo 8 long a = 6;
float primitivo 4 float a = 5.6;
double primitivo 8 double a = 5.6;
long double primitivo 16 double a = 5.6;
struct Foo{ int a; int b;} strutturato 4 + 4 struct Foo foo = {4,5};

Notare che C99 non specifica la dimensione dei tipi primitivi (non specifica nemmeno il numero di bit in un byte!). L'unica certezza è che char è un byte, e che char <= short <= int <= long.

I tipi strutturati sono struct, enum e union.

Variabili

In C è possibile:

  1. dichiarare;
  2. definire;
  3. inizializzare;

una variabile.

Dichiarazione

Dichiarare significa associare ad un nome (aka identificatore) un tipo ben preciso. Per esempio extern int a; al di fuori di una funzione rappresenta una dichiarazione: il compilatore C adesso sa che esiste una variabile di tipo intero chiamata "a" da qualche parte, ma non sa l'indirizzo in memoria di suddetta variabile!

E' possibile dichiarare quante volte si vuole una variabile. E' inoltre possibile dichiarare anche le funzioni (attraverso i classici prototipi):

int add(int a, int b);

Definizione

Definire una variabile significa associare ad un nome (aka identificatore) un tipo ben preciso ed allocare il congruo numero di byte in memoria per memorizzare la variabile. Per esempio int a; al di fuori di una funzione rappresenta una definizione: il compilatore C adesso sa che esiste una variabile di tipo intero chiamata "a" e ne conosce anche l'indirizzo in memoria, ad esempio "0xFF45FF34".

E' possibile definire una variabile una ed una sola volta. E' inoltre possibile definire anche le funzioni (attraverso prototipo + corpo della funzione):

int add(int a, int b) {
    return a + b;
}

Inizializzazione

Una variabile viene inizializzata quando viene definita e, nel contempo, gliene se assegna un valore ben preciso. Per esempio int a = 5 è un'inizializzazione. Proprio come una definizione, l'inizializzazione di una variabile può essere fatta una volta sola.

Modificatori di flusso

I modificatori di flusso ti permettono di modificare la sequenza delle istruzioni che il programma esegue.

If

La condizione è un'espressione booleana. Se la condizione è diversa da 0 allora il programma eseguirà solo le istruzioni in ALPHA, altrimenti eseguirà solo le istruzioni in BETA. Ricorda: in C lo 0 è "Falso" mentre ogni altro numero è "Vero".

if (condizione) {
    //ALPHA: codice da eseguire se la condizione è vera
} else {
    //BETA: codice da eseguire se la condizione è falsa
}

Puoi scrivere elseif in questo modo:

if (condizione1) {
    ...
} else if (condizione2) {
    ...
} else if (condizione3) {
    ...
} else {
    ...
}

Switch case

Il switch ... case ti permette di effettuare una scelta non tra 2 casistiche (come l'if) ma tra più scenari. C'è una variabile che rappresenta un controllo (che deve essere intera o enumerabile) e varie casistiche:

int a;
switch(a) {
case 0: {
    //do something when a == 0
    break;
}
case 1: {
    //do something when a == 1
    break;
}
default: {
    //do something when a is not compliant with anyother scenario (this section is optional)
}
}

I case rappresentano delle "etichette" (come quelle del goto). Perciò è necessario alla fine dei case mettere il break. Se non lo si fa, il programma passerà al case immediatamente sottostante.

While

Il ciclo while esegue una lista di istruzioni fino a che la condizione non diventa falsa (quindi 0):

while (condizione) {
    ...
}

Esiste anche il ciclo do while che la stessa identica cosa tranne che è assicurato che il ciclo venga eseguito almeno una volta:

do {
    ...
} while (condizione);

Quando la condizione è falsa (o 0) il computer esce dal ciclo. Il ciclo while dovrebbe essere quando non si conosce a priori il numero di volte in cui il body del ciclo viene eseguito.

For loop

Il for loop è sintatticamente definito in questo modo:

for (statements1; expression1; statements2) {
    ...
}

La semantica del for loop è la seguente:

{
    statements1;
    while (expression1) {
        ...
        statements2;
    }
}

Per esempio il seguente ciclo for:

for (int i=0; i<10; i++) {
    printf("i is %d\n", i);
}

è equivalente a:

{
    int i=0;
    while (i<10) {
        printf(i is %d\n", i);
        i++;
    }
}

Il for loop dovrebbe essere usato per rappresentare cicli in cui si conosce a priori il numero di volte in cui si deve eseguire il corpo del ciclo.

Puntatori

I puntatori possono essere considerati il cuore di C. Essi permettono di raggiungere una flessibilità incredibile. Siccome spesso sono un argomento piuttosto controverso ne diamo una trattazione più articolata.

Definizione

Per prima un puntatore è un tipo di variabile. Un puntatore contiene un indirizzo in memoria. Quindi per esempio valori ammissibili di puntatori possono essere:

  • "0xFF345E";
  • "0x000000";
  • "0x9C5ED2";

Siccome la memoria RAM indirizzabile da un sistema operativo dipende dal numero di bit con il quale opera, un puntatore ha dimensioni diverse a seconda dell'architettura del computer che stai usando. A volte esso è grande:

  • 4 byte, perché deve indirzzare fino a 2^32 byte di memoria RAM, ossia 4GB. Questo valore è tipico di architettura a 32 bit;
  • 8 byte, perché deve indirizzare fino a 2^64 byte di memoria RAM, ossia tantissimo!. Questo valore è tipico di architetture a 64 bit;

I puntatori materialmente si definiscono nel seguente modo:

tipo_di_concetto_puntato *nome_variabile_puntatore;

Per esempio:

int *a;

La precedente istruzione definisce una variabile chiamata "a" che contiene l'indirizzo in memoria di un elemento di tipo intero. Abbiamo parlato di tipo di un "elemento": ma che cos'è un elemento puntabile? Materialmente un elemento puntabile potrebbe essere:

  • variabili;
  • costanti;
  • funzioni;

In generale comunque ogni singola cella in memoria virtuale è potenzialmente puntabile da un puntatore. Spesso i puntatori sono rappresentati nel seguente modo:

pointer_drawing

In questo esempio, p punta ad una stringa.

Operatori

Un puntatore è un indirizzo in memoria. Ma gli indirizzi in memoria sono in fin dei conti numeri interi (seppure spesso per convenienza espressi in notazione esadecimale). Quindi in generale tutte le operazioni che possono essere applicate ai numeri interi possono essere applicate ai puntatori. In più, tuttavia, i puntatori definiscono 3 operazioni in più:

  1. Operatore di referenziazione: dato un elemento e, &e ne ritorna l'indirizzo in memoria. Per esempio:

     int add(int a, int b) {
         printf("pointer of a is %p\n", &a);
         return a + b;
     }
    

    Mostrerà a schermo valori come "0xFF34D5" o "0xFFFFF4".

  2. Operatori di dereferenziazione: data un puntatore p, *p ne ritorna il valore contenuto nella cella di memoria puntata da p. Questa operazione ha un'implicazione estremamente sottile: per avere il valore è necessario conoscere il tipo del contenuto nella di memoria. Mi spiego meglio: Consideriamo il seguente stato della memoria:

     | ... |0xFF01|0xFF02|0xFF03|0xFF04| ... |
     | ... | 0x41 | 0x42 | 0x43 | 0x00 | ... |
    

    Cosa succede se noi eseguiamo *p quando p = 0xFF01 ? La risposta dipende. Quando il compilatore vede *p, esso esegue 3 azioni:

    1. La prima cosa è andare all'indirizzo in memoria puntato, in questo caso "0xFF01";
    2. La seconda cosa è selezionare n byte partendo dalla cella in memoria precedentemente ottenuta (dove n è la grandezza del tipo) e selezionandole dalla sinistra alla destra;
    3. La terza è interpretare i byte ottenuti.

    Per esempio supponiamo che p sia stato definito come char *p. In questo caso il programma andrà nella cella di memoria "0xFF01" e considererà 1 byte (perché sizeof(char) è 1). Otterrà quindi il byte "0x41" che andrà ad interpretare come un char, ossia come 'A'.

    Ma cosa succede invece se p è stato definito come short* p? In questo caso il programma andrà sempre nella cella di memoria "0xFF01" ma stavolta considererà 2 byte (almeno sul mio computer sizeof(short) è 2). I byte ottenuti saranno quindi 0x41 0x42, che, convertiti in un numero, sarà 16706 (supponendo un'architettura little endian).

    Facciamo un terzo esempio, anche se ormai pernso che il concetto sia chiaro: supponiamo ora che p sia stato definito come bool_pair *p dove:

     typedef struct {
         bool a; //each bool takes 1 byte
         bool b;
     } bool_pair;
    

    In questo caso ancora una volta *p darà una struttura di tipo bool_pair in cui entrambi i valori sono true (questo perché 0 è "falso" mentre ogni altri valore, compreso 0x41 e 0x42 rappresenta il valore di verità "vero").

    I byte coinvolti sono sempre gli stessi, ma la rappresentazione è cambiata poiché il tipo dato puntato è cambiato.

  3. Operatore di accesso ->: quando siamo in una struct*, possiamo usare questo operatore come shortcut. I 2 codici sono equivalenti:

     typedef struct {
         int a;
         int b;
     } int_pair;
    
     int_pair *pair;
    
     //le seguenti operazioni sono identiche: entrambe accedono al valore del campo "a" del puntatore a struttura "pair"
     (*pair).a;
     pair->a;
    

Utilizzo dei puntatori

I precedenti paragrafi hanno descritto più o meno la maggior parte (se non tutta) la conoscenza di base per capire cosa sono i puntatori. Andiamo ora capire quali sono gli utilizzi più tipici.

Passaggio per referenza

Uno degli usi più comuni dei puntatori è quello per effettuare il passaggio per referenza di un parametro di una funzione. All'inizio abbiamo detto che quando una funzione viene eseguita, vengono copiati i valori all'interno dei parametri e delle copie diventano le variabili all'interno della funzione stessa. Tuttavia cosa succede se volessimo modificare il valore della vera variabile iniziale? In altre parole:

void increaseOf(int _a, int _b) {
    _a += _b;
}

int main(const int argc, const char* args[]) {
     int a = 5;
     increaseOf(a, 10);
     printf("We have increase the variable a! Now it is %d\n", a);
}

La funzione increaseOf non incrementa la vera variabile a, ma incrementa la copia della stessa (_a). Questo perché le funzioni passano i propri parametri per valore. Potremmo usare una variabile globale per risolvere questo problema, ma le variabili globali sono in genere da evitare. E' possibile risolvere il problema con un singolo puntatore:

void increaseOf(int *_a, int _b) {
    *_a = *_a + _b;
}

int main(const int argc, const char* args[]) {
     int a = 5;
     increaseOf(&a, 10);
     printf("We have increase the variable a! Now it is %d\n", a);
}

Perché funziona? Stavolta alla funzione noi non passiamo il valore della variabile a, ma passiamo il suo riferimento (il suo indirizzo, per esempio 0XFF54D2). Questo indirizzo viene poi copiato sempre per valore e inserito in una nuova variabile di tipo int* chiamata _a. Il contenuto di suddetta variabile, però, è ancora l'indirizzo di a, ossia "0xFF54D2". Riottenendo il valore contenuto in a e incrementando suddetto valore di _b ci permette di risolvere il problema.

parametri di out

Cosa succede se vogliamo che una funzione ritorni più di un valore di uscita? Per esempio consideriamo una funzione che, dati 3 numeri, calcola il massimo ed il minimo. Una funzione solitamente può ritornare un solo valore. Tramite i puntatori possiamo fare in modo di ritornare più valori.

void computeAnalysis(int a, int b, int c, int *min, int *max) {
    if ((a > b)&&(a > c)) {
        *max = a;
        *min = b < c ? b : c;
    } else if ((b > a)&&(b > c)) {
        *max = b;
        *min = a < c ? a : c;
    } else {
        *max = c;
        *min = a < b ? a : b;
    }
}

int main(const int argc, const char* args[]) {
    int min;
    int max;

    computeAnalysis(4, 2, 7, &min, &max);
    printf("min=%d max=%d\n", min, max);
}

Avremmo potuto anche creare una struttura e passare per riferimento la struttura stessa, ma a volte non si vuole creare una nuova struttura solo per gestire l'output di una singola funzione.

Layer di puntatori

Questo è un argomento complicato: i puntatori possono essere annidati tra di loro. Ossia volendo è possibile scrivere:

int *****p;

Questo è un puntatore che punta ad una cella di memoria di tipo int ****p, la quale contiene un valore che punta ad un'altra cella di memoria contenente un valore di tipo int ***p, che al suo interno contiene un valore di tipo int **p il quale punta ad un'altra cella di memoria che contiene un valore di tipo int *p la quale punta ad una cella di memoria contenente un valore intero. Spesso non è così complicato ma, almeno nella mia esperienza, mi è capitato di arrivare 3 puntatori innestati insieme (vedi avanti).

NULL pointer

Spesso è necessario dire che un puntatore non punti a nulla. La libreria stdlib.h contiene una macro chiamata NULL che è un redefinizione del valore 0. Usa sempre suddetta macro quando vuoi che un puntatore non punti ad alcunché. Stai però attento: non è possibile ottenere *p quando p = NULL: eseguendo suddetta operazione, otterrai il famigerato segmentation fault.

Puntatore a void

Esistono anche i puntatori a void, ossia puntatori definiti così:

void* p;

Un puntatore a void* è un puntatore che referenzia un qualcosa di non ben precisato. Puoi usarlo come un normalissimo puntatore perché lo spazio occupato in memoria (aka sizeof(p)) non è diversa da tutti gli altri puntatori. L'unica particolarità del puntatore a void è che per essere dereferenziato (*p) deve essere prima convertito in un puntatore con un tipo ben preciso: ha senso, dato che nessuna variabile può essere di tipo void:

int a = 5;
long b = 1000;
void* p = NULL;

//p può puntare a qualunque tipo
p = &a;
p = &b;
//devo però castare p ad un puntatore di un particolare tipo prima di dereferenziarlo
printf("p vale %p ma il contenuto è %l\n", p, *((long*)p));

Array

In C un array si dichiara nel seguente modo:

tipo variabile[numero di celle nell'array];

Per esempio il seguente codice alloca in memoria un array di 10 elementi chiamato "array" in cui ogni cella contiene un tipo "int". Il mio computer alloca 40 byte (4 * 10) per rappresentare l'intera struttura.

int a[10];

Per modificare una cella dell'array basta eseguire il codice:

a[5] = 3; //il numero contenuto nella cella 5 dell'array ora è 3

Per leggere una cella dell'array invece bisogna eseguire:

a[5]; //ritorna il numero contenuto nella cella 5

Materialmente però la variabile "a" è grande 4 byte. Perché? Perché in realtà la variabile "a" contiene soltanto l'indirizzo in memoria dove si trova la prima cella dell'array. In altre parole, se si definisce un array:

short a[4];
a[0] = 1;
a[1] = 3;
a[2] = 5;
a[3] = 7;

in memoria si ha:

    | ... |0xFF01|0xFF02|0xFF03|0xFF04| ... |
    | ... | 0x01 | 0x03 | 0x05 | 0x07 | ... |

dove ogni cella è uno short. Nel mio computer uno short è grande 1 byte, perciò ogni cella dell'array sarà grande 1 byte. l'array "a" ha 4 celle, quindi ci saranno in totale 4 byte allocati. La variabile "a" conterrà però solo il riferimento alla prima cella, ossia a[0]. Infatti stampando a si otterrà "0xFF01".

Un array può essere quindi visto come un puntatore!

Dualità array e puntatore

Un array quindi può essere facilmente convertito in un puntatore. La prova del fatto è che se vogliamo passare un array ad una funzione, possiamo farlo facilmente in questo modo:

void perform analysis(int size, int* a) {
    for (int i=0; i<size; i+) {
        printf("a[%d] = %d\n", i, a[i]); //è un puntatore, ma ci accediamo come se fosse un array
    }
}

int main(const int argc, const char* args[]) {
    int array[6] = {0, 1, 2, 3, 4, 5};
    performAnalysis(6, array); //non dobbiamo mettere "&", perché già l'array è un indirizzo!
}

Inoltre, è vero anche il viceversa: possiamo accedere ad un puntatore come se fosse un array: se il puntatore è stato definito come int* p; allora l'operazione di p[5] darà un valore di tipo int.

Creazione degli oggetti in memoria

In C uno sviluppatore ha 2 modi per allocare spazio all'interno della memoria per i suoi usi e consumi.

Il primo è di definire variabili di un certo tipo: ciò consente una allocazione automatica dello spazio che poi verrà automaticamente ripulito una volta che lo scope della variabile termina (per esempio se una variabile è locale in una funzione, lo scope della variabile finirà quando la funzione arriverà al comando return).

Il secondo è l'allocazione manuale di memoria: questa seconda opzione crea sì spazio che può essere usato dal programmatore, ma la sua gestione viene completamente lasciata allo sviluppatore stesso. A volte, però, è necessario utilizzarla.

Consideriamo per esempio il caso in cui vogliamo creare una lista di interi. Non possiamo usare un array perché l'array ha dimensioni fisse e noi non sappiamo quanti numeri l'utente inserirà. Ogni elemento ha 2 componenti: l'intero da memorizzare e un puntatore al prossima cella della lista. Per esempio:

typedef struct list_cell {
    int number;
    struct list_cell *next;
} list_cell;

list_in_memory

int main(const int argc, const char* args[]) {
    char buffer[100];
    int n;
    list_cell list;

    printf("Scrivi un numero\n");
    if (fgets(buffer, 100, stdin) != NULL) {
        if (sscanf(buffer,"%d", n) != 1) {
            //aggiungi "n" alla lista list...
        }
    } 
}

Dobbiamo per forza di cose creare noi in memoria spazio dove salvare i dati variabili dell'utente. Per allocare in memoria n byte, possiamo usare la funzione malloc:

list_cell* newCell = (list_cell*) malloc(sizeof(list_cell));
if (newCell == NULL) {
    fprintf(stderr, "Spazio su RAM esaurito!\n);
}

malloc alloca n byte. Siccome sizeof ti permette di ottenere il numero di byte usati da un tipo, malloc e sizeof vanno spesso a braccietto. malloc ritorna un void*, che quindi deve essere castato correttamente al tipo di puntatore richiesto. Quantitativamente parlando, malloc ritorna un puntatore che punta al primo dei sizeof(list_cell) byte che ha allocato in memoria. Se lo spazio in memoria non è sufficiente, malloc ritorna NULL: controlla quindi sempre che malloc ti abbia ritornato un valore ammissibile per evitare segmentation faults.

Una particolarità: malloc non da alcuna garanzia sul fatto che i byte che ti ha allocato siano settati tutti a 0: quindi ricordati di settarli propriamente te!

Una volta utilizzato lo spazio in memoria, devi esplicitamente eliminarlo tramite la funzione free:

free(newCell);

Il programma capirà automaticamente quanti byte deve eliminare, quindi non lo devi fare te. Ricorda, ad ogni malloc corrisponde solitamente una free: non soddisfare questa condizione genera quasi sempre memory leaks.

Oltre a malloc e free, esiste una terza funzione, calloc che fa un lavoro molto simile a malloc: Essa alloca sempre n byte di spazio, ma:

  1. Assicura che siano tutti settati a 0;
  2. lo fa in modalità pseudo-array. Tu gli dici di quanti byte è grande una cella dell'array ed il numero di celle e calloc alloca la memoria congrua.

Per esempio:

//crea un array di 10 elementi
int* array = calloc(sizeof(int), 10);

Stack Vs Heap

Normalmente C alloca spazio in 2 porzioni distinte di memoria, lo stack e l'heap.

  1. Lo stack è uno spazio di memoria gestito: sullo stack vengono poste tutte le costanti, le variabili locali di funzioni e le variabili globali create dal tuo programma. Il compilatore automaticamente genera il codice necessario per liberare correttamente lo spazio occupato da suddette variabili; qui non si deve usare free;
  2. L'Heap è uno spazio di memoria *non gestito: è qui che malloc e calloc vanno ad inserire le strutture dati che richiedi. In questo spazio è necessario l'uso di free per pulire la memoria;

Almeno sul mio computer, un trucco per capire se un indirizzo è un indirizzo di heap piuttosto che di stack è osservare il suo valore: se ha un valore con molte "F" (per esempio 0xFFFFF) allora è molto probabile che sia un indirizzo sullo stack: chiaramente quest'ultima regola è più una regola euristica che altro.

Curiosità

Operatore comma

In C esiste anche l'operatore ",". La sua definizione è molto semplice:

(statement1, statement2, statement3, statement4, ... statementN)

Vengono eseguiti tutti gli statement (quindi dall'1 fino all'N). Tuttavia viene ritornato solo il valore dell'ultimo statement (ossia "statementN"). Per esempio (esempio disponibile qui):

int a = 5;
int b = 3; 
printf("a=%d b=%d comma operator=%d \n", a, b, (a += 2, b, a + b));

genererà l'output:

a=7 b=3 comma operator=10

Quando è utile? Quando devi per forza eseguire una singola istruzione che ritorna un particolare valore ma il tuo codice necessita di eseguire 2 funzioni. Invece di creare una funzione ad hoc contenente entrambe le funzioni puoi usare l'operatore ",". E' consigliato, quando lo si utilizza, di inserirlo tra parentesi tonde siccome "," già viene usato come separatore di argomenti di funzione e macro.

Riferimenti

⚠️ **GitHub.com Fallback** ⚠️