Intermediate openFrameworks - Pointers
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?
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
- compile it§:
g++ -o size size.cpp
- run it:
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
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.
§ If you are using XCode 4+, you’ll have to install the Command Line Tools to get the command-line c++ compiler,
† 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
fooinstead 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.
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 = 0x7fff5d93ba60 &foo = 0x7fff5d93ba64 &foo = 0x7fff5d93ba68 &foo = 0x7fff5d93ba6c &bar = 0x7fff5d93ba40 &bar = 0x7fff5d93ba48 &bar = 0x7fff5d93ba50 &bar = 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
baroccupying neighboring blocks.
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
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
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?
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
joe completely untouched. In other words, the complete execution of the program goes something like:
- Construct a Player object called
joe.positionto (0, 0)
- call moveRight
- make a Player called
- copy all attributes of
- make a Player called
- print out
joe'sposition, 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
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.
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.
&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
foois 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
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…