Introduction

C and C++ unlike a lot of other languages provide the ability to directly interact with hardware memory - normally described as a middle-level or systems language - through the use of pointers. Every variable declared in any language has a memory location, and every memory location has an address used to locate it. C++ gives you the power to directly modify this memory.

Accessing Memory

In C++, the memory address (where a value lives) of a variable can be accessed through the && operator - see the example below.

std::string food = "Pizza";
cout << food;  // outputs the value of food (Pizza)
cout << &food; // outputs the memory address of food (0x6dfed4)
std::string food = "Pizza";
cout << food;  // outputs the value of food (Pizza)
cout << &food; // outputs the memory address of food (0x6dfed4)

Pointers

A pointer on the other hand, is a variable that stores the memory address of a variable as its value, and is created using the * operator. A pointer can exist for any data type, as it merely points to a memory address of the variable.

int x = 5;
int* y = &x;
// Equivalent to
int *y = &x;

cout << x; // outputs the value of x (5)
cout << y; // outputs the memory address of x (0x6dfed4)
cout << &y; // outputs the memory address of y (0x6ffd4)
cout << *y; // outputs the value of x (5)
int x = 5;
int* y = &x;
// Equivalent to
int *y = &x;

cout << x; // outputs the value of x (5)
cout << y; // outputs the memory address of x (0x6dfed4)
cout << &y; // outputs the memory address of y (0x6ffd4)
cout << *y; // outputs the value of x (5)

Arrow Operator

The arrow operator is a shorthand method for accessing the contents of a struct or class pointer - normally the .. operator would be used for non-pointer access. The example below shows the equivalences.

class Rectangle {
  int width, height;
  public:
  void set_values (int,int);
  int area (void);
} rect;

Rectangle* p_rect = &rect; // creates a pointer to the rect declared above
p_rect->area;
// This is equivalent to
(*p_rect).area;
class Rectangle {
  int width, height;
  public:
  void set_values (int,int);
  int area (void);
} rect;

Rectangle* p_rect = &rect; // creates a pointer to the rect declared above
p_rect->area;
// This is equivalent to
(*p_rect).area;

References and Pointers

By default, arguments to a function in C++ are passed by value, and the changes made to any arguments inside the function will not affect the original value, and instead are made to a copy. The 3 primary ways to pass arguments are shown below.

Call by value

int square1(int n)
{
  cout << &n; // outputs the memory address of n (0x6dfed4)
  n *= n;
  // original n is unchanged - a copy of n has been created,
  // only the n inside the function is altered
  return n;
}
int square1(int n)
{
  cout << &n; // outputs the memory address of n (0x6dfed4)
  n *= n;
  // original n is unchanged - a copy of n has been created,
  // only the n inside the function is altered
  return n;
}

Call by reference with a pointer argument:

int x = 5;
int *p_x = &x;
square(p_x);

void square(int *n)
{
  cout << n; // outputs the memory address of n, which is the same as x (0x6dfed4)
  *n *= *n; // deferences to get the value at the memory address of p_x,

  // updating the value stored there
  cout << *n; // outputs the squared value of x (25)
  cout << x; // outputs the altered value of x (25)
}
int x = 5;
int *p_x = &x;
square(p_x);

void square(int *n)
{
  cout << n; // outputs the memory address of n, which is the same as x (0x6dfed4)
  *n *= *n; // deferences to get the value at the memory address of p_x,

  // updating the value stored there
  cout << *n; // outputs the squared value of x (25)
  cout << x; // outputs the altered value of x (25)
}

Call by reference with reference argument:

int x = 5;
square(x);

void square(int &n)
{
  // inside the function, the reference is used to access the
  // actual argument used in the call
  cout << &n; // outputs the memory address of x (0x6dfed4)
  cout << n; // outputs the value of x (5)

  // Implicit de-referencing (without '*')
  n *= n;
  cout << n; // outputs the altered value of n, which is equal to x (25)
  cout << nx; // outputs the altered value of x (25)
}
int x = 5;
square(x);

void square(int &n)
{
  // inside the function, the reference is used to access the
  // actual argument used in the call
  cout << &n; // outputs the memory address of x (0x6dfed4)
  cout << n; // outputs the value of x (5)

  // Implicit de-referencing (without '*')
  n *= n;
  cout << n; // outputs the altered value of n, which is equal to x (25)
  cout << nx; // outputs the altered value of x (25)
}

Benefits of Pointers

There are a number of benefits to using pointers:

  • Values that are passed in to a function do not have to be copied, especially useful with large objects.
  • They allow more than one value to be returned from a function.
  • Memory access through pointers is stated to be more efficient than through an array.
int x = 5;
int y = 55;
int *p_x, *p_y = &x;
edit_two(*x, *y);

void edit_two(int *x, int *y)
{
  *x += 5;
  *y += 5;

  cout << *x; // edits the value at the memory address of x (10)
  cout << y*; // edits the value at the memory address of x (60)
}
int x = 5;
int y = 55;
int *p_x, *p_y = &x;
edit_two(*x, *y);

void edit_two(int *x, int *y)
{
  *x += 5;
  *y += 5;

  cout << *x; // edits the value at the memory address of x (10)
  cout << y*; // edits the value at the memory address of x (60)
}

Hopefully this has cleared up any confusion surrounding pointers, and you can begin to understand those insufferable nullptr exceptions.