Pointers

Pointers video (28 minutes) (Spring 2021)

Pointers

Before talking about pointers, we need to talk about what a variables and memory.

Note: I'm going to assume that sizeof(int) == 4, sizeof(long) == 8, and sizeof(T*) == 8 (e.g. sizeof(int*) == 8). This is the case on most 64-bit architecture/compiler combinations.

Variables In Memory

When you declare a variable by saying int x, part of the compiler's job is to decide where in memory that variable should go. On my 64-bit laptop, an int is 4 bytes, so the compiler will need to make sure there's at least 4 bytes for x in memory. If you have multiple variables, the compiler has to plan out where each one goes.

// Note: I'm assuming sizeof(int) == 4 for this example.
// When the compiler sees this code, it might give x an address like 1,024
// (0x400) and y and address like 1,024 + sizeof(int) = 1,024 + 4 = 1,028
// (0x404). You don't know where x and y will be stored or if they'll be next to
// each other in memory *but* you also don't have to do any work to make sure
// that all of your variables have some place to be and they aren't overlapping.
int x = 5, y = 7;

Pointers: Indices into Memory

Memory is basically a giant array of bytes where pointers are like indices into memory. When you write code like int *p = &x;, the compiler sets aside sizeof(int*) bytes of memory for p and then sets the value of those bytes to represent the address of x, which in our example is 0x400 (1,024). In a practical example, the address will probably be some very large number like 0x7fff1394fa1c (140733521918492).

// Note: I'm assuming sizeof(int) == 4 for this example.
// When the compiler sees this code, it might give p an address like 2,048
// (0x800), x and address like 2,048 + sizeof(int*) = 2,048 + 8 = 2,056
// (0x808), and y an address like 2,068. You don't know where x, y, and p will
// be stored or if they'll be next to  each other in memory *but* you also don't
// have to do any work to make sure that all of your variables have some place
// to be and they aren't overlapping.
// You do know that the bytes at &x will represent the int 5 and the bytes at &p
// will represent &x (e.g. 0x808).
int x = 5, y = 7;
int* p = &x;

Arrays

If you have an array, int a[] = {11, 22, 33, 44, 55};, then the compiler will set aside 5 * sizeof(int) == 20 bytes somewhere. Since it's an array, you are guaranteed that the ints are stored right after each other in memory. You don't know where &a[0] is but you do know that &a[1] is sizeof(int) == 4 bytes after &a[0] and in general that &a[i + d] is d * sizeof(int) == d * 4 bytes after &a[i].

The variable a actually behaves almost identically to a pointer and represents the address of the 0th element in the array; in basically every way, you can treat it as a pointer and it will work the same. If you have int *p = a; // same as p = &a[0], then p and a will behave in almost the same way. You can actually write code like p[i] and get the same exact result as a[i] because in both cases, what you're really doing is taking the address that a or p is pointing to, then adding i * sizeof(int) to it, then reading those sizeof(int) == 4 bytes from memory.

*p and *a also do the same thing and more interestingly, *(p + 4) and *(a + 4) also do the same thing, but it might not be exactly what you expect. The + 4 doesn't add 4 bytes to the address, it adds 4 * sizeof(int) bytes to the address. Since the compiler knows that a is an int[] and p is an int*, it knows that + d means it should add d * sizeof(int) to the address, not just d to the address. If I wanted to do a for loop over a using just pointers, I could write code like this:

int a[] = {11, 22, 33, 44, 55};
int *begin = a, *end = a + 5;

// If &a[0] is 0x800 and sizeof(int) == 4, then this would print:
// 0x800  11
// 0x804  22
// 0x808  33
// ...
for (int *p = begin; p != end; ++p) {
  cout << p << "  " << *p << endl;
}

Pointers to Pointers

I might make more detailed notes on pointers to pointers later but I'll just say this short bit for now:

So far, we've just looked at int* that point to int but you can have a pointer to any type, including other pointers. For any type and variable, T x, you can have a T* p. When you write *p, that code will read the address that p represents, then go read sizeof(T) bytes from that address and interpret those bytes as representing a T. If T = int*, then... dereferencing that pointer (*p) will go to the address of x (&x) and read those sizeof(T*) == sizeof(int*) == 8 bytes and interpret them as an int pointer.

Example Program

Let's tie all this together with a quick example program to demonstrate:

pointers_example.cpp

#include <iostream>

using std::cout;
using std::endl;

int main(int argc, const char *argv[]) {
  int x = 5, y = 7;
  int a[] = {11, 22, 33, 44, 55};
  int *p = &x;

  cout << "sizeof(int) == " << sizeof(int) << endl;
  cout << "sizeof(int*) == " << sizeof(int*) << endl;
  cout << "x == " << x << "  (at address " << &x << ")" << endl;
  cout << "y == " << y << "  (at address " << &y << ")" << endl;
  cout << "a == " << a << "  (at address " << &a << ")" << endl;
  cout << "p == " << p << "  (at address " << &p << ")" << endl;
  cout << endl;

  p = a; // same as p = &a[0];
  cout << "Now p == " << p << " == a == " << a << endl;
  cout
      << "*a == " << *a << " && a[0] == " << a[0] << " && "
      << "*p == " << *p << " && p[0] == " << p[0]
      << "  at address " << a << " " << p << endl;
  for (int i = 0; i < 5; ++i) {
    cout
        << "*(a + " << i << ") == " << *(a + i) << "  at " << a + i << endl
        << "    a[" << i << "] == " << a[i]     << "  at " << &a[i] << endl
        << "*(p + " << i << ") == " << *(p + i) << "  at " << p + i << endl
        << "    p[" << i << "] == " << p[i]     << "  at " << &p[i] << endl;
  }
  cout << endl;

  cout << "You can iterate using indices in an array or with pointers:" << endl;
  int *begin = a, *end = a + 5;
  for (int *p = begin; p != end; ++p) {
    cout
        << "int at address " << p << " is " << *p << " which is "
        << p - begin << " ints from address " << begin << endl;
  }
  return 0;
}
$ clang++ -pedantic -Wall -lm -std=c++20 -o pointers_example pointers_example.cpp
$ ./pointers_example
sizeof(int) == 4
sizeof(int*) == 8
x == 5  (at address 0x7ffee445f4ac)
y == 7  (at address 0x7ffee445f4a8)
a == 0x7ffee445f490  (at address 0x7ffee445f490)
p == 0x7ffee445f4ac  (at address 0x7ffee445f488)

Now p == 0x7ffee445f490 == a == 0x7ffee445f490
*a == 11 && a[0] == 11 && *p == 11 && p[0] == 11  at address 0x7ffee445f490 0x7ffee445f490
*(a + 0) == 11  at 0x7ffee445f490
    a[0] == 11  at 0x7ffee445f490
*(p + 0) == 11  at 0x7ffee445f490
    p[0] == 11  at 0x7ffee445f490
*(a + 1) == 22  at 0x7ffee445f494
    a[1] == 22  at 0x7ffee445f494
*(p + 1) == 22  at 0x7ffee445f494
    p[1] == 22  at 0x7ffee445f494
*(a + 2) == 33  at 0x7ffee445f498
    a[2] == 33  at 0x7ffee445f498
*(p + 2) == 33  at 0x7ffee445f498
    p[2] == 33  at 0x7ffee445f498
*(a + 3) == 44  at 0x7ffee445f49c
    a[3] == 44  at 0x7ffee445f49c
*(p + 3) == 44  at 0x7ffee445f49c
    p[3] == 44  at 0x7ffee445f49c
*(a + 4) == 55  at 0x7ffee445f4a0
    a[4] == 55  at 0x7ffee445f4a0
*(p + 4) == 55  at 0x7ffee445f4a0
    p[4] == 55  at 0x7ffee445f4a0

You can iterate using indices in an array or with pointers:
int at address 0x7ffee445f490 is 11 which is 0 ints from address 0x7ffee445f490
int at address 0x7ffee445f494 is 22 which is 1 ints from address 0x7ffee445f490
int at address 0x7ffee445f498 is 33 which is 2 ints from address 0x7ffee445f490
int at address 0x7ffee445f49c is 44 which is 3 ints from address 0x7ffee445f490
int at address 0x7ffee445f4a0 is 55 which is 4 ints from address 0x7ffee445f490