Intermediate openFrameworks - Pointers
Introduction
Many people coming to openFrameworks/c++ from Processing (or similar) are terrified of the dreaded pointer. Perhaps they have heard horror stories about programs crashing for mysterious reasons, confusing syntax, or having to master an esoteric dark art dealing with the deep-down silicone innards of their computer. In fact, the pointer itself is only indirectly related to the real culprit: dynamic memory allocation.
As you probably know, languages such as Java/Processing, C#, and most scripting languages use automatic memory management, and/or [garbage collection](http://en.wikipedia.org/wiki/Garbage_collection_(computer_science), which means that there is essentially an additional routine running alongside your program that keeps track of which variables are still in use and which ones can be released to free up memory. C++, on the other hand, uses manual memory management, which means that a garbage collector doesn’t have to run, which leaves more processing power for your program to do awesome stuff.
The only time that managing your own memory becomes a problem is when you choose to use dynamic memory allocation: one of 3 types of memory allocation in c++. So what is dynamic memory allocation, and why would I want to use it?
And, for bonus points: How can you avoid using it?
Memory-related operators
Before we talk about dynamically allocated memory, we have to understand a few simple memory-related concepts within c++ that will help us to understand the relationship between variables and the physical memory in which they are stored. So let’s get them out of the way.
sizeof() - Variables take up space
You can (and perhaps already do) think of the memory of your computer as a ton of tiny boxes, each of which is 1 byte. If you have 4GB of memory, (1 byte = 1024kb, 1MB = 1024kb, 1GB = 1024MB) then you have 4,294,967,296 (over 4 billion) of these little boxes. An int
takes up 4 of these boxes, or 0.00000000093132% of your 4GB, while a double
takes up 8 boxes. Consider the following program:
- save this program as
size.cpp
- compile it§:
g++ -o size size.cpp
- run it:
./size
You should see:
sizeof(foo) = 4
This very simple program illustrates the use of a function† called sizeof, which simply tells you how many bytes (“boxes”) a particular variable type takes up in memory. Try changing int
(on line 7) to float
, double
, long
, and char
and see what output you get. Notice that we aren’t assigning a value to “foo”, and yet it still takes up space in memory.
NOTE I’ve often seen people confuse sizeof() with a function that returns the size of an array, like count in PHP, the length attribute in JavaScript, or even vector::size(). Sadly, this isn’t the case. In a nutshell, there is no fool-proof way to get the size of a c++ array. This thread on StackOverflow explains the problem well.
§ If you are using XCode 4+, you’ll have to install the Command Line Tools to get the command-line c++ compiler, g++
.
† Full disclosure: If you look closely at the documentation, sizeof
is actually an “operator” and not a function. This StackOverflow question explains why, but when I started reading it, I began involuntarily hitting myself in my face, so I stopped.
For a more detailed overview of variable types, check out the cplusplus.com documentation on the subject.
BONUS POINTS Try this one out too! How much memory does a Player
use and why?
The Reference Operator
Each one of our tiny boxes also has a unique address. The addresses are sequential, hexidecimal numbers. When we declare a variable, the chunk of memory that it fills starts at one of these tiny boxes. This is commonly known as the memory address of a variable. In order to find out where in our “ton of tiny boxes” a variable is being stored, we can use the reference operator.
Run this and you should see something similar to the following†:
&foo = 0x7fff538059c4
So, what’s going on here? As we see on the second line, when we add a &
before a variable, instead of telling us the value of foo
, the reference operator will tell us the memory address of the “tiny box” that foo
starts in. Notice that we don’t need to assign a value to foo
for it to be assigned a memory address.
NOTE We learned above that an int takes up 4 bytes, and since memory addresses are sequential, that means that the int foo stretches from 0x7fff538059c4 to 0x7fff538059c7.
TANGENT Try printing out the value of
foo
instead of the address (take out the ‘&’). Interestingly, c++ does not auto-initialize variables, so if you simply declare a variable without assigning a value, it will contain whatever value was in that block of memory before your program started, but interpreted as whatever kind of variable you declare. This could be 4 bytes of just about anything that any other program had in memory.
† If you run this code, your memory address will almost certainly be different then mine, but it should be a hexadecimal number with ‘0x’ in front of it, which is the c++ way of denoting a base 16 number.
Memory blocks
To further illustrate this point, let’s take a closer look at what happens when we declare two arrays.
Run this and you should get something similar to:
&foo[0] = 0x7fff5d93ba60
&foo[1] = 0x7fff5d93ba64
&foo[2] = 0x7fff5d93ba68
&foo[3] = 0x7fff5d93ba6c
&bar[0] = 0x7fff5d93ba40
&bar[1] = 0x7fff5d93ba48
&bar[2] = 0x7fff5d93ba50
&bar[3] = 0x7fff5d93ba58
If you look closely at the memory addresses of the array elements (they seemed to be allocated in reverse order of declaration), you can see that bar
starts at ‘a8’ and since it is an array of doubles
, each successive element of the array is 8 blocks after the previous, ending at ‘c0’. Then foo
picks up at ‘c8’, and since an int
takes up 4 bytes, it increments in chunks of 4.
NOTE c++ guarantees that an array will get sequential memory addresses (kind of the definition of an array?), but AFAIK, the same cannot be said of variables that are declared right next to each other in your code. In other words, you can’t count on
foo
andbar
occupying neighboring blocks.
Pointers
We’ve already seen how we can print out the address of a variable. Next we will take it one step further and create a variable that can hold that address: a pointer. Consider the following program, which does essentially the same thing as the last one, but we assign the address of foo
to pFoo
before printing it out.
And you should get:
&foo = 0x7fff538059c4
pFoo = 0x7fff538059c4
The takeaway: in the same way that a decimal number is a type of variable known as a float
, a memory address is a type of variable known as a pointer
. Another way to think about this: for each variable type (int, char, double, ofPoint), there is a type of variable that can hold the memory address of that type (int*, char*, double*, ofPoint*).
Pointer to an object
REMINDER So far we are still just covering memory-related syntax, which is important, but kind of useless out of context. Starting in the next section, you get some context, so just hold on.
One last caveat about pointers: so far, we have only looked at pointers to primitive data types. Unfortunately, pointers to objects behave slightly differently than regular pointers. Take a look:
- Make an ofPoint object
- Initialize the x and y attributes
- Make a pointer to that ofPoint
- Increment the x and y attributes using the pointer to the ofPoint
Remember that ofPoint* pPos
is just a memory address. That memory address isn’t actually an object, but a hexadecimal number (perhaps something like 0x7fff538059c4
). So you can’t simply say pPos.x += 5
or pPos.y += 2
. Instead, you have to use this funky arrow operator: ‘->’.
Bonus question What would happen if you did increment a pointer directly?
Answer Garbage!
Dereference Operator
As the name suggests, the dereference operator is the opposite of the reference operator. But what does that mean?
The reference operator, when placed before a variable, will give you the memory address of that variable.
The dereference operator, when placed before a pointer, will give you the value stored at the memory location.
foo = 5
&foo = 0x7fff55d069a4
pFoo = 0x7fff55d069a4
*pFoo = 5
In this snippet, we are doing basically the same thing as the previous example, only now we have a way to get the actual value that pFoo
points to.
“Pass By Copy” vs “Pass By Reference”
So what are the practical applications of these memory operators? Why is all of this necessary? Here is one reason:
In most newer languages, when you pass an object to a function, it is assumed that you want to be able to manipulate the object within the function.
Consider the following pseudo-code:
QUESTION What would joe’s position be at the end of the program?
In most newer languages, the answer would be (5, 0). But in c++, it’s (0,0). The default behavior of c++ is to pass by value, meaning a copy of joe
is made and assigned to somePlayer
, and the “+= 5” is done to somePlayer
, leaving joe
completely untouched. In other words, the complete execution of the program goes something like:
- Construct a Player object called
joe
- set
joe.position
to (0, 0) - call moveRight
- make a Player called
somePlayer
- copy all attributes of
joe
tosomePlayer
- increment
somePlayer.position.x
by 5
- make a Player called
- print out
joe's
position, which is still (0, 0)
Notice that 2 “Player” objects are created – one called joe
and a completely different one called somePlayer
that is a copy of joe
. No change is ever made to joe
.
Ta da! Pass by copy.
You can remedy this problem in two ways:
Pass by reference
Passing by reference is probably the most straightforward way. All you have to do is put an ampersand after an argument, and that means, “use the actual object that I am passing in; don’t make a copy”.
At the end of this program, joe.position
would be (5,0).
Pass a pointer
The other, more complicated way is to pass the memory address of the object (a pointer) into the function.
Notice that the function now expects pointer to a Player object - a memory address at which a Player object begins. Technically, this is still “pass by copy” – the moveForward
function makes a copy of &joe
(the memory address of joe
) and assigns it to Player* somePlayer
(pointer to a player object). Notice another change: since somePlayer is just a memory address, you have to use this funky arrow operator ‘->’ instead of a dot (‘.’) to access it’s member variables and functions.
Memory Allocation
Okay, with those simple tools under our belt, we can start to talk about the good stuff:
Memory allocation is the process by which your computer finds memory for variables. Whenever you declare a variable, your computer must find somewhere in memory to place that variable. You probably realize that this process happens somehow when you run a program, but if you use a language with auto memory management, you’ve had no need to give it much thought.
There are 3 ways that allocation can take place: static, automatic, and dynamic†. You probably understand the first two already. It’s really only important to understand the distinction between static and automatic so that you can appreciate them in relation to dynamic, so here it goes:
† Not all languages have all three forms of allocation. In some cases (e.g. Java), even if a form of allocation is supported, there are restrictions such as allowing automatic allocation for built-in types, but requiring dynamic allocation for object types (i.e. instances of classes).
Static and Automatic Memory Allocation
For most programmers, it will suffice to say that the difference between static and automatic memory allocation is the difference between global and non-global variables. That is, if a variable is declared at program startup, before main() is even called, it’s statically allocated. If you could change program logic so that a variable is never declared, it’s probably automatic allocation.
Output:
&foo = 0x1033650b0
&bar = 0x7fff5c89b9ac
foo
is allocated when the program begins and sticks around for the entire life of the application. This is known as static allocation.
bar
is allocated when f()
is called, and then is released immediately after f()
returns. This is known as automatic allocation because it is automatically allocated when it comes into scope and automatically deleted after it goes out of scope. In any programming language, this is pretty much common sense.
TIP Notice how different the memory addresses of ‘foo’ and ‘bar’ are compared to the examples above. You might think that, since they are allocated within a millisecond of each other, they would have similar addresses. In fact, since
foo
is allocated at program startup and ‘bar’ is allocated upon entry into a block, they are stored in different memory sectors. For more information, check out this page.
Dynamic Memory Allocation
Congratulations! You’ve made it! Let’s talk about dynamic memory allocation (henceforth DAM). If you haven’t already looked at the lesson on constructors, take a minute now and go through it.
If you are coming from a language with automatic memory management, you might take for granted the fact that the memory that a variable uses is tied to it’s scope. For example, it should be pretty obvious that, in the following code, letter
doesn’t exist outside of the printRandomLetter()
function.
In other words, the memory that letter
occupies is allocated when printRandomLetter()
begins, and then is released when it finishes. Both static and automatic allocation work this way.
But with DAM, you can create a variable (or “allocate memory”, more accurately) that is outside of any scope.
To be continued…