If each memory address can hold 1 byte (8 bits) of data
That’s not exactly right—it’s not that memory is partitioned into an array of boxes, each of which is 1 byte in size; it’s that 1 byte is the smallest addressable unit of memory. You can address memory in larger increments.
So if you have a pointer
char *p containing some address such as
0x12345670, that’s essentially just an offset into memory—it points to the start of a region that may comprise a single byte, or multiple bytes, such as an integer, array, or struct. (In fact it’s slightly more complicated, since what you see as a flat address space is actually virtual memory that’s mapped onto physical memory by the operating system kernel, but for the purposes of this explanation it doesn’t make a difference.)
A 32-bit integer with value
0xAABBCCDD at address
p simply occupies 4 bytes. These bytes may be arranged by the CPU in big-endian order, where the most significant bits are stored at the lowest address:
Or little-endian, where the least significant bits are stored at the lowest address:
A programming language like C abstracts over this somewhat to provide a convenient way to address objects of different sizes. Suppose there is some array of 32-bit integers
p is the address of the first element:
p = &a. In assembly, if you want to iterate over this array, you need to increment
p by 4 each time to move it to the next integer:
&a == p
&a == p + 4
&a == p + 8
&a == p + 12
&a == p + 16
In C, an expression like
p + 1 doesn’t just add a number of bytes to the value of
p, it adds multiples of the object size,
p were typed as
uint32_t *p, then
a would be at
p + 1,
p + 2, and so on. Under the hood,
p + n becomes something like
(char *)p + n * sizeof(*p).
how does the CPU know when to stop reading bits of the memory addresses?
A primitive type like an integer is always a fixed size. When you write
*pi += 42 to add
42 to the contents of the 32-bit integer referred to by
pi, that’s translated specifically to a 32-bit indirect-add instruction. A compound type like an array is just a series of values at addresses that are multiples of the object size—your program is responsible for only accessing within the bounds of the array. Higher-level languages insert automatic checks at runtime or compile-time to ensure memory safety by preventing invalid array accesses, among other things.
A dynamically allocated value like the result of
malloc is just a region of memory that the allocator has given you control over, which in turn it obtains from the operating system. You can cast it to whatever type you want, such as an array of custom structures, as long as you only access within the region that you’ve been granted by the allocator.