I should start out by saying that C and C++ were the first programming languages I learned. I started with C, then did C++ in school a lot, and then went back to C to fluent in it.
The first thing that confused me about pointers when learning C was the simple:
char ch;
char str[100];
scanf("%c %s", &ch, str);
This confusion was mostly rooted in having been introduced to using reference to a variable for OUT arguments before pointers were properly introduced to me. I remember that I skipped writing the first few examples in C for Dummies because they were too simple only to never get the first program I did write to work (most likely because of this).
What was confusing about this was what &ch
actually meant as well as why str
didn't need it.
After I became familiar with that I next remember being confused about dynamic allocation. I realized at some point that having pointers to data wasn't extremely useful without dynamic allocation of some type, so I wrote something like:
char * x = NULL;
if (y) {
char z[100];
x = z;
}
to try to dynamically allocate some space. It didn't work. I wasn't sure that it would work, but I didn't know how else it might work.
I later learned about malloc
and new
, but they really seemed like magical memory generators to me. I knew nothing about how they might work.
Some time later I was being taught recursion again (I'd learned it on my own before, but was in class now) and I asked how it worked under the hood -- where were the separate variables stored. My professor said "on the stack" and lots of things became clear to me. I had heard the term before and had implemented software stacks before. I had heard others refer to "the stack" long before, but had forgotten about it.
Around this time I also realized that using multidimensional arrays in C can get very confusing. I knew how they worked, but they were just so easy to get tangled up in that I decided to try to work around using them whenever I could. I think that the issue here was mostly syntactic (especially passing to or returning them from functions).
Since I was writing C++ for school for the next year or two I got a lot of experience using pointers for data structures. Here I had a new set of troubles -- mixing up pointers. I would have multiple levels of pointers (things like node ***ptr;
) and always tripping over myself. I'd dereference a pointer the wrong number of times and eventually resort to figuring out how many *
I needed by trial and error.
At some point I learned how a program's heap worked (sort of, but good enough that it no longer kept me up at night). I remember reading that if you look a few bytes before the pointer that malloc
on a certain system returns you can see how much data was actually allocated. I realized that the code in malloc
could ask for more memory from the OS and this memory was not part of my executable files. Having a decent working idea of how malloc
works is a really useful.
Soon after this I took an assembly class, which didn't teach me as much about pointers as most programmers probably think. It did get me to think more about what assembly my code might be translated into, though. I had always tried to write efficient code, but now I had a better idea how to.
I also took couple of classes where I had to write some lisp. When writing lisp I wasn't as concerned with efficiency as I was in C. I had very little idea what this code might be translated into if compiled, but I did know that it seemed like using lots of local named symbols (variables) made things a lot easier. At some point I wrote some AVL tree rotation code in a little bit of lisp that I had a very hard time writing in C++ because of pointer issues. I realized that my aversion to what I thought were excess local variables had hindered my ability to write that and several other programs in C++.
I also took a compilers class. While in this class I flipped ahead to the advances material and learned about static single assignment (SSA) and dead variables, which isn't that important except that it taught me that any decent compiler will do a decent job of dealing with variables which are no longer used. I already knew that more variables (including pointers) with correct types and good names would help me keep things straight in my head, but now I also knew that avoiding them for efficiency reasons was even more stupid than my less micro-optimization minded professors told me.
So for me, knowing a good bit about the memory layout of a program helped a lot. Thinking about what my code means, both symbolically and on the hardware, helps me out. Using local pointers that have the correct type helps a lot. I often write code that looks like:
int foo(struct frog * f, int x, int y) {
struct leg * g = f->left_leg;
struct toe * t = g->big_toe;
process(t);
so that if I screw up a pointer type it is very clear by the compiler error what the problem is. If I did:
int foo(struct frog * f, int x, int y) {
process(f->left_leg->big_toe);
and got any pointer type wrong in there the compiler error would be a whole lot more difficult to figure out. I would be tempted to resort to trial and error changes in my frustration, and probably make things worse.