Pointers are easy ? Or are they ?
Pointers are pretty easy to use and understand unless we fall in deep dereferencing and more complex topics like array of pointers and multiple dereferences and incrementing or decrementing them.
Basic Pointer use:
int a = 3;
int* ptr = &a;
std::cout << a << std::endl; //prints value of a
std::cout << &a << std::endl; //prints address of a
std::cout << ptr << std::endl; //also prints address of a
std::cout << *ptr << std::endl; //prints value of a
Now lets go a little deep using pointer to a pointer.
int a = 3;
int* ptr = &a;
int** itr = &ptr;
std::cout << itr << std::endl; //prints address of ptr
std::cout << &ptr << std::endl; //also prints address of ptr
std::cout << *itr << std::endl; //prints address of a since value at itr is ptr which is address of a
std::cout << **itr << std::endl; //prints value of a, dereferencing once gives us the address of a i.e. &a and further dereferencing that we get the value of a
Now lets go a little more deep and try to understand pointer decay in arrays.
Lets say we got an array int arr[] = { 3, 7 }
, now if we were to say int* ptr = &arr[0]
, this will make ptr
store the address of arr[0]
, but…if we were to pass arr
to a function that expects a int*
it will decay into a pointer, lets see:
void func(int* arr){
//arr decays to a pointer pointing to the first element in the array
std::cout << *arr << std::endl;
}
int main(){
int arr[] = { 3, 7 };
func(arr);
}
This is called a decay to a pointer !
Understanding arrays a bit more deeply
When we say int arr[] = { 3, 7 }
, Its an array containing 2 integers ! Not A Pointer !
It is a variable of type int[2]
! Yes It is an special variable of type array containing 2 integers means it takes up 8 bytes
in memory !
The decay only happens in specific situations like if passed to a function or doing pointer arithmetic !
int
is different from int[]
!
An array is always of type type[N]
where N
is the number of elements in the array.
The main concept is that its all down to memory.
So, understanding that lets see another code snippet:
int arr[] = { 3, 7 };
std::cout << &arr[0] << std::endl; //prints the address of value at index 0
std::cout << &arr << std::endl; //also prints the address of value at index 0
// BUT...
int* ptr = &arr; //This is wrong ! ❌ Results in a compile error
int* ptr_2 = &arr[0] //This is correct ! ✅ Compiles without any issue
Lets see what’s happening here,
ptr
expects address of a int
but rather got address of an array, type int[2]
. arr
is an array and does not decay to a pointer here ! Meanwhile arr[0]
is a valid int
and passing its address to a int
pointer is totally legal !
❗ BUT WAIT….
Why does &arr[0]
and &arr
print the same address even if their types are NOT the same ?
Because &arr
gives the address to the whole block of array not just a singular int
! &arr
will result in type (*)[N] //pointer to an array of N elements
. If we were to do pointer arithmetic and add 1
to arr
it will literally jump N
bytes (8 in this case) ahead in memory, unlike &arr[0]
which will jump just 4 bytes if incremented by 1.
We can verify this by the following code snippet:
int arr[] = { 3, 7 };
std::cout << &arr[0] << std::endl; //prints the address of value at index 0
std::cout << &arr << std::endl; //also prints the address of value at index 0
int (*ptr)[2] = &arr; //declaring a pointer to an array of 2 elements
int* val = &arr[1]; //normal int pointer poiniting to index 1 element in the array
std::cout << ++ptr << std::endl; //prints the address of the next byte after end of array. Jumped by N bytes(8 bytes in this case).
std::cout << ++val << std::endl; //same prints the address of the next byte after the end of the array. Jumped by exactly 4 bytes
Just a note: Doing &arr
or similar prevents decay to a pointer since we are explicitly taking out the address of arr
. But doing this will result in a decay though:
int arr[] = { 3, 7 };
int* ptr = arr; //decays to pointer to the first element i.e. arr[0]
Alright, Lets move on to a little more complex topic, or pitfall ?
Lets look at this snippet:
int a = 1;
int b = 2;
int* arr[] = { &a, &b }; //array of pointers !
int** pp = arr; //pp is a pointer to a pointer pointing to arr[0];
++**pp; //dereference pp to get VALUE of a i.e. 1 and increment it to 2;
pp++; //increment pp by 1 to point to the next index
++**pp; //dereference pp to get teh VALUE of b i.e. 2 and increment it to 3;
std::cout << a << " " << b << std::endl; //print values of a which is 2 and b which is 3.
Now this is all good not such a pitfall, but a question asked by my cousin, why not do ++*p
instead of pp++
? Wont it cause pp
to move to the next element in the array anyway ?
GOOD QUESTION !
Lets see what’s happening !
pp
points to arr[0]
and doing pp++
makes it point to the next element i.e. arr[1]
, all good but lets see what ++*p
does. It says dereference pp
once and increment it by 1.
pp
= `&arr[0].*pp
=arr[0]
=&a
.**pp
=*(arr[0])
=*(&a)
=a
=1
.
All good so we now what *pp
is, its &a
now lets say the address of a
is 0x100
and that’s where pp
point to, now doing ++*p
will increment the pointer by 4 bytes since a
is an int
, now pp
points to 0x104
which is a garbage address ! Therefore, doing **pp
now will result in printing of a garbage value !
But Why is b
not next to a
? Isn’t it stored in an array and arrays are contiguous blocks of memory ? (This is actually my question).
The Answer: Yes, Arrays store in contiguous blocks of memory but a
and b
themselves are not stored rather their addresses, means their address stored as values are contiguous. See first 2 lines a
and b
are initialized independently they can be anywhere in the stack not necessarily next to each other !
Lets see one more topic…
Look at this code snippet:
int arr[] = { 3, 7 };
int* ptr = arr; //decays to pointer and points to arr[0]
ptr += 1; //will increment ptr to point to arr[1]
arr += 1 //Error ❌ Will not compile
Why so ?
Because arr
is NOT a pointer, its an ARRAY ! U cannot do arr = arr + 1
. U can do arr[0 + 1]
but not the earlier ! Why ? Because decay to a pointer only happens if its an rvalue
!
int* ptr = arr
herearr
is anrvalue
and we got a validlvalue
that can bind to it therefore it can decay to a pointer.- Doing
std::cout << *arr << std::endl
is also validstd::cout
knows how to handle arvalue
!
This is the very reason ! arr = arr + 1
cannot happen because the lvalue
is an array and NOT a pointer !
Well here is something from ChatGPT :
template<typename T, std::size_t N> void foo(T (&arr)[N])
That looks cursed !
Lets see what’s going On :
A template with a type T
and N
with size std::size_t
(Don’t exactly know the gist of this size_t
).
A function prototype accepting an array of type T
and passed as reference therefore the whole array will be passed without decaying into a pointer. Doing a sizeof()
will return N
and not just the size of the pointer !