C Without Crying in the Terminal — part 1 of 1
Functions and pointers in C
In this post, I am going to cover a topic that I honestly have never seen discussed on YouTube or in courses, so the idea here is to first explain what you need to know beforehand and then get into the subject.
6 min read
available in:ENPT
Functions and Pointers#
In this post, I am going to cover a topic that I honestly have never seen discussed on YouTube or in courses, so the idea here is to first explain what you need to know beforehand and then get into the subject.
What do I need to know to keep reading?#
- You need to understand what a pointer is and what its purpose is.
- You need to understand the basics of functions in C.
- You need to know what casting and a prototype are.
- Basic knowledge of C.
A brief introduction to pointers#
Pointers are like arrows: they tell you where something is — in this case, an address in memory.
What many people fail to notice, and end up getting confused about when dealing with pointers, is the following.
A pointer usually appears in a declaration like this:
int y = 10;
int *x = &y;Important insight: x is still a normal variable — what changes is its type. x is a variable of type "pointer to int", and the value it stores is an address (the address of y, obtained with &y). When you write *x (the dereference), you do not read the value of x itself, but rather the value that is at that address — in other words, the value of y.
In summary:
x→ the stored address (where the arrow points).*x→ the value stored at that address (the10fromy).
This is the key insight that helps you avoid getting confused about pointers.
Even so, this is still a simple concept, because from here we could go further and talk about multiple indirection (pointer to pointer), and so on.
Anatomy of a function in C#
A classic function in C is defined by: the storage class (whether it is static or not), the return type, the name, the parameters, and the block (the body). C is a block-structured language, which allows local variables to be declared inside blocks — and, in some compilers (as a GCC extension), even nested functions, although that is not part of standard C.
int main(void) {
return 0;
}The most common example is the main function itself, the entry point of every C program.
A function's name and what happens during a call#
Here is a detail that confuses many people: in C, a function's name almost always "becomes" a pointer to it. Whenever you use a function's name inside an expression — except when it is the operand of & or sizeof — the compiler automatically converts that name into a pointer to the function. This behavior has a name: function-to-pointer decay.
Because of this, later on we will be able to assign a function to a pointer without writing anything special. This is also why f, &f, and the calls f(), (*f)(), and even (**f)() all work: in the end, they all refer to the same function entry address.
When you actually call a function, the program jumps to its entry address and creates a stack frame (activation record) on the stack. This frame stores what that call needs: the return address (where execution should go back when the function finishes), the arguments, and the local variables. The return value usually comes back through a register rather than through the stack, but this depends on the calling convention and the architecture.
Calling a function through a pointer#
#include <stdio.h>
int count_girlfriends(void) {
return -1;
}
int main(void) {
// Let us create a pointer to hold this function.
int (*cg)(void) = count_girlfriends;
// cg -> the variable name (a function pointer); cg points to the entry point of count_girlfriends.
// The * in (*cg) is what marks "this is a pointer"; we write (*cg) to mirror the form of the call (*cg)().
// (void) -> this is the "signature" (the prototype) we chose: this function receives no parameters.
// int -> at the beginning, this is the return type.
printf("Return [%d]\n", cg());
return 0;
}Notice that I wrote count_girlfriends(void), not count_girlfriends(). This matters: in C, an empty () in a declaration means "I am not specifying which parameters or how many parameters there are" (the old K&R style), and this disables the compiler's argument checking. (void), on the other hand, explicitly means "zero parameters" and allows the compiler to protect you. Since we are specifically talking about a prototype here, whenever a function receives nothing, we will use (void). (In C23, this rule changed and an empty () came to mean (void), but using (void) remains the safest and most portable habit.)
A practical consequence of the decay we saw earlier: all of these forms are equivalent and print the same value.
int (*cg)(void) = count_girlfriends; // or &count_girlfriends — they are the same
printf("%d\n", cg()); // direct form
printf("%d\n", (*cg)()); // dereferencing "by hand" — same resultThis is the standard example of how to pass a function's "entry point" to a pointer.
Pointer to a function that returns a function#
In C, we have two ways to write a function that returns another function — or, more precisely, a pointer to a function, since you do not return the function itself, but rather its address.
One of these forms causes headaches, while the other makes the code much cleaner. The clean version (using typedef) comes right next; the more cluttered, "by hand" version has a section of its own later on ("Writing functions in a classic and strange way").
It is worth understanding the cluttered form because it forces you to see more clearly the patterns of how C handles pointers (and the order in which the declaration is "read"). However, for everyday code, always prefer the simpler option. Before getting there, in addition to the clean form, this section shows two closely related and equally useful uses of function pointers: receiving them as parameters (the callback) and storing them in a table.
1. A clean way to return a function in C#
#include <stdio.h>
// We create an alias (typedef) for a "function pointer" type.
// We will use it both as a return type and as a parameter type.
typedef int (*calc_action)(int a, int b);
// Our target function.
int sum(int a, int b) {
return a + b;
}
// calc receives nothing and returns a calc_action (a function pointer).
calc_action calc(void) {
// Returning our target function (the name "sum" decays into a pointer on its own).
return sum;
}
int main(void) {
// calc() returns the function; the second pair of parentheses calls it immediately.
printf("Sum: [%d]\n", calc()(1, 1));
// You can also store the return value in a variable, because the typedef
// works for variables as well as parameters.
// That is why this is the clean approach you find in modern libraries.
calc_action c2 = calc();
printf("Sum: [%d]\n", c2(2, 2));
return 0;
}Output:
Sum: [2]
Sum: [4]2. Receiving functions as parameters in C#
#include <stdio.h>
// The same typedef as before: a pointer to a function that receives (int, int) and returns int.
typedef int (*calc_action)(int a, int b);
// calc receives a, b, and a function (ca) that decides which operation to apply to a and b.
int calc(int a, int b, calc_action ca) {
return ca(a, b);
}
// Our addition function.
int sum(int a, int b) {
return a + b;
}
// Our subtraction function.
int sub(int a, int b) {
return a - b;
}
int main(void) {
// Calling calc and passing functions as arguments.
int rsum = calc(1, 1, sum);
int rsub = calc(1, 1, sub);
printf("rsum: %d, rsub: %d\n", rsum, rsub);
return 0;
}Output:
rsum: 2, rsub: 0Passing a function as an argument like this (the famous callback) is exactly what standard library functions such as qsort do: you provide the "rule" (how to compare two elements), and the generic algorithm does the rest without requiring you to rewrite anything.
3. Bonus: a function table#
Because calc_action is a type like any other, you can even create an array of function pointers and choose the operation by index — a very common pattern for replacing long sequences of if/switch statements.
#include <stdio.h>
typedef int (*calc_action)(int a, int b);
int sum(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int main(void) {
// Each position in the array stores a pointer to an operation.
calc_action ops[] = { sum, sub, mul };
const char *names[] = { "sum", "sub", "mul" };
for (int i = 0; i < 3; i++) {
printf("%s(6, 2) = %d\n", names[i], ops[i](6, 2));
}
return 0;
}Output:
sum(6, 2) = 8
sub(6, 2) = 4
mul(6, 2) = 12As you may have noticed, you can use this in several ways in C while keeping things very clear — and much less confusing than writing the pointer type "by hand" every time.
Writing functions in a classic and strange way#
This is the "headache" way of returning a function that I mentioned earlier: writing it "by hand", without a typedef.
It is very likely that, if you look at old C libraries, you will find function signatures written in very different ways. So let us describe a few of them.
#include <stdio.h>
int sum(int a, int b) {
return a + b;
}
// Let us write a function "p" that returns a function.
// The tip for reading the declaration is to start with the name and move from the inside out:
//
// int ( *p(void) )(int, int)
// ------- -> p is a function that receives (void)...
// ( * ) -> ...and returns a pointer...
// int (int, int) -> ...to a function that receives (int, int) and returns int.
int (*p(void))(int, int) {
return sum;
}
int main(void) {
// p() returns the sum function; the second pair of parentheses calls sum(1, 1).
printf("%d\n", p()(1, 1));
return 0;
}Writing a function that returns a function that returns a function#
At this point it becomes a major headache, but the idea remains the same: a somewhat "recursive" syntax built in layers.
#include <stdio.h>
int sig(void) {
return -1;
}
// p2 receives (int, int) and returns a pointer to a function (void) -> int.
int (*p2(int a, int b))(void) {
(void)a; // a and b are not used here; this cast to void only suppresses the "unused parameter" warning.
(void)b;
return sig;
}
// p receives (void) and returns a pointer to a function like p2,
// that is: a function (int, int) that returns a pointer to a function (void) -> int.
int (*(*p(void))(int, int))(void) {
return p2;
}
int main(void) {
// p() -> returns p2
// p()(1, 1) -> calls p2, which returns sig
// p()(1, 1)() -> calls sig, which returns -1
printf("%d\n", p()(1, 1)());
return 0;
}Writing a function with a function parameter#
To finish, here is the most direct and common form: a parameter that is a function pointer, written "by hand" (without a typedef).
#include <stdio.h>
int sum(int a, int b) {
return a + b;
}
// int (*p)(int, int) is the parameter: a pointer to a function (int, int) -> int.
int calc(int a, int b, int (*p)(int, int)) {
return p(a, b);
}
int main(void) {
printf("%d\n", calc(1, 1, sum));
return 0;
}Compare this with Section 2: it is exactly the same idea, except that there the calc_action typedef hid this int (*p)(int, int) behind a friendlier name.
Well, if you practice and try to understand how each piece fits together, you will develop a good understanding of how to use function pointers — in these and many other forms.