They wonât be able to find bugs in your code if they canât figure out how it works.
In the previous installment of our introductory series about the C language, we outlined the basic syntax of the switch() statement ( demo link ):
#include <stdio.h>int main() { int i = 1; switch (i) if (0) case 0: puts("i = 0"); else if (0) case 1 ... 10: puts("i = 1 ... 10"); else if (0) case 11: puts("i = 11"); else if (0) default: puts("i = something else"); return 0;}
Today, letâs continue with function declarations. You will be delighted to discover that the following code compiles cleanly with gcc - Wall â and that it calls puts() exactly once ( link ):
int typedef puts(char* puts; char* puts; char* puts);int main() { puts(puts); puts("Welcome to my humble abode!");}
The explanation is simple. The first line is not actually a function declaration: instead, because of the buried (and out-of-order) typedef keyword, it defines a type. More specifically, it defines a function type â a fairly useless but permitted construct in C that shouldnât be confused with the more practical function pointer type. In the end, we create a type named puts that can be used to declare a function according to the template of int (âŠ).
The second mystery might be whatâs going on with the parameters â char* puts; char* puts; char* puts. This syntax is an obscure GNU extension called a forward parameter declaration. The intended use is this:
int myfunction(int len; char data[len], int len) { ...}
In essence, itâs a way to tell the compiler that a parameter of a certain type and name is forthcoming, so that we can reference it ahead of the time. You can apparently have as many semicolon-delimited forward declarations as youâd like, but in the end, weâre just creating a type for int (char*). At the typedef stage, parameter names are ignored and have no global effects, so repeating puts in there is just for show.
Past this point, puts is a type, and we canât redefine the symbol in the global scope; that said, the language permits symbols to be shadowed within nested blocks ( link ):
char* foo = "bar";int main() { int foo = 123; /* No error */ return foo;}
âŠless intuitively, the same also applies to types ( link ):
typedef float foo;int main() { int foo = 123; /* No error */ return foo;}
The final piece of the puzzle is the observation that parentheses can be added in variable declarations with no ill effects ( link ):
int main() { int (foo) = 123; return foo;}
This brings us back to what the puts(puts) line in main() actually does: it is not a function call at all! Instead, itâs equivalent to â puts putsâ â a declaration of a function named puts that follows the int (char*) type template. Critically, this newly-instantiated puts symbol clobbers the global type, so the next reference to puts is a real function call.
All right, all right â too easy! Letâs move on to control flow ( link ):
#include <stdio.h>typedef int _();int main() { puts("Welcome to my humble program."); _ main asm("_");}int z() { puts("ANYTHING IS POSSIBLE AT ZOMBO.COM"); return 0; _ z asm("main");}
If you run this code, the only output will be an ad for zombo.com. Why? For one, we have another function typedef in there â but if we get rid of it for clarity, the code still doesnât make much sense ( link ):
...int main() { puts("Welcome to my humble program."); int main() asm("_");}...
The other trick is the asm(ââŠâ) syntax. Itâs not actually an assembly block; when the keyword appears in a function or variable declaration, it specifies an underlying âassembler nameâ for the C symbol. Youâd normally use it like so ( link ):
int foobar(char*) asm("puts");int main() { foobar("Hello world"); return 0;}
In our earlier example, we attach the renaming directive to a local declaration of main() that shadows the global symbol â but the result of the renaming is global! In effect, we renamed main() to _ () and z() to main(). Clang complains, but GCC doesnât mind at all.
Letâs follow that trail for a bit longer. Check out the following code ( link ):
int main() { i("This is fine."); return 0;}[[gnu::unused]] void elsewhere() { typedef int i(); for (i i, i asm("puts"), i;;);}
The code prints âthis is fineâ. Recent versions of GCC allow the iterator variable of a for() loop to be a function declaration, so we can bury the renaming there!
I donât quite know why this for () syntax is now allowed, but it gets even more wacky if we trade a function declaration for a fully-fledged definition ( link ):
#include <stdio.h>int main() { int i = 0; for (void _(){} i++ < 3; ) _(), puts("this is fine."); return 0;}
A two-expression for()?! Sort of! Three independent elements are still expected, but a function definition ( void _() { } ) is special in C in that it doesnât end with a semicolon. In other words, for (; ) is âcorrectâ, while for (; ; ) would be not.
While I have you here, did you know that the C language has a BASIC compatibility mode that enables line numbering? Itâs true, use if (BASIC) to see if your compiler supports the feature ( link ):
#include <stdio.h>typedef int BASIC[];int main() { if ((BASIC) { [20] puts("cruel"), [30] puts("world"), [10] puts("hello"), });}
Iâm sure you can figure this one out. And with this, we conclude todayâs introductory lesson of C. Until next time, fellow programmers!
I write well-researched, original articles about geek culture, algorithms, and more. If you like the content, please subscribe. Itâs increasingly difficult to stay in touch with readers via social media; my typical post on X is shown to less than 5% of my followers and gets a ~0.2% clickthrough rate.