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 here arr is an rvalue and we got a valid lvalue that can bind to it therefore it can decay to a pointer.
  • Doing std::cout << *arr << std::endl is also valid std::cout knows how to handle a rvalue !

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 !