Pointers video (28 minutes) (Spring 2021)
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.
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;
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;
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 int
s 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;
}
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.
Let's tie all this together with a quick example program to demonstrate:
#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