(Yet Another) Simple Explanation For Pointers in C
December 12, 2018
This is an answer to a Quora question How would you explain the most complex concepts of C, such as pointers, in the easiest way to a 13-year-old boy?
I removed myself from Quora forever so this answer doesn’t exist there anymore, but the question still is.
By themselves pointers are not a hard concept. Dereferencing them is. ;)
To actually understand what is going on in C, one has to have a specific mental model of how the C runtime treats the memory. It is, in fact, how all of languages treat the memory, but that’s another story.
Memory model
Memory of the computer is a grid of cells. Each cell holds a number from 0 to 255. So that 16GB RAM plank in your motherboard is internally a huge field divided by one-byte squares, OK?
Absolutely everything which we think is “inside a memory of the computer”: pictures, text, videos, programs themselves, are stored as collections of numbers inside those cells. Contents of a single video file, for example, may fill billions of those cells.
Data address
Each of those squares have an address. This address is itself a number, a single number. So, in fact, memory is not a grid but a line. But grid is simpler to visualize in head, and software engineers always visualize boatloads of things in their heads, so the usual interpretation is that memory is a grid. Each cell in the memory holds a value, which is a number, and have an address, which is another number.
Machine code
Now, everything is a set of numbers, and to do anything you have to interpret those numbers in some way. The piece of hardware which actually executes your program, the CPU, can do quite a small number of different things, in fact. Most of the stuff it can do is related to adding numbers and seeking memory cells by their addresses. Variety of things that you can do with CPU is so small, that you can assign every command a number and store sequences of those numbers in the same memory as the data you manipulate!
Such sequences of numbers are called “machine code” and you can code directly using them if you want. In fact, at the beginning of programming, people did exactly that as they had nothing else.
With special syntax, C allows you to write pieces of your program in machine code.
Compilation
Writing programs with numbers is not convenient at all, and reading them, let alone changing, is even harder, so people wrote programs which translate some other language into machine code. So the C was born.
To make a program using C, you write a source code of a program, as text. After that, you run a special program called a C compiler, which takes your source code and creates a sequence of numbers which is the machine code for a computer to execute. Those numbers are the program itself. Usually they are saved as a separate file on a disk. Such files are called executable binary files, or executable files, or binary files. To run your program, you need to use your operating system to run, or “execute”, them.
One of the first most important features of C language is ability to use variables.
Variables
Variable is a name you give to a sequence of cells in memory. There are several complications, as you can name not only cells in memory but some other separate places like stack, but I mentioned that only for completeness’s sake, it does not change the principle.
Variable is a place in memory which has a name now. So, in the code you can use commands to write to that place in memory and read from it using that name. Like that:
int i = 1;
i = i + 1;
When literate programmers who know C read this code, they understand it like following, if they don’t want to delve into much details.
When computer will execute line 1, it will find some place in memory, and make a note for itself that this place is called “i” now. This variable should be treated as “integer” variable, because it is marked with “int” before the name. So computer will remember that this place is 4 cells wide, because on the imaginary computer programmer in question wants his program to run on integers take 4 bytes of space.
Finally, at the end of line 1, computer will fill this place named “i” with the number corresponding to letter 1 in the source code, which is, unsurprisingly, 0000 0000 0000 0001
.
When the computer will execute line 2, it will need to fill the variable i with the value stored in the variable i
plus 1
. To do that, it needs to know what number is stored in the variable i, and as it knows already what place in memory corresponds to name i, it just goes there and fetches what we wrote there in line 1, which is still number 0000 0000 0000 0001
.
That’s it. Whew, that was long road. We’re closing in on really hard stuff now.
Functions
I will skip this part because I don’t want to re-type the whole K&R book in this answer. In short, in C, functions are names which we give not to data, but to sequences of actions which computer has to do. It’s names we give for code.
Pointers
I know, I know, you all here for this part. Here you are.
We call “pointers” variables which hold not the values, but addresses of other variables. This require us to be diligent:
int i = 1;
int* ip = &i;
i = *ip + 1;
When the code above will be executed by the computer, place in memory called “i” will still hold 0000 0000 0000 0010
(which is binary bytes for decimal number 2), the same as with my previous example. However, the computer will perform completely different things to achieve that.
First, on line 2, it will “create” another “variable”, that is, remember another name for another place in memory. This name will be “ip
” and as we said not “int
” but “int*
” computer knows that it’s not an integer, but the address of variable which is integer. Addresses of another variables take 8 cells of memory if your CPU is modern 64-bit one. On older 32-bit processors it will take 4 cells, because that’s what it means to be “64-bit” processor.
The funny combination of symbols &i
which we wrote as a value to store in the variable ip means that computer should find the address of the variable i. Not the value of the variable — the numbers stored in cells of memory — but its address — address of the first cell of memory of this variable.
Now the ip
variable is a “pointer”. We say that when variable holds not a value, but an address to another variable.
After that, at line 3, we wrote “*ip
”. This is called “dereferencing” a pointer. The number which is stored in its memory cells is not a value which is useful as is. It’s an address of another memory cell. So the *ip
operation takes the address stored in the ip variable, goes there in memory, and takes value out of there, which is an integer number we know.
Basically, name “i” and the address stored inside variable “ip” point to the same place in memory, and in that place in memory the mighty number 0000 0000 0000 0001
is being stored. To execute line 3, computer finds this number by dereferencing ip variable, adds 1 to it, then stores the result in the place in memory which is called i
.
Pointer arithmetic
And this is the real pain when talking about C and C++. Chtulhu lives in here exactly. Problem is, as addresses are just another numbers, you can manipulate them in C as yet another numbers, but with some differences associated.
For example, here’s the code:
int i = 1;
int* ip = &i;
ip = ip + 1;
i = *ip;
At line 3 we do some interesting thing. We add 1 to the value stored under the name ip
, which is an address, as we already know. What should happen when you increase the address, which is the number? Well, you will get the address of the next cell in memory after this one.
As the result, at line 4, address stored inside ip
does not point to the same cell as the name i anymore. Now it points to the second cell inside the set of 4 cells remembered under the name “i”. And when the computer actually does *ip
and dereferences the ip variable it will get not the contents of 4 cells stored under the name i but the last 3 cells of them plus the one next to them. Which can be anything at all — another running program’s data, possibly the another program or zeroes or scraps of data left over from the program which already exited long ago. When I execute the above code I get 6422316 instead of 1 or 2.
Pointer arithmetic is incredibly useful when you know exactly where you stuff lies in the memory and for some reason want to quickly jump between different places. But if you make a mistake resulting errors can be so severe as to crash your operating system or worse, silently break something so you will not know about it. A lot of computer visures are made based on knowledge about mistakes people did when using the pointers in their programs. It’s that hard to understand and use properly.
P. S. I can’t believe I actually tried to Google “pointer width on 64 bit systems” :facepalm.gif: