NEW

Post Top Ad

Blossom Themes

Tuesday, March 22, 2011

Dev-corner: Bart K. explains (and rants about) C/C++ pointers

C pointer syntax is an egregious design flaw. It's so bad, in fact, that it's solely responsible for multiple generations of programmers struggling with pointers, which frankly aren't that hard of a concept. I'm going to take some time today to explain pointers and, in the process, point out exactly what it is I can't stand about C/C++ pointer syntax. If you've never been able to wrap your head around pointers up until now, please give this a read, and hopefully you'll have a better understanding of them by the time you get to the end. I'll start with the basics (which you probably already know, but please bear with me).

Conceptually, a pointer is a variable that points to a location in memory, generally another variable. In C and C++, the actual data stored in a pointer is a memory address, which is just a number that signifies a particular location in memory. You can interact with a pointer in two ways. You can either look at the actual value of the pointer, which as I said is a memory address, or you can
dereference the pointer (I'll explain this in a bit) and look at the value stored in the memory address that it's pointing to. A pointer can point to any type of variable. What's important to remember, though, is that pointers are their own type, distinct from the type of variable they're pointing to. A pointer to a character is not the same type as a character, which brings me to my first major beef with C/C++ pointer syntax. I'd like to introduce you to our recurring villain: the asterisk (*). The asterisk is a villain because he does two completely different things that both have to do with pointers, and he does them in a way that's very confusing. I'll address the first of these things now. Let's say you're declaring an integer.
int i;
Now you want to declare an integer pointer:
int *i;
When you're declaring variables, the asterisk signifies that the variable is a pointer. Easy enough, right?

Ha! If only.

A pointer is a type, the same way that character and integer are types. And yet, you don't declare them the same way as you declare other types. In fact, you can mix them in with declarations for different types, which is horribly inconsistent. C doesn't let you declare a mix of integers and characters on the same line -- so why can you declare a mix of integers and integer pointers on the same line?

Here's a better way to think of pointer declarations:
int* i;
Note that the space is now between the asterisk and the variable, so you're seeing "int*" instead of "int". This is conceptually a much better way to declare your pointers, because it stresses the fact that pointers are a distinct type. Unfortunately, here is where the evil asterisk rears his ugly head. Let's say now I want to declare two integers:
int a, b;
Good so far. Now I'm going to declare two integer pointers:
int* a, b;
Simple, right? No! The above code is wrong, even though it should be correct. What the above line of code actually does is declare 'a' as in int pointer and 'b' as a regular old integer, making it very difficult to treat pointers the way they ought to be treated -- as a separate type.

You either have to declare them one per line or declare them in a way that's conceptually misleading:
int *a, *b;
The above code correctly declares two pointers but makes the evil asterisk appear to be in his other role, which I'll get to in a bit. First, one other important note about declaring pointers: Never leave them uninitialized. It's 2011 at the time of this post -- the overhead of initializing a pointer is utterly minuscule compared to the amount of time it will save you later on when it's time to debug your code. The real way to declare a pointer is like this:
int* i = 0;
So, what am I doing here? Remember that a pointer is its own type, and it holds a memory address that points to a location in memory. When I set a pointer equal to something, I'm actually changing that memory address itself, not what's stored in the memory address. Hence, the above variable declaration is creating a pointer that points to memory address zero, which is invalid.

Now why on earth would I want to do that? Simple! When C looks at a pointer, it has no idea if the number contained in that pointer is a valid piece of allocated memory or just some random number that points off to Tux-only-knows-where. So what does C do? It just trusts you. It assumes, sometimes wrongly, that the number that's sitting inside that pointer is a valid memory address, and will happily write to that address if you tell it to, regardless of whether you've actually allocated memory there. Often times this will overwrite some of your code or another variable, and your program will continue humming happily along with no idea that it just made a huge mess of itself, only to mysteriously crash later on in a way that's very difficult to trace.

The thing about zero is the C knows it's not a valid address, so when you try to do something to memory address zero, your program will crash in an immediate, clean, and traceable way, which makes debugging a lot easier. If you're super hardcore and you're writing code on an embedded system where processor power is expensive, you can always remove the "= 0" bit later, although I wouldn't necessarily recommend it.

Another perfectly valid thing that you can do is just allocate the memory immediately when you declare the pointer instead of setting it to zero (or null, as they call it). In C, you'd do this with the malloc() function, and in C++, you'd use the 'new' operator. Here's how that looks:
/* In C: */
#include <stdlib.h>
int* i = (int*) malloc(sizeof(int));

/* In C++: */
int *i = new int;
Egad, what's all that craziness in the C version?

Let's look at the C declaration again, this time in glorious technicolor!
#include <stdlib.h>
int* i = (int*) malloc(sizeof(int));
First we'll take a look at malloc(). Malloc is actually a function that's declare in stdlib.h, which is why we included it. What malloc() does is allocates a block of memory and returns a void pointer to the allocated memory address.

A void pointer is useless on its own, since 'void' is a type that doesn't actually store anything. The intent, in this case, is that you cast the void pointer to whatever kind of pointer you're allocating, which brings us to the (int*) bit. If you're familiar with C and C++, you've probably already seen a cast, which is where you convert one type of variable to another. What we're doing in this case is telling it to change the void pointer that malloc() returned into an int pointer, so we can then set i equal to it. [edit: There are some good arguments in favor of omitting the cast, although my own C++ habits lead me to include it (not that one ought to be using malloc() in c++ anyway. See this stackoverflow link here: http://stackoverflow.com/questions/605845/do-i-cast-the-result-of-malloc (courtesy of
A2889261)]

The last part, the sizeof(int) is telling malloc() how much memory to allocate. Since we want a pointer to an integer, we're allocating enough space to store an integer. Sizeof is a function that's built into C that returns the size in memory of the given type. Hence, sizeof(int) returns the size of an integer in bytes, which can vary depending on your system architecture.

So, to summarize, the above line of code creates an integer pointer, initializes an int-sized block of memory, and sets the new pointer to point to the memory address you just initialized. Ugh.

C++ is way simpler in this regard:
int *i = new int;
When you say "new int" or "new char" or "new whatever" in C++, you're going through the same process as above, but in a much more readable way. The "new" operator looks at the type you gave it, initializes the memory for that type, and returns a pointer to it.

Hmm, now where'd that evil little bastard asterisk go? Ah, there he is! He's off dereferencing stuff!

So what does dereference mean, exactly? Well, remember how we when we initialized the pointer to zero, we were actually setting the address to zero, and not the contents of the address. One has to wonder at that point, how do we interact with the stuff contained within the address the pointer is pointing at? You dereference the pointer.

Dereferencing is an operator. An operator works essentially like a function, in that it takes a value and returns a different value. When you dereference an integer pointer, you get the integer that the pointer is pointing to in memory. When you dereference a character pointer, you get the character that the pointer is pointing to in memory. What's important to remember here is that when you dereference a pointer, it returns a different type than the pointer itself. This is because a pointer isn't the same type as the item that it points to -- a pointer to an integer is a pointer, not an integer.

So how do we actually dereference a pointer? We use... the asterisk! (dun dun DUNNNNN!)

This brings us to my other big problem with the asterisk. It does two completely different things. In one case, it signifies a type. In the other case, it's an operator. Let's look at this little piece of C++ code:
int* i = new int; // Here, I'm using * to signify
// a type.
(*i) = 100; // Here I'm using * as the dereference
// operator. I'm setting the value of the
// memory address that i points to to 100.

cout << "This is a memory address: " << i << "\n";
cout << "This is the value of that memory address: " << (*i) << "\n";
Notice that when I'm dereferencing i, I write (*i). The parentheses aren't strictly necessary, but they're another way to differentiate between using the asterisk to declare a pointer type and using it to dereference a pointer. In this case, the asterisk is an operator that operates on the pointer i and returns an integer variable that's located at the memory address i points to. I can then treat the result of that operation the same way I would treat any other integer.

Now, some more explanation on why I like to write my pointer declarations the way I do ("int* i;" instead of "int *i;"). Look at this code here:
int i = 0;
i = 0;
These above two lines of code do the same thing, the only difference being that the first line also declares i as an integer before it sets it to zero. Now look at this code:
int *i = 0;
*i = 0;
Any reasonable person would assume (wrongly) that the two above lines of code do exactly the same thing, with the exception that the first line also declares i as a pointer. In actuality, the first line ("int *i = 0;") creates a pointer an initializes the address that the pointer points to to zero. The second line dereferences the pointer and sets the value stored at the address to zero. In any case, if you try running the above two lines of code in immediate succession, you'll get a null pointer error, because you've set the pointer to a null address.

Instead, consider writing your code this way:
int* i = 0;
(*i) = 0;
Now, it's a lot more clear, despite the efforts of that thrice-damned asterisk, that the first line is initializing a pointer and the second line is dereferencing a pointer and setting the value of its address. (Note that running these two lines in immediate succession would still cause a null pointer error, for the same reason as above).

Finally, there's one last thing that you need to know about pointers: C and C++ don't care when pointers fall out of scope, which means that you need to explicitly get rid of them when you're finished with them. If you don't, the memory will never be de-allocated even once the pointer falls out of scope, so you won't know where it is to be able to de-allocate it or reference to it. This is what's called a memory leak, and it can cause your code to run slowly and eventually crash when it tries to allocate more memory than the machine has available. Delving deeply into memory allocation is beyond the scope of this blog entry, but here's a quick note about how to do it:
/* In C: */
#include <stdlib.h>
free(i);
i = 0;

/* In C++: */
delete i;
i = 0;
The free() function is essentially the reverse of the malloc() function, in that it tells the computer you're no longer using the memory that the pointer points to. The computer then releases the memory back into the pool so that it can be allocated again. The C++ delete operator does the same thing. The only catch here is that if you're writing in C++, even though it's possible to use malloc() and free(), you can't mix malloc() with delete and new with free(), since internally they work in different ways. In C++, it's best practice to avoid malloc() and free altogether, and you should never delete a pointer allocated with malloc(), or free() a pointer allocated with new.

The other thing you'll notice here is that I say "i = 0;" after I deallocate the pointer in both cases. This is just so that C and C++ will know in the future that the pointer no longer points to a valid address. If (as is often the case) you're deallocating a pointer at the end of a function, it's generally okay to skip this step, since it's just going to fall out of scope anyway. However, if you want to be as safe and clean as possible, it doesn't hurt to just leave it in.

So there you have it. My pointers rant. I predict that there will be at least some comments below saying that you don't have to do it my way, and that it's shorter to mix your pointer and non-pointer declarations or that it's better to not initialize your pointers. If you're new to pointers, ignore those people. Once you've gained a solid understanding of them, you can start breaking the rules a little bit. What I've written here isn't necessary in terms of language syntax; it's simply a good set of habits for beginners to get into in order to keep their understanding of pointers as clear as possible and also ease debugging.

If you have questions, feel free to ask in the comments. If you see any real, actual errors in my code, please point them out.

Peace,

Bart K.
OpenGameArt.org

No comments:

Post a Comment

Post Top Ad

Blossom Themes