If each memory address can hold 1 byte (8 bits) of data
Non è esattamente giusto - non è che la memoria è suddivisa in una serie di scatole, ognuna delle quali ha una dimensione di 1 byte; è che 1 byte è la più piccola unità di memoria indirizzabile . È possibile indirizzare la memoria con incrementi maggiori.
Quindi, se hai un puntatore char *p contenente un indirizzo come 0x12345670 , questo è essenzialmente solo un offset in memoria: punta a l'inizio di una regione che può comprendere un singolo byte o più byte, come un numero intero, un array o una struttura. (In effetti è leggermente più complicato, dal momento che ciò che vedete come uno spazio di indirizzamento piatto è in realtà memoria virtuale che è mappato sulla memoria fisica dal kernel del sistema operativo, ma ai fini di questa spiegazione non lo fa fare la differenza.)
Un intero a 32 bit con valore 0xAABBCCDD all'indirizzo p occupa semplicemente 4 byte. Questi byte possono essere disposti dalla CPU nell'ordine big-endian , dove i più bit significativi sono memorizzati all'indirizzo più basso:
0x1234566F …
0x12345670 0xAA
0x12345671 0xBB
0x12345672 0xCC
0x12345673 0xDD
0x12345674 …
O little-endian , dove i bit significativi minimi sono memorizzati all'indirizzo più basso:
0x1234566F …
0x12345670 0xDD
0x12345671 0xCC
0x12345672 0xBB
0x12345673 0xAA
0x12345674 …
Un linguaggio di programmazione come C riassume in qualche modo questo per fornire un modo conveniente per indirizzare oggetti di dimensioni diverse. Supponiamo che ci sia una serie di interi a 32 bit a , e p è l'indirizzo del primo elemento: p = &a[0] . In assembly, se desideri eseguire iterazioni su questo array, devi incrementare p per 4 ogni volta per spostarlo al numero intero successivo:
&a[0] == p
&a[1] == p + 4
&a[2] == p + 8
&a[3] == p + 12
&a[4] == p + 16
…
In C, un'espressione come p + 1 non aggiunge solo un numero di byte al valore di p , ma aggiunge multipli della dimensione dell'oggetto, sizeof(*p) -so se p è stato digitato come uint32_t *p , quindi a[1] sarebbe a p + 1 , a[2] a p + 2 e così via. Sotto il cofano, p + n diventa qualcosa come (char *)p + n * sizeof(*p) .
how does the CPU know when to stop reading bits of the memory addresses?
Un tipo primitivo come un intero è sempre una dimensione fissa. Quando scrivi *pi += 42 per aggiungere 42 al contenuto del numero intero a 32 bit a cui fa riferimento pi , questo viene tradotto specificamente in un'istruzione di aggiunta indiretta a 32 bit. Un tipo composto come un array è solo una serie di valori agli indirizzi che sono multipli della dimensione dell'oggetto: il tuo programma è responsabile dell'accesso solo all'interno dei limiti dell'array. Le lingue di livello superiore inseriscono controlli automatici in fase di runtime o in fase di compilazione per garantire sicurezza della memoria impedendo, tra le altre cose, accessi non validi all'array.
Un valore allocato dinamicamente come il risultato di malloc è solo una regione di memoria che l'allocatore ti ha dato il controllo, che a sua volta ottiene dal sistema operativo. Puoi lanciarlo su qualunque tipo tu voglia, come una serie di strutture personalizzate, purché tu acceda solo all'interno della regione che ti è stata concessa dall'allocatore.