The Life and Times of Pointers and References in C++

The Life and Times of Pointers and References in C++

Alright, References and Pointers! I spent a really great amount of hours trying to understand what these compound types are. What are they? The perfect way I understand them is, references are typically another name for referring to an object. References aren’t objects as other regular variables we define in a program, rather they are bound to already existing objects. Knowing this means that references can’t just exist on their own without having something they refer to. An object must exist, before a reference can be made. The consequence of this is that references must be initialized as soon as they are defined. Binding a reference to an object is as simple as defining a variable. We can define a regular variable as:

int score = 100;

Now to bind this variable to a reference, we just need to prefix the reference name with the reference declarator “&”. i.e

int &scoreRef = score;

This simple assignment operation is different from a regular assignment operation because, this doesn’t copy the value of score to scoreRef. Instead, it is binding the score object to scoreRef. Any arithmetic operations that can be performed on score can be equally done with scoreRef. If we try to fetch the value of scoreRef, we are actually obtaining the value of the score object itself. It is also important to note that if the operations performed on the scoreRef changes its value, the original object (score) which it is bound to equally changes as well. An example!

int lives = 55;
int someTotal = lives;
someTotal = someTotal + 17;
cout <<someTotal <<endl; //expected value here is 72
cout <<lives <<endl; //expected value here is 55

However, using the same example with reference:

int lives = 55;
int &someTotal = lives;
someTotal = someTotal + 17;
cout << someTotal <<endl; //expected value here is 72
cout << lives <<endl; // value here is also 72

When the lives variable was bound to someTotal, we added 17 to the initial value. This addition changed the value of someTotal to 72. Since someTotal is bound to lives, it changes the value of lives as well. Another way to look at it is, someTotal is the same as lives, only difference is that we have another name for it.

Important things to know about references:

  1. References must be initialized immediately.
  2. The data type of a reference must be the same as the data type of the object it is bound to. e.g: double points = 3.01245; int &pointsRef = points; //error! points should be an integer

  3. A reference must be bound to an object. This means that we can’t bind a reference to a literal. An example, we can’t do stuff like this;

int &scoreRef = 100; // error! cos 100 is a literal not an object.

Pointers

Pointers! Well, pointers are like references being that they also refer to an object… difference is that they refer to the memory address of the object they point to. Also, when a pointer is initialized to point to a particular object, they can be reassigned to point to another address of an object at any time throughout the lifecycle of the program.

Defining a pointer can be done by prefixing the pointer name with an asterisk, and since pointers point to memory addresses, we can obtain the memory address of an object by prefixing the object it points to with a “&”. For example:

int score = 100;
int *score = &score;

If we try to print the value of scoreRef, the result would be a series of strange alphanumeric characters, which is actually the address the object holds in memory. Right now, printing scoreRef yields a result of “003FFEA0”.

A pointer does not need to be initialized at the time it is defined. If just defined, the pointer is known to be a null-pointer. There are also several ways can intentionally define a null pointer as:

int *somePointer = 0;
int *somePointer1 = NULL;
int *somePointer2 = nullptr;

Quick note: In example a above, it would be dangerous to assume that literals such as 5,6,7,8… can be assigned to a pointer. This assumption is wrong, and such assignment would fail since a pointer should refer to an object. Only 0 works because, assigning 0 to a pointer is just a way of defining the pointer as a null pointer. It is also illegal to assign an int variable to a pointer, even if the variable is 0.

void pointers can hold the address of any object of any datatype. The type of the object at the address would be regarded as unknown. This however limits the kind of operations that can be performed on such pointer. You can only pass or return a void pointer, but you cannot use a void pointer to operate on the object it addresses. You can declare a void pointer as:

int score = 45;
void *somePointer = &score;

Doing arithmetic operations like this below won't work since the compiler literally has no idea of the type of the object and if the operation would be valid.

int score = 45;
void *somePointer = &score;

int x = *somePointer; //error! Dereferencing somePointer doesn't yield an int

Dereferencing somePointer only yields the object they point to, but we can’t perform mutable operations on them such as adding/subtracting a value from it since the compiler can’t know if the object is an integer and accepts arithmetic operations, or if the object is a string that could be concatenated, or just something else.

Important things to know about pointers:

  • If a pointer points to an object, the object it points to can be accessed by dereferencing the pointer. Dereferencing a pointer yields the object to which it points to. We dereference a pointer by prefixing the pointer object with an asterisk. An example:
int score = 70;
int *scorePointer = &score;

cout << scorePointer << endl; //prints out the address like “783FUEI0”
cout << *scorePointer << endl; //prints out the the value of score, which is 70;

If we assign a different value to the dereferenced pointer, it changes the value of the object to which the pointer points to. For example,

*scorePointer = 1400; // this assignment changes the original value of score to 1400;

Note that it is illegal to dereference a null pointer

  • Two pointers can be equal if they hold the same address. This means that two pointers can be compared with “==” and “!=”. Two pointers can have the same address if they’re both null pointers, they point to the same object, or if the two pointers point to an address that is one-past the same object.

Okay, that last statement though. I did a bit of research on what “one-past an object” means? Assume we have an array of objects, and a pointer points to the address of this array. In the stack of addresses, it is possible to have a pointer that points to the address of a location in memory that is just immediately next to the address of the array object. In the same program, it is also possible that this “unknown memory location” that is just immediately after the location of the array object has been assigned to another object. And this object could equally have a pointer that points to it. In this scenario, there’s a probability that the pointer of this object, and the pointer that is just after (one-past) the location of the array share the same memory address. I could only understand this weird stuff thanks to this stackoverflow answer.

  • References aren’t objects as explained earlier. Now since pointers point to the address locations of objects, we can’t have a pointer to a reference. But pointers are objects in itself, so we can have references to pointers. An example;
int level = 47; //some harmless variable;
int *levelPointer = &level; //a pointer that points to the address of level
int *&someReference = levelPointer; // someReference is now a reference to the pointer “levelPointer”.

Since references are just another way of representing an existing object, someReference is actually a pointer and dereferencing it yields the level object which has a value of 47.

  • We can assign a pointer to point to another object, even if the pointer already points to another existing object.
    int x = 150;
    int y = 100;
    int * somePointer = &x; // pointer
    cout << *somePointer <<endl; // dereferencing the pointer prints 150
    somePointer = &y; // valid assignment to another object
    cout << * somePointer <<endl; // dereferencing the pointer prints 100
    

So there it is! That's pretty much how I understand these two compound types.

Now, a bit of therapy from a community that gives me comfort😊

I pretty much knew Horizon: Zero Dawn would be a great game just from seeing some shots. Watched a bit of the gameplay and realized I wasn't wrong! This is one of my many favorites I've seen so far.