Let's imagine that we are the original creators of
the programming language itself.
How could we solve the problem of collections from the previous lesson?
Well, let's go step by step. I'm here declaring a simple integer variable.
The program will know that a simple integer will need
four bytes of allocated space in memory.
And now, let's say that I want to figure out a way to
store a collection of four integers,
but I don't want to declare a new variable for each number.
The first step would be to allocate enough space.
So instead of 4 bytes, now we need 16 bytes in memory, 4 bytes for each integer.
We need to have some kind of a specific notation to
inform the compiler to do this.
Since I want to store 4 integers,
I could put a number 4 inside of the square brackets next to the identifier.
Great! Now we have enough allocated space to store the collection of
integers, but we are faced with another problem.
Since this whole collection is placed under one name,
one identifier, how are we going to refer to each one of these numbers
specifically? This is a memory management problem.
And you know what can help us with memory management?
Well,
pointers, of course. Instead of seeing this identifier as just the name of
the whole collection, let's say that the program should consider it as a
pointer to the first element in the collection.
Now that we've established this, what do you think will
happen when I try to dereference it?
The referenced collection identifier will give us the value of the first
element. And of course, now that we have the dereferenced value,
we can also set it to something else, if we wanted to.
Nothing new here. This is dereferencing 101.
So now we have a way to read and write to the first element of
the collection, but what about other elements?
Since these integers are stacked next to each other, we could
theoretically add four to this first address, and we would end up with
an address of the first byte of the second element.
But this is not how pointer arithmetic works.
C++ has a built‑in functionality to make this easier. Whenever
you try to add some number to the memory address or, in other
words, to the pointer itself,
compiler will do some pointer arithmetic on its own.
This is another reason why we need pointer data types.
For example,
when you add one to do the integer pointer, compiler will
know that this pointer points to an integer.
So instead of looking at the next byte in memory it will
use the size‑of operator in the background to see how big
is an integer on this system.
When it figures out that the size of an integer is 4,
it will multiply 4 with 1 and move to this memory address.
So if you had 2, this will actually look at the memory address, which is
8 bytes apart from the original address, and so on.
This might seem unnecessarily confusing, but the main point of this
behavior is so that we can easily work with collections of data. So,
we can use this number as an offset to get the address of other
elements. To keep this idea consistent, I will add an offset to the
first element, 2, but since it doesn't need an offset, I will add 0.
Now we have a way to get the memory address of each element in the
collection. To dereference these addresses and get the element itself, we
can just dereference this whole expression. But be careful to add these
parenthesis because if you don't include them,
this would dereference the first element of the collection,
and then add the number 1 to the value of that element, and
this is not what we intend to do.
Since this collection is an ordered series of elements with the same data type,
let's stop calling it a collection.
The official term for this structure in C and C++ is the array.
And if you try to work with arrays in this way by
using the dereference operator, it would work. You can test it yourself.
However, this is a really messy and complex way to access elements.
It leaves a lot of space for mistakes. To fix this, language
creators invented a better syntax for array management.
When working with arrays,
you will rarely have the need to get the memory address of each element.
What we are most interested in is usually the values of these elements.
So instead of dereferencing and using pointer arithmetic in this way,
let's put this offset inside of the square brackets.
This operator is telling the compiler that we want to get the dereferenced
element from the array at this offset from the first element.
So if I put 0 inside of the square brackets,
this would equal to this expression.
We are accessing the first element in the collection, and
the offset for the first element is 0.
This is why you will hear that arrays start with 0 instead of 1.
But what if for some reason we need memory addresses? How
would we get them from this new syntax?
Well, this new syntax works just like a regular variable name.
We can just use the address‑of operator to get the address of each element.
Both of these syntaxes are valid, but using square brackets
is much more intuitive. As you can see,
the name of the array is basically just a pointer, but it's not really a
pointer, and there are a few differences between a regular pointer and an
array identifier. Syntactic sugar provided by the array allows us to use
the address‑of operator on the array's name and still get the memory
address of the first element.
Also, you cannot reassign this array name to some other place in memory.
Once you declare an array, this pointer cannot be reassigned.
There is also a difference with using the size‑of operator,
but we will talk about this later.
Now you know why we need arrays and how they actually work. In the next lesson,
we will see how to use them in practice.