Pointers and Memory

Computer Memory and Pointers

A computer’s memory is nothing more than a giant array. If you have eight gigabytes of RAM, it only means you have a gigantic eight-billion element array with each element being one byte. It’s the operating system’s job to manage this giant array, handing out small chunks of it to programs that need it. A pointer is an index on this giant array. In other words, pointers are just numbers.

To declare a pointer variable, simply add a * next to the type name. For example, if you want an int pointer named x, simply write int* x. Note that in some programs, you might see int *x (note the different spacing), but that’s basically the same.

It’s also possible to declare pointers to other pointer types. For example, int** x is just a pointer for the int* type.

Pointer Operations

The most basic operation for working with a pointer is the * operator. Roughly, if you have a pointer p, then *p is just the value at memory address p. The pointer’s type determines how much of the memory it reads. If you have an int pointer, it will look at the next 4 bytes from that memory address. If you have a char pointer, it will only look at one byte. Later on we’ll look at defining our own data types, and you can expect the * operator to read the correct number of bytes that correspond to our custom data types.

The other interesting operator right now is the & operator. The & operator takes a variable and gives us the memory address of that variable. In other words, & returns a pointer to something. In case you’ve been wondering, this is how scanf works and why we have to put & beside the variables we want scanf to save to. Functions cannot modify the variables that are passed to it, but if it has the correct memory address, it can just write data directly to that memory address. And that is why scanf needs us to give the memory addresses to save into.

An Example About Pointers

Here is a program that uses pointers. Please compile and run it with the command line, and then study its output carefully.

  1. #include <iostream>
  2. using namespace std;
  3. int main() {
  4. long long c = 1000;
  5. cout << "c is " << (long long)c << endl;
  6. cout << "&c is " << (long long)&c << endl << endl;
  7. long long d = 2000;
  8. cout << "d is " << (long long)d << endl;
  9. cout << "&d is " << (long long)&d << endl << endl;
  10. long long* x = &c;
  11. cout << "x is " << (long long)x << endl;
  12. cout << "&x is " << (long long)&x << endl << endl;
  13. cout << "*x is " << (long long)*x << endl;
  14. long long** y = &x;
  15. cout << "y is " << (long long)y << endl;
  16. cout << "&y is " << (long long)&y << endl << endl;
  17. cout << "*y is " << (long long)*y << endl;
  18. cout << "**y is " << (long long)**y << endl;
  19. }

Note that the code (long long)x is known as typecasting. If you don’t know what it is, we encourage you to read about it. You can think of it though as a conversion from one type to another. In this case, we’re converting everything to long long, which means that in the example (long long)&c we'll get to see the memory addresses themselves as integers.

An Experiment About Pointers as Numbers

Here is a fun little experiment to see what pointers are actually doing.

  1. #include <iostream>
  2. using namespace std;
  3. int main() {
  4. // The first byte of x is 3, the next is 5, then 7, then 11.
  5. unsigned int x = (3 << 24) + (5 << 16) + (7 << 8) + 11;
  6. // The unary & operator returns the memory address of a variable we already have.
  7. // &x gives us the memory of x as an integer pointer.
  8. // By the way, unary means single-argument. You might be more familiar with the unary minus operator.
  9. // -a is a unary operator that returns the negative of a.
  10. // So this takes the memory address of x and forcibly stores it in a pointer to a char (1 byte variable).
  11. char* ptr = (char*)&x;
  12. // Print out the data that ptr points to as an integer as a number
  13. cout << "The value of ptr points to is: " << (int)*ptr << " and the memory address is " << (unsigned long long)ptr << endl;
  14. // Make ptr point to the next byte. It can be thought of as an integer after all.
  15. ptr++;
  16. cout << "The value of ptr points to is: " << (int)*ptr << " and the memory address is " << (unsigned long long)ptr << endl;
  17. ptr++;
  18. cout << "The value of ptr points to is: " << (int)*ptr << " and the memory address is " << (unsigned long long)ptr << endl;
  19. ptr++;
  20. cout << "The value of ptr points to is: " << (int)*ptr << " and the memory address is " << (unsigned long long)ptr << endl;
  21. }

The output that you get from this experiment depends on your computer’s processor. You will almost certainly get 11, 7, 5, then 3, but on rarer computers you will get 3, 5, 7, 11. This is the effect of something strange called Endianness, and we won’t be discussing that now. Actually, we likely won’t discuss it ever since it’s just a weird quirk of history. The important thing to remember is that memory is just one giant array, and we can cut it up however we like. The demonstration here is just a kind of silly way to play around with pointers.