Curious (ANSI) C Quirks and Oddities

Update: Thursday, 30. April

ANSI C is one of the oldest and most influential programming languages of all time. Despite its efficiency and closeness to the hardware, it harbors numerous pitfalls that can surprise even experienced developers time and again. In this article you will find a comprehensive collection of typical quirks, stumbling blocks, and best practices in C. The goal is to give you a deeper understanding of the language, avoid sources of errors, and write portable, clean code – especially in low-level programming.

Many problems arise from implicit type conversions, undefined behavior, or compiler-dependent implementations. Knowing these peculiarities allows you not only to avoid bugs, but also to make targeted use of the full power of C.

Contents

1. Multiple Dereferencing of Function Pointers

2. Sign Handling of Constants (signed vs. unsigned)

3. Order of Evaluation of Function Arguments

4. Parentheses with Pointers and Increment/Decrement Operators

5. Risk of Errors in Type Conversions

6. Undefined Behavior

7. Dynamic Memory Management – Typical Errors

8. Best Practices for Clean C Code

 

1. Multiple Dereferencing of Function Pointers

Function pointers are a powerful but also error-prone tool in C. Confusion can quickly arise, especially with multiple dereferencing:

 void func(void) { printf("Hello\n"); } void (*fp)(void) = func; 
void (**fpp)(void) = &fp; (*fpp)(); // correct (**fpp)(); // also correct

Both calls work, since function pointers can be automatically dereferenced. Nevertheless, readability suffers greatly. Recommendation: Avoid unnecessarily complex pointer constructions.

 

2. Sign Handling of Constants (signed vs. unsigned)

What output does the following code produce?

 unsigned int a = 1; 
if (a > -1) printf("1 > -1");
else printf("1 <= -1");

The result is surprising: 1 <= -1

Reason: With mixed types, -1 is converted to unsigned int. This produces the largest possible positive number – and that is of course greater than 1.

Note: Mixing signed and unsigned frequently leads to unexpected behavior.

Important Rule

  • Operations involving unsigned are usually computed in unsigned
  • Negative values can become very large positive numbers

 

3. Order of Evaluation of Function Arguments

The order in which function arguments are evaluated is not defined in C:

 int i = 1; printf("%d %d\n", i, i++); 

The result is undefined and may vary depending on the compiler.

Recommendation: Avoid side effects within function arguments.

 

4. Parentheses with Pointers and Increment/Decrement Operators

Small differences in parenthesization have major effects:

 char *lp = line; ch = *lp++; // equivalent to *(lp++) ch = (*lp)++; // increments the value, not the pointer 

Note: Operator precedences in C are complex. When in doubt, always use parentheses!

 

5. Risk of Errors in Type Conversions

The behavior of char is implementation-dependent:

 char a = 228; printf("%d\n", a); 

Output can be:

  • 228 (unsigned char)
  • -28 (signed char)

Recommendation:

  • Explicitly use signed char or unsigned char
  • Pay attention to compiler flags (-fsigned-char, -funsigned-char)

 

6. Undefined Behavior

A central concept in C is Undefined Behavior. Examples:

 int x = 5; x = x++; // undefined 

Or:

 int a[3] = {1,2,3}; printf("%d\n", a[5]); // access outside the array 

Such code can:

  • appear to work
  • crash
  • produce unpredictable results

 

7. Dynamic Memory Management – Typical Errors

A common mistake is incorrect handling of malloc and free:

 int *p = malloc(sizeof(int) * 10); free(p); free(p); // error: double free 

Other typical problems:

  • Memory leaks (no free)
  • Dangling pointer (access after free)
  • Forgetting NULL checks

 

8. Best Practices for Clean C Code

  • Use explicit types: clearly define signed/unsigned
  • Use parentheses: for better readability and safety
  • Avoid side effects: especially in complex expressions
  • Enable compiler warnings: -Wall -Wextra
  • Keep code portable: no compiler-dependent tricks
  • Manage memory cleanly: use malloc/free consistently

 

Conclusion

C is an extremely powerful language – but it is precisely this closeness to the hardware that also makes it susceptible to subtle errors. Knowing the typical quirks and pitfalls allows you not only to write more stable code, but also to take full advantage of C.

Do you know any other peculiarities or typical errors in C? Feel free to share them in the comments and expand this collection!

Comments 0

 

Write new comment: