For the sake of code golf, you first need to decide if you wish to compete in strictly conforming ("real") C, in which case you can't rely on non-standard extensions, poorly-defined behavior or obsolete features.
Or you can compete in non-standard extensions (gcc/GNU etc), when whatever binary the compiler lets through with default settings is fair game - including those that may be abusing poorly-defined behavior.
In strictly conforming hosted C, the form
int main() is the most compact. Empty parenthesis is obsolete style but it has not yet been formally withdrawn. Alternatively two parameters can be used, normally named argv and argc though C allows us to rename them. In conforming C, we may not change their types however, so the most compact conforming version is:
int main(int c,char**v)
(Or use compatible types.)
main(c,v) is however not conforming.
In conforming freestanding C, other forms of main() may be used, but that's for embedded systems and not likely applicable for code golf.
In non-conforming/gcc extensions, old C90 style
return 0; is no longer necessary in main() as from C99 - omitting it is well-defined.
Format of functions. Non-conforming K&R style functions are still supported by most compilers, for example:
f(a,b)int a;char b;
This can be handy when you need to declare parameters and local variables with the same type.
Abuse static storage duration. Variables declared at file scope have static storage duration and are therefore guaranteed by the standard to be zero-initialized, saving the need to do such in code.
Abuse C90 implicit int. In C90 and non-conforming gcc extensions you can declare a variable as
x; and you get an
int. Similarly, in C90 functions that declare no return type were assumed to return
Omit include files. While not allowed in strict C, non-standard gcc allows to omit all
Always use for loops.
for(;;) is the most compact form. The first clause can be used for variable declarations, which is handy since you have to leave a semicolon there anyway. The 2nd and 3rd clauses can contain pretty much any code, such as a long list separated by
, operator. In many cases you can place the whole program there and then end the line with
;, no need for a loop body with
Use recursion instead of loops. Recursion can save a lot of overhead code, in case the equivalent loop has a suitable exit condition that needs to be there no matter.
?: instead of if-else. It is far more compact and can be used to write long nested expressions in compact ways.
&& short circuit instead of if. For example
if(foo)bar(); can often be written as
Mix ++ and -- with other expressions. Contrary to good programming practice, this can save characters. For example
for(;;i--)printf("%d",a[i]); could be written as
for(;;)printf("%d",a[i--]);. Experiment with prefix and postfix versions too. Big chance that you end up with a non-conforming solution though.
! is generally handy, for example when converting a non-zero value to 1, or to get boolean 1/0 out of expressions.
Use bitwise operators when possible.
|| can sometimes be replaced with
^ xor instead of
sizeof doesn't need parenthesis if the operand is an expression. Don't do
sizeof(expr), always do
Pointer arithmetic instead of array indexing.
# pre-processor stringification. If you can afford a
#define then everything inside it can be turned to string literals, which is handy if you have to repeat a lot of strings. Pre-processor string concatenation is also handy:
"foo" "bar" will create "foobar".
int can often be used instead of
char for storing characters.
Integer constants are more effective than character ones. Use
char ch=32; not
char ch=' '.
Use binary arithmetic instead of larger numbers.
~0 instead of
1<<30 instead of 0x40000000. And so on.
X-macros are handy for avoiding repetition. Particularly when listing data. One example here: https://codegolf.codidact.com/posts/279820/279856#answer-279856
Abuse the poor type system and endianess when printing strings. Wildly non-conforming:
int x=65; puts(&x); This prints the null terminated string
"A", since puts treats the raw binary on a 32 bit int little endian machine as hex
41 00 00 00. This can also be handy for integer to string conversions where you already have the integer in memory.
puts when possible. It is compact and saves you from printf with the burdensome format string. It also prints new line implicitly. And I've seen some other clever uses like
malloc(puts(s)) instead of
Use VLA. Avoid malloc, use VLA instead.
int x[n]; beats
Use evil non-standard libs. Some compilers support
putch and similar compact functions as non-standard extensions without the need to include special headers.