References and Pointers

References

A reference is essentially an alias to an existing variable. A variable declared by reference to another variable is essentially an alternative name to access the space allocated to the original variable. The & operator is used to declare variables by reference:

int x = 42;
int& y = x;

After writing the above, what do you think will be the output for the following code?

y = 0;
cout << x;
⚠️

Note that references have to be initialized at the time of declaration, otherwise you would get an error at the time of compilation. Also, references once declared cannot be reassigned, and they also cannot be "nested" (in other words, it is not possible to have a reference to a reference).

Passing values by reference in functions

Let's revist the example of swapping of two variables via function:

#include <iostream>
using namespace std;

void swap(int &a, int &b){
	int temp = a;
	a = b;
	b = temp;
	return;
}

int main(){

	int x = 10, y = 20;
	cout << x << " " << y;
  cout << endl;
	swap(x,y);
	cout << x << " " << y;

}

In the example above, what got passed was values of x and y, but the function drew them in as references, and updated the original values. In particular, when swap was called, what effectively happened was the following:

int &a = x;
int &b = y;

So a is essentially a reference to x, and b is a reference to y.

You can achieve the same effect by passing addresses and setting up the function to expect pointers instead of references, as we will see in the next section.

Pointers

For every datatype in C++, there is another datatype that stores a memory address of variables of a given type. How you declare a pointer, therefore, depends on the kind of variable that you want the address for.

Syntax: <datatype>* <name>;

int* x;
int *y;

Every variable stores its value someplace in the computer's memory. When you print a variable, you print the value that's stored in it. But if you want to know the address of the place where the variable is stored, then you need a pointer.

Since you don't explicitly assign the address yourself in the program, you need to use the built in operator & to recover the address assigned to the variable by the compiler.

#include <iostream>
using namespace std;

int main(){
    int x = 42;
    int *ptr = &x;
    cout << ptr << endl << &x;
}

What you should see is some gibberish, but consistent gibberish 😇

If you have an address at hand, then you can use the dereferencing pointer * to recover the value stored by the variable at the address.

#include <iostream>
using namespace std;

int main(){
    int x = 42;
    int *ptr = &x;
    cout << *ptr << endl << x;
}

🧐 Now you should see 42 printed twice.

Can you predict the output of the following code?

#include <iostream>
using namespace std;

int main(){

    int x = 42;
    int *ptr = &x;
    int **ptr_to_ptr = &ptr; 
    cout << "PTR: " << ptr << endl;
		cout << "ADDRESS OF PTR: " << ptr_to_ptr << endl;
    cout << "RECOVERING PTR VIA PTR_TO_PTR: " << *ptr_to_ptr << endl;
    cout << "STUFF AT PTR BY DOUBLE DEREFERENCING: " << **ptr_to_ptr << endl;
}

Note that ** is a pointer to a pointer type in the declarration.

Also, ** on the last line is dereferencing the pointer twice, hence recovering the original value of the variable x.

Note that all pointer data types, because they need to store (somewhat loooong) addresses, all have the same size irrespective of the type of the data that they are pointing to. Try it yourself:

#include <iostream>
using namespace std;
#define ll long long int;

int main(){
    int x = 42;
    int *ptr = &x;

    long long int y = 100042;
    long long int *lptr = &y;    

    char ch = 'a';
    char *chptr = &ch;

    bool b = true;
    bool *bptr = &b;

    cout << sizeof(ptr) << " " << sizeof(lptr) << " ";
    cout << sizeof(chptr) << " " << sizeof(bptr);
}

Pointer Arithmetic

  • Pointers can be incremented or decremented
  • You can add and subtract values from pointers

What happens when you increment pointers of difffrent types by one?

If you run the following code:

#include <iostream>
using namespace std;

int main(){
    int x = 42;
    int *ptr = &x;

    long long int y = 100042;
    long long int *lptr = &y;    

    float f = 0.999;
    float *fptr = &f;

    bool b = true;
    bool *bptr = &b;

    cout << ptr << endl << ptr+1 << endl << endl;
    cout << lptr << endl << lptr+1 << endl << endl;
    cout << fptr << endl << fptr+1 << endl << endl;
    cout << bptr << endl << bptr+1 << endl;
}

then you will see some output like this:

0x7ffee0ea56ac
0x7ffee0ea56b0

0x7ffee0ea5698
0x7ffee0ea56a0

0x7ffee0ea568c
0x7ffee0ea5690

0x7ffee0ea567f
0x7ffee0ea5680

Can you guess what's going on?

Pointers and Arrays

If you have the address of the first element of the array (also sometimes called the base address, which can be accessed using the name of the array), you can navigate the array by manipulating the base address. If you have an array arr with n elements, then arr is the base address pointer, and arr+(n-1) points to the last element of the array.

For example, if you run the following code:

#include <iostream>
using namespace std;

int main(){
    
    int arr[5] = {10,30,20,25,50};
    for(int i = 0; i < 5; i++){
        cout << i << " " << (arr+i) << "\t" << *(arr+i) << endl;
    }

    int n = 5;
    sort(arr,arr+n);

    cout << endl << endl;

    for(int i = 0; i < 5; i++){
        cout << i << " " << (arr+i) << "\t" << *(arr+i) << endl;
    }
}

you should see something like this (of course, with different addresses):

0 0x7ffeedc18690	10
1 0x7ffeedc18694	30
2 0x7ffeedc18698	20
3 0x7ffeedc1869c	25
4 0x7ffeedc186a0	50


0 0x7ffeedc18690	10
1 0x7ffeedc18694	20
2 0x7ffeedc18698	25
3 0x7ffeedc1869c	30
4 0x7ffeedc186a0	50

Passing values by reference in functions using pointers

Instead of passing references to variables, you can also pass pointers to variables in your functions and this will have the same effect as passing references: you just need to take care to ensure that your function's declaration is setup to handle incoming pointers and you dereference things properly.

See the example below and compare it with the previous implementation of swapping by reference that we had above.

#include <iostream>
using namespace std;

void swapbyptr(int *a, int *b){
    int temp = *a;
    *a = *b;
    *b = temp;
    return; 
}

int main(){

	int x = 10, y = 20;
	cout << x << " " << y;
  cout << endl;
  int *xptr = &x;
  int *yptr = &y;
  swapbyptr(xptr,yptr);
	cout << x << " " << y;
}

Types of pointers

  • Dangling pointers — a pointer that has been freed up and is pointing to an arbitrary location
  • Wild pointers — a pointer that has been declared but not initalized
  • Null pointers — pointers that explicitly points to nothing - can be declared using the syntax int *p = NULL;
  • Void pointers — a pointer that is pointing to an area in memory that does not have any specific type; to dereference void pointers you would need to typecast it. Void pointers can be declared using void *ptr.