Why are pointers such a leading factor of confusion for many new, and even old, college level students in C or C++? Are there any tools or thought processes that helped you understand how pointers work at the variable, function, and beyond level?
What are some good practice things that can be done to bring somebody to the level of, "Ah-hah, I got it," without getting them bogged down in the overall concept? Basically, drill like scenarios.
I think it might actually be a syntax issue. The C/C++ syntax for pointers seems inconsistent and more complex than it needs to be.
Ironically, the thing that actually helped me to understand pointers was encountering the concept of an iterator in the c++ Standard Template Library. It's ironic because I can only assume that iterators were conceived as a generalization of the pointer.
Sometimes you just can't see the forest until you learn to ignore the trees.
Pointers is a concept that for many can be confusing at first, in particular when it comes to copying pointer values around and still referencing the same memory block.
I've found that the best analogy is to consider the pointer as a piece of paper with a house address on it, and the memory block it references as the actual house. All sorts of operations can thus be easily explained.
I've added some Delphi code down below, and some comments where appropriate. I chose Delphi since my other main programming language, C#, does not exhibit things like memory leaks in the same way.
If you only wish to learn the high-level concept of pointers, then you should ignore the parts labelled "Memory layout" in the explanation below. They are intended to give examples of what memory could look like after operations, but they are more low-level in nature. However, in order to accurately explain how buffer overruns really work, it was important that I added these diagrams.
Disclaimer: For all intents and purposes, this explanation and the example memory layouts are vastly simplified. There's more overhead and a lot more details you would need to know if you need to deal with memory on a low-level basis. However, for the intents of explaining memory and pointers, it is accurate enough.
Let's assume the THouse class used below looks like this:
When you initialize the house object, the name given to the constructor is copied into the private field FName. There is a reason it is defined as a fixed-size array.
In memory, there will be some overhead associated with the house allocation, I'll illustrate this below like this:
The "tttt" area is overhead, there will typically be more of this for various types of runtimes and languages, like 8 or 12 bytes. It is imperative that whatever values are stored in this area never gets changed by anything other than the memory allocator or the core system routines, or you risk crashing the program.
Allocate memory
Get an entrepreneur to build your house, and give you the address to the house. In contrast to the real world, memory allocation cannot be told where to allocate, but will find a suitable spot with enough room, and report back the address to the allocated memory.
In other words, the entrepreneur will choose the spot.
Memory layout:
Keep a variable with the address
Write the address to your new house down on a piece of paper. This paper will serve as your reference to your house. Without this piece of paper, you're lost, and cannot find the house, unless you're already in it.
Memory layout:
Copy pointer value
Just write the address on a new piece of paper. You now have two pieces of paper that will get you to the same house, not two separate houses. Any attempts to follow the address from one paper and rearrange the furniture at that house will make it seem that the other house has been modified in the same manner, unless you can explicitly detect that it's actually just one house.
Note This is usually the concept that I have the most problem explaining to people, two pointers does not mean two objects or memory blocks.
Freeing the memory
Demolish the house. You can then later on reuse the paper for a new address if you so wish, or clear it to forget the address to the house that no longer exists.
Here I first construct the house, and get hold of its address. Then I do something to the house (use it, the ... code, left as an exercise for the reader), and then I free it. Lastly I clear the address from my variable.
Memory layout:
Dangling pointers
You tell your entrepreneur to destroy the house, but you forget to erase the address from your piece of paper. When later on you look at the piece of paper, you've forgotten that the house is no longer there, and goes to visit it, with failed results (see also the part about an invalid reference below).
Using
h
after the call to.Free
might work, but that is just pure luck. Most likely it will fail, at a customers place, in the middle of a critical operation.As you can see, h still points to the remnants of the data in memory, but since it might not be complete, using it as before might fail.
Memory leak
You lose the piece of paper and cannot find the house. The house is still standing somewhere though, and when you later on want to construct a new house, you cannot reuse that spot.
Here we overwrote the contents of the
h
variable with the address of a new house, but the old one is still standing... somewhere. After this code, there is no way to reach that house, and it will be left standing. In other words, the allocated memory will stay allocated until the application closes, at which point the operating system will tear it down.Memory layout after first allocation:
Memory layout after second allocation:
A more common way to get this method is just to forget to free something, instead of overwriting it as above. In Delphi terms, this will occur with the following method:
After this method has executed, there's no place in our variables that the address to the house exists, but the house is still out there.
Memory layout:
As you can see, the old data is left intact in memory, and will not be reused by the memory allocator. The allocator keeps track of which areas of memory has been used, and will not reuse them unless you free it.
Freeing the memory but keeping a (now invalid) reference
Demolish the house, erase one of the pieces of paper but you also have another piece of paper with the old address on it, when you go to the address, you won't find a house, but you might find something that resembles the ruins of one.
Perhaps you will even find a house, but it is not the house you were originally given the address to, and thus any attempts to use it as though it belongs to you might fail horribly.
Sometimes you might even find that a neighbouring address has a rather big house set up on it that occupies three address (Main Street 1-3), and your address goes to the middle of the house. Any attempts to treat that part of the large 3-address house as a single small house might also fail horribly.
Here the house was torn down, through the reference in
h1
, and whileh1
was cleared as well,h2
still has the old, out-of-date, address. Access to the house that is no longer standing might or might not work.This is a variation of the dangling pointer above. See its memory layout.
Buffer overrun
You move more stuff into the house than you can possibly fit, spilling into the neighbours house or yard. When the owner of that neighbouring house later on comes home, he'll find all sorts of things he'll consider his own.
This is the reason I chose a fixed-size array. To set the stage, assume that the second house we allocate will, for some reason, be placed before the first one in memory. In other words, the second house will have a lower address than the first one. Also, they're allocated right next to each other.
Thus, this code:
Memory layout after first allocation:
Memory layout after second allocation:
The part that will most often cause crash is when you overwrite important parts of the data you stored that really should not be randomly changed. For instance it might not be a problem that parts of the name of the h1-house was changed, in terms of crashing the program, but overwriting the overhead of the object will most likely crash when you try to use the broken object, as will overwriting links that is stored to other objects in the object.
Linked lists
When you follow an address on a piece of paper, you get to a house, and at that house there is another piece of paper with a new address on it, for the next house in the chain, and so on.
Here we create a link from our home house to our cabin. We can follow the chain until a house has no
NextHouse
reference, which means it's the last one. To visit all our houses, we could use the following code:Memory layout (added NextHouse as a link in the object, noted with the four LLLL's in the below diagram):
In basic terms, what is a memory address?
A memory address is in basic terms just a number. If you think of memory as a big array of bytes, the very first byte has the address 0, the next one the address 1 and so on upwards. This is simplified, but good enough.
So this memory layout:
Might have these two address (the leftmost - is address 0):
Which means that our linked list above might actuall look like this:
It is typical to store an address that "points nowhere" as a zero-address.
In basic terms, what is a pointer?
A pointer is just a variable holding a memory address. You can typically ask the programming language to give you its number, but most programming languages and runtimes tries to hide the fact that there is a number beneath, just because the number itself does not really hold any meaning to you. It is best to think of a pointer as a black box, ie. you don't really know or care about how it is actually implemented, just as long as it works.
I think the main barrier to understanding pointers is bad teachers.
Almost everyone are taught lies about pointers: That they are nothing more than memory addresses, or that they allow you to point to arbitrary locations.
And of course that they are difficult to understand, dangerous and semi-magical.
None of which is true. Pointers are actually fairly simple concepts, as long as you stick to what the C++ language has to say about them and don't imbue them with attributes that "usually" turn out to work in practice, but nevertheless aren't guaranteed by the language, and so aren't part of the actual concept of a pointer.
I tried to write up an explanation of this a few months ago in this blog post -- hopefully it'll help someone.
(Note, before anyone gets pedantic on me, yes, the C++ standard does say that pointers represent memory addresses. But it does not say that "pointers are memory addresses, and nothing but memory addresses and may be used or thought of interchangeably with memory addresses". The distinction is important)
Some answers above have asserted that "pointers aren't really hard", but haven't gone on to address directly where "pointer are hard!" comes from. Some years back I tutored first year CS students (for only one year, since I clearly sucked at it) and it was clear to me that the idea of pointer is not hard. What's hard is understanding why and when you would want a pointer.
I don't think you can divorce that question - why and when to use a pointer - from explaining broader software engineering issues. Why every variable should not be a global variable, and why one should factor out similar code into functions (that, get this, use pointers to specialize their behaviour to their call site).
I think that what makes pointers tricky to learn is that until pointers you're comfortable with the idea that "at this memory location is a set of bits that represent an int, a double, a character, whatever".
When you first see a pointer, you don't really get what's at that memory location. "What do you mean, it holds an address?"
I don't agree with the notion that "you either get them or you don't".
They become easier to understand when you start finding real uses for them (like not passing large structures into functions).
In my first Comp Sci class, we did the following exercise. Granted, this was a lecture hall with roughly 200 students in it...
Professor writes on the board:
int john;
John stands up
Professor writes:
int *sally = &john;
Sally stands up, points at john
Professor:
int *bill = sally;
Bill stands up, points at John
Professor:
int sam;
Sam stands up
Professor:
bill = &sam;
Bill now points to Sam.
I think you get the idea. I think we spent about an hour doing this, until we went over the basics of pointer assignment.