Pointers vs References

beginner c++11 memory

Pointers and references look very similar to each other on the surface and in practice will produce identical compiled code and performance in program. So then why does C++ have both pointers and references and why not just pick one to always use? There’s a few differences between the two:

Different Syntax

References were designed to simplify the syntax of certain operations when compared to pointers. This is especially obvious when looking at the multiplication of two variables because pointer dereference and multiplication share the same symobl, the asterisk(*). Here’s an example:

#include <iostream>

int multiply_ref(const int& a, const int& b) {
  return a * b;
}

int multiply_ptr(const int* const a, const int* const b) {
  return *a * *b;
}

int main() {
  int a = 2;
  int b = 3;

  std::cout << multiply_ref(a, b) << "\n";
  std::cout << multiply_ptr(&a, &b) << "\n";
}
6
6

Looking at the above code you may prefer the look of the function that accepts “constant int references” than the one that accepts “constant pointers to constant int”.

Pointers can Repoint

Pointers are variables that store the address of what they are pointing to. References are “aliases” of other variables that act as if they were that variable. Because of this difference you can change what a pointer points to via assignment but you can’t change what a reference aliases via assignment. It will just change the referenced variable. Here’s an example:

#include <iostream>

int main() {
  int a = 2;
  int b = 3;
  int c = 4;
  int d = 5;

  int* int_ptr = &a;
  int& int_ref = b;

  std::cout << "a: " << a << "\n";
  std::cout << "b: " << b << "\n";
  std::cout << "c: " << c << "\n";
  std::cout << "d: " << d << "\n";
  std::cout << "int_ptr: " << int_ptr << "\n";
  std::cout << "*int_ptr: " << *int_ptr << "\n";
  std::cout << "int_ref: " << int_ref << "\n";

  int_ptr = &c;
  int_ref = d;

  std::cout << "a: " << a << "\n";
  std::cout << "b: " << b << "\n";
  std::cout << "c: " << c << "\n";
  std::cout << "d: " << d << "\n";
  std::cout << "int_ptr: " << int_ptr << "\n";
  std::cout << "*int_ptr: " << *int_ptr << "\n";
  std::cout << "int_ref: " << int_ref << "\n";
}
a: 2
b: 3
c: 4
d: 5
int_ptr: 0x7ffee84736ac
*int_ptr: 2
int_ref: 3
a: 2
b: 5
c: 4
d: 5
int_ptr: 0x7ffee84736a4
*int_ptr: 4
int_ref: 5

Pointer Math

Because math with references is just math with the variables they alias adding or subtracting with a reference is the same as math with the underlying variable. However, because pointers can repoint, addition or subtraction with a pointer changes what the pointer points to by the size of what is being pointed to. This means that if you have objects that are contiguous, or next to each other, in memory (such as is guaranteed with std::array and std::vector) you can move through those elements with a single pointer and pointer math. This is called iteration and is what for loops do for you under the hood. Here’s an example:

#include <iostream>
#include <vector>

int main() {
  std::vector<int> vec{10, 9, 8};
  if (vec.size() < 3) {
    std::cout << "We're assuming vec has at least 3 elements\n";
    return 1;
  }

  // data() returns the first element or nullptr if there's nothing
  int* vec_data = vec.data();

  std::cout << "Pointer\n";
  std::cout << *vec_data << "\n";
  std::cout << *(vec_data + 1) << "\n";
  std::cout << *(vec_data + 2) << "\n";

  // front() returns a reference to the first element
  // Calling front() on an empty container is *undefined*
  // Your program *may* crash, or it may not if vec is empty
  int& vec_front = vec.front();

  std::cout << "Reference\n";
  std::cout << vec_front << "\n";
  std::cout << vec_front + 1 << "\n";
  std::cout << vec_front + 2 << "\n";

  std::cout << "For Loop\n";
  for (const int& n : vec) {
    std::cout << n << "\n";
  }
}
Pointer
10
9
8
Reference
10
11
12
For Loop
10
9
8

Nullptr

There is one special pointer value in C++, the nullptr. No matter what type a pointer points to it can always be assigned nullptr. nullptr means the pointer points to nothing and so can be used to represent the absence of data like how “0 apples” represents the idea of “I have no apples”. Because of this, if your function accepts a pointer and is unsure it’s not nullptr you should check with an if statement first. Here’s an example:

#include <iostream>
#include <vector>

bool maybe_print_int(const int* const maybeInt) {
  // Pointers can be implicitly cast to bool for if and return
  if (maybeInt) {
    std::cout << *maybeInt << "\n";
  }
  return maybeInt;
}

int main() {
  std::vector<int> vec{10, 9, 8};

  // data() returns the first element or nullptr if there's nothing
  int* vec_data = vec.data();
  if (maybe_print_int(vec_data)) {
    std::cout << "Printed an int\n";
  } else {
    std::cout << "No int to print\n";
  }
}
10
Printed an int

There is no special “nullref”. A reference should always alias an existing variable. C++ demands a reference refer to something, but sometimes objects are unable to comply. Instead of crashing the program will continue on in these cases, at least for some time, which is no good at all. Similar to checking for nullptr, if a function returns a reference it may have unenforced restrictions on when you may call that function. Always check the documentation for functions and their preconditions or when you’re allowed to call them. Here’s an example of how to guard for this:

#include <iostream>
#include <vector>

void print_int(const int& i) {
    std::cout << i << "\n";
}

int main() {
  std::vector<int> vec{10, 9, 8};

  // Does vec have at least 1 element?
  if (!vec.empty()) {
    // front() returns a reference to the first element
    // Calling front() on an empty container is *undefined*
    // Your program *may* crash, or it may not if vec is empty
    int& vec_front = vec.front();

    print_int(vec_front);
  }
}
10

For more C++ By Example, click here.