Pointer & Reference

Pointer

Definition

Pointers are the variables that store the memory addresses of other variables.

  • You can declare a pointer’s type by appending * to the pointed-to type.

    int *p; // p is a pointer to an int
  • Operator & is used to get the address of a variable(fundamental or user-defined).

    int num = 30;
    // the type of p is int *
    int *p = # // p stores the address of num
  • Operator * is used to access the value stored in the pointer.

    *p = 40; // assign 40 to the variable pointed to by p
    cout << num << endl; // print 40

Declaration of Pointer

When declaring a pointer, you need to specify the type of the pointer, which is the type of the variable it points to.

  • It’s a good practice to initialize a pointer to nullptr to indicate that it currently does not point to any valid memory address.
  • Sometimes you may see people use NULL instead of nullptr. This is also valid, but nullptr is preferred in modern C++.
int num = 30;
// declare pointer
int *p1 = nullptr, *p2 = nullptr;
p1 = &num; // assign address of num to p1
p2 = p1; // assign address stored in p1 to p2

*p1 = 40; // assign 40 to the variable pointed to by p1
*p2 = 50; // assign 50 to the variable pointed to by p2

How pointer works

Declaration

int num = 30;
// declare pointer
int *p1 = nullptr, *p2 = nullptr;
p1 = &num;
p2 = &num;
  • Both p1 and p2 point to the same memory address.1 2
  • p1 and p2 are both pointers to int.
  • & is the address-of operator. &num returns the address of num.

Dereference operator *

int num = 30;
// declare pointer
int *p1 = &num;
int *p2 = p1;

*p1 = 40; // now *p2 is also 40
  • You can directly assign the address of a variable to a pointer.
  • * is dereference operator.
    • *p accesses the value stored in the memory location p points to.
  • *p help you modify the value stored in the memory location p points to.

Pointer of pointer

int num = 30;
int *p = &num;
int **pp = &p;
  • Pointer are variables that store the address of another variable.
  • A pointer of pointer is a variable that stores the address of another pointer.

Pointer of struct

We can use pointer to point to a struct.

struct Student {
    char name[4];
    int born;
    bool male;
};

Student stu = {"Tom", 2000, true};
Student *pStu = &stu;

Here, pStu is a pointer to a Student struct. 3

There are two ways to access the members of a struct pointed to by a pointer.

  1. Use *pStu to get the struct pointed to by pStu, then use . dot operator to access the members of the struct.
    • should use brackets () to group *pStu first to get the struct before using the dot operator ..
(*pStu).name = "Amy";
(*pStu).born = 2001;
(*pStu).male = false;
  1. Use -> arrow operator to access the members of the struct directly.
pStu->name = "Amy";
pStu->born = 2001;
pStu->male = false;

const pointer

A const pointer is a pointer that cannot be changed after it is initialized.

int num = 30;
int *const p = &num;

int num2 = 40;
p = &num2; // error: cannot assign a new address to a const pointer
*p = 50; // valid: change the value stored in the memory location p points to
  • p stores the address of num.
  • p cannot be changed after it is initialized.
    • i.e. you cannot assign a new address to p.
  • *p can be changed.
    • i.e. you can change the value stored in the memory location p points to.

Pointer to const

int num = 1;
const int *p = &num;

int num2 = 2;
p = &num2; // valid: change the address stored in p
*p = 3; // error: cannot assign a new value to a const int
  • p stores the address of num.
  • *p is a const int.
    • Doesn’t mean the value stored in the memory location p points to is a constant.
    • Just mean you cannot change the value stored in the memory location p points to through *p.
  • p itself can be changed.
    • i.e. you can assign a new address to p.

const pointer to const

You cannot change the address stored in p, nor the value stored in the memory location p points to.

int num = 1;
const int *const p = &num;

int num2 = 2;
p = &num2; // error: cannot assign a new address to a const pointer
*p = 3; // error: cannot assign a new value to a const int

Rule of thumb

Read from right to left, * divides the type into two parts.

  1. After * is the type of the pointer
  2. Before * is the type of the variable the pointer points to

Examples:

  • int *p means p is a pointer to an int.
  • const int *p means p is a pointer to a const int.
  • int *const p means p is a const pointer to an int.
  • const int *const p means p is a const pointer to a const int.

Pointers and Arrays

Array name as pointer

An array name acts like a pointer to the first element of the array.

int arr[] = {1, 2, 3, 4};
int *p1 = arr;
int *p2 = &arr[0];
cout << arr << endl; // print the address stored in arr
cout << p1 << endl; // print the address stored in p1
cout << p2 << endl; // print the address stored in p2
  • arr, p1, and p2 all store the address of the first element of the array.
  • arr

Difference between pointer and array

int *p = nullptr;
int arr[] = {1, 2, 3, 4};

cout << p << endl; // print the address stored in p
cout << arr << endl; // print the address stored in arr
cout << sizeof(arr) << endl; // print the size of arr
cout << sizeof(p) << endl; // print the size of the pointer p
  • arr is a constant pointer to the first element of the array.
    • You cannot assign a new address to arr.
    • You can assign a new value to the elements of the array.
  • sizeof(arr) returns the size of the array.
  • sizeof(p) returns the size of the pointer p.

Array as function parameter

When you pass an array to a function, the array name is decayed to a pointer to the first element of the array.

void printArray(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        cout << arr[i] << " ";
    }
}

int arr[] = {1, 2, 3, 4};
printArray(arr, 4);
  • arr is decayed to a pointer to the first element of the array.
  • You must pass the size of the array to the function.

Pointer arithmetic

Pointer can be added or subtracted by an integer.

  • The compiler figures out the new address by adding the integer to the pointer and then multiplying the integer by the size of the type the pointer points to.
    • i.e. new address = old address + integer * (size of the type)
int arr[] = {1, 2, 3, 4};
int *p = arr;

cout << *(p + 1) << endl; // print 2
cout << *(p + 2) << endl; // print 3
cout << *(arr + 2) << endl; // print 3

An example with struct:

struct Student {
    char name[4];
    int born;
    bool male;
};

Student stu[] = {{"Tom", 2000, true}, {"Amy", 2001, false}, {"Ted", 2002, true}};
Student *p = stu;

cout << (stu + 1)->name << endl; // print Amy
cout << (p + 1)->name << endl; // print Amy
cout << (*(stu + 1)).name << endl; // print Amy
cout << (*(p + 1)).name << endl; // print Amy
  1. (stu + 1) gets the address of the second element of the array.
  2. *(stu + 1) gets the second element of the array.
  3. (*(stu + 1)).name gets the value stored in the name field of the second element of the array.

[] operator

The [] operator is equivalent to pointer arithmetic.

We use arr and pointer arithmetic to access the elements of the array.

int arr[] = {1, 2, 3, 4};
int *p = arr;

cout << arr[2] << endl; // print 3
cout << *(arr + 2) << endl; // print 3
cout << p[2] << endl; // print 3
cout << *(p + 2) << endl; // print 3

Here, arr[2] is equivalent to *(arr + 2), [] can be applied to both array and pointer.

Out of bounds

Both pointer arithmetic and [] operator don’t do boundary checking.

int arr[] = {1, 2, 3, 4};
cout << arr[10] << endl; // out of bounds
cout << *(arr + 10) << endl; // out of bounds

Sometimes programmers perform pointer arithmetic or use the [] operator on a pointer to a fundamental type. This practice is dangerous and should be avoided.

int num = 1;
int *p = &num;
cout << p[-1] << endl; // out of bounds
cout << *(p + 1) << endl; // out of bounds

Reference

Definition

Refrences are safer, more convenient versions of pointers.

  • A reference is an alias for a variable. (Kinds of like a pointer, but you don’t need to use * to access the value.)
  • A reference must be initialized when declared.
  • Once a reference is initialized to a variable, it cannot be reseated to refer to another variable.

To declare a reference, use & appended to the type name.

int num = 1;
int &ref = num; // ref is a reference to num

Background

If you have a huge struct and you want to pass it to a function,

struct Student {
    char name[20];
    int born;
    char address[100];
};

void print_student(const Student stu) {
    cout << "Name: " << stu.name << endl;
    cout << "Born: " << stu.born << endl;
    cout << "Address: " << stu.address << endl;
}
  • const is used to ensure that the struct is not modified.
  • The whole struct is copied into the function, which is not efficient.

The pointer version

void print_student(const Student *pStu) {
    cout << "Name: " << pStu->name << endl;
    cout << "Born: " << pStu->born << endl;
    cout << "Address: " << pStu->address << endl;
}
  • Good: Only pass the pointer, not the whole struct.
  • const also ensures the stuct is not modified.

Two issues:

  1. The user can pass a nullptr to the function.
  2. The function logic is simple, sometimes you assign the pointer to sth else by mistake.

Updated pointer version

void print_student(const Student *const pStu) {
    if (pStu == nullptr) {
        cout << "Student is nullptr" << endl;
        return;
    }
    cout << "Name: " << pStu->name << endl;
    cout << "Born: " << pStu->born << endl;
    cout << "Address: " << pStu->address << endl;
}
  • Two const ensures the pointer is not nullptr and the pointer is not reassigned.
  • There’s a check if the pStu is nullptr to avoid runtime error.

The reference version (Pass by reference)

void print_student(const Student &stu) {
    cout << "Name: " << stu.name << endl;
    cout << "Born: " << stu.born << endl;
    cout << "Address: " << stu.address << endl;
}
  • Pass by reference also saves the copy of the struct.
  • Reference must be initialized when declared, so no nullptr check is needed.
  • stu is the alias of the variable passed to the function, const ensures the variable is not modified. 4

Another example

Swap two numbers using reference and pointer.

Reference version:

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

int x = 1, y = 2;
swap(x, y);
cout << x << " " << y << endl; // print 2 1

Pointer version:

void swap(int *a, int *b) {
    if (a == nullptr || b == nullptr) {
        cout << "Invalid input" << endl;
        return;
    }
    int temp = *a;
    *a = *b;
    *b = temp;
}

int x = 1, y = 2;
swap(&x, &y);
cout << x << " " << y << endl;

Dynamical array

Stack and heap

5

Stack allocation

A stack allocation is a memory allocation method that is used for local variables and function parameters.

  • The size of memory allocated on the stack is fixed at compile time.
  • When the function returns, the memory is automatically freed.
  • Fast allocation and deallocation compared to heap allocation.
  • Limited by the stack size.

Heap allocation

A heap allocation is a memory allocation method that is used for dynamic memory allocation.

  • Heap memory is accessible or exists until it is explicitly freed.
  • There’s no automatic de-allocation, you need to manually free the memory when you are done using it.
  • Slower than stack allocation.
  • The size of the Heap-memory is quite larger as compared to the Stack-memory.

Why use heap allocation?

  • Dynamic size: The size of the array is determined at runtime.
  • Large size: The size of the array can be large.
  • Extended lifetime: The array can be accessed after the function returns.
  • Manual management: Heap allocation gives programmers more control over memory usage

Operator new

  • new is used to allocate memory on the heap.
  • delete is used to free the memory on the heap.
// allocate an int, do nothing
int *p = new int();

// allocate an int, initialize to 0
int *p2 = new int();
int *p3 = new int{}; // C++11

// allocate an int, initialize to 5
int *p4 = new int(5);
int *p5 = new int{5}; // C++11

// allocate a struct
Student *pStu = new Student();
Student *pStu2 = new Student {'Tom', 2000, true}; // C++11

Operator delete

delete is used to free the memory which is allocated by new.

To prevent memory leak, you need to free the memory allocated by new when you are done using it.

// free the memory allocated by new
delete p;
delete p2;
delete p3;
delete p4;
delete p5;
delete pStu;

Operator new[]

  • new[] is used to allocate memory on the heap for an array.
  • delete[] is used to free the memory on the heap for an array.
int * pa1 = new int[3];

// allocate and initialize to 0
int *pa2 = new int[3]();
int *pa3 = new int[3]{}; // C++11

// allocate 16 int, the first 4 are initialized to 1, 2, 3, 4
// rest are initialized to 0
int *pa4 = new int[16]{1, 2, 3, 4};

// allocate memory for 16 Student structs
Student *paStu = new Student[16];
Student *paStu2 = new Student[16]{{"Tom", 2000, true}, {"Amy", 2001, false}}; // C++11

Operator delete[]

delete[] is used to free the memory on the heap for an array.

delete[] pa1;
delete[] pa2;
delete[] pa3;
delete[] pa4;
delete[] paStu;
delete[] paStu2;

Allocate multidimensional array

When you allocate a 2-d array on the heap, you need to delete it in a nested way.

int **arr = new int*[3];

for (int i = 0; i < 3; i++) {
    arr[i] = new int[3];
}

for (int i = 0; i < 3; i++) {
    delete[] arr[i];
}
delete[] arr;
  • arr is a pointer to an array of “pointers to int”.
  • arr[i] is a pointer to an array of int.
  • You need to delete the inner arrays first before deleting the outer array.

Footnotes

  1. Note that p1 and p2 are also variables, they are also stored in memory. Suppose we have them on a 32-bit machine, the size of p1 and p2 is 4 bytes.↩︎

  2. The addresses in the figure are not the actual addresses in your computer. They are just for illustration.↩︎

  3. The grey box represents paddings to align the struct to the memory address.↩︎

  4. When reviewing the code, you should also take a look at how to call the functions.↩︎

  5. Image from hyperskill.org↩︎