Complex Pointers in C Made Easier
Requirements
Here’s what I’m assuming you can understand: a declaration int *x;
initializes a variable x
to have the value of a pointer that (in this case) points to an address in memory; at that address there is space allocated for an int
(how much that is in actual bits depends on the implementation). So we say the type of x
is pointer to an integer. Are we on the same page? If not, I recommend this reading and practice. Any disagreement, let me know.
You think you know things
That out of the way, could you tell me what the type of the next x
is?
int (*(*(*x)[])(float, float))(char *, int **);
I'll let you take your time.
The astute reader might ask: why the heck would I ever want to know this? And the answer might vary. One good reason is for the sake of the challenge. It seems hard, and you want to prove to yourself that you're capable. Other compelling case is if you have an exam in computer science that will require you to learn it. Let's look at a practical (albeit admittedly contrived) example:
enum cmds { M, A, D, S };
// maps a character to a value in enum cmds
int hash(const char choice);
int mult(int, int);
int add(int, int);
int divi(int, int);
int sub(int, int);
int main(void)
{
int (*operations[])(int, int) = { [M] = mult,
[A] = add,
[D] = divi,
[S] = sub };
char choice = getchar();
int x, y;
scanf(" %d %d", &x. &y);
printf("%d", operations[hash(choice)](x, y));
}
The idea is that when we run this program and pass, for instance, m 2 4
to stdin, it should print 8
. Or d 5 2
, print 2
. The line
printf("%d", operations[hash(choice)](x, y));
can only be agnostic about what function was chosen due to the nature of the variable operations
. It is an array of pointers to functions that take two integers and return an integer. You can read that as an array of... operations (binary, integer operations, to be more precise).
That is a simpler case of complex pointer declaration than the one I've shown you before, but C is a powerful language, so you can rest assured it will let you overuse its capabilities. A third motivation to learn the most complex forms I'll mention, is so that the easier ones, when they become useful, come more naturally to you.
The Goods
The parsing of complex pointer declarations follow two simple rules:
Move from the identifier out
Favor arrays [ ] and functions () over pointers * in the definition
Let's go back to the operations
example:
int (*operations[])(int, int);
Rule #1, we start at &operations
Sill from rule #1, we should move out. Either to
*
or to[]
. Following rule #2, we go to[]
, &arrayAll that is left inside the parentheses, respecting rule #1, is the
*
, so &pointerOutside of that, a
()
&function taking two integers and returning an integer (on the left)
Notice where I italicized and put an &. We use it to describe the declaration: operations is an array of pointers to functions taking two integers and returning an integer. You can check I'm not crazy and wrote the same thing up there (dyslexic folks catching strays, sorry).
Try to visualize these steps. If you look at the symbols describing the types, your eyes will be zig-zagging. Now let me show you what I see, and in what order, when I do this back and forth:
[] * (int, int) -> int
(no, it's not Haskell)
Writing that was easy. Interpreting what it means as well. Try it. You should arrive at the definition of operations
.
Chances are you forgot the first example I gave. I did. Try to represent, either mentally or in a text editor the order of types/symbols in the definition of its type:
int (*(*(*x)[])(float, float))(char *, int **);
The answer is below
* [] * (float, float) -> * (* char, ** int) -> int
If it isn't obvious already, x
is a pointer to an array of pointers to a function that take in two floats and returns a pointer to a function that takes in a pointer to a char and a pointer to a pointer to an integer and returns an integer.
Renaming things
Other than just understanding this madness, you can try to make this Clean Code™ or whatever. Let's grab the operations
one last time
int (*operations[])(int, int);
Now instead of going from the inside out, we are going the only other direction available.
int ()(int, int);
It's a function, taking two ints and returning an int. Let's define a type for that:
typedef int Fcn(int, int);
Inside the parentheses we have:
*operations[]
Since we're going in the reverse direction, we'll look first at the pointer (not the array, as we would before) and define a type for it, using the prior type:
typedef Fcn *Fcn_ptr;
Are you following? We used Fcn *
to say "function pointer" and defined the fitting name Fcn_ptr
for that. And we're left with:
operations[]
We can agree it's an array. Array of what? Well, an array of the previous stuff: function pointers. Let's see the typedef:
typedef Fcn_ptr Fcn_ptr_arr[];
Hear the sound of that: "function pointer array". It's semantically equivalent to "array of pointers to function" and it's exactly what we have. We can declare operations
with that type.
typedef int Fcn(int, int);
typedef Fcn *Fcn_ptr;
typedef Fcn_ptr Fcn_ptr_arr[];
Fcn_ptr_arr operations = {[M] = mult...};
Epilogue
If you haven't already, take some time to recreate in your mind what I shared in the article. What can you remember? What could you explain? What is the use of complex pointer declarations? How does the process of using typedef
to alias it works? Make sure to always think for yourself.
Let me know your ideas about C and the pointer syntax, what you're struggling to understand, some word of gratitude to make my mom proud or anything in between.
Subscribe to my newsletter
Read articles from João Gabriel directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by