Unlike some languages, it is ineffective to learn C with trial and error. This is because there can be valid C programs that produce no warnings or errors on some or all implementations but may behave differently depending on the compiler, platform, or any arbitrary or random factors. C has the concepts of undefined, unspecified and implementation-defined behavior.

Undefined behavior

Undefined behavior is defined in C11 §3.4.3p1:

No. Consider the following:

behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this International Standard imposes no requirements.

There is then an informative note and example:

NOTE Possible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message).

EXAMPLE An example of undefined behavior is the behavior on integer overflow.

The C standard lists at least 200 different ways undefined behavior can occur. I’m not going to list them all here. Here are a few examples:

// Accessing a pointer which points to an 
// object whose lifetime has expired
// C11 §6.2.4p2    
char *x = malloc(4); 
free(x); 
printf("%p\n", (void *)x); // undefined behavior
    
// Multiple unsequenced side effects on the same object
// C11 §6.5p2
int x = 42; 
x = x++; // undefined behavior
    
// Pointing a pointer beyond one past the end of an array
// C11 §6.5.6p8
int a[3];
*p = a + 4; // undefined behavior
    
// Attempting to modify a string literal
// C11 §6.4.5p7
char *x = "foo";
x[0] = 'b'; // undefined behavior

When undefined behavior occurs, the C implementation is not required to issue any diagnostic message and it is at liberty to do absolutely anything, including making your program appear to do whatever you intended.

Furthermore, undefined behavior renders the entire program undefined, not just the point where the bad code exists. Consider this example from “Defining the undefinedness of C”1:

#include <stdio.h>

int main(void) {
    int r = 0, d = 0;
    for (int i = 0; i < 5; i++) {
        printf("%d\n", i);
        r += 5 / d; // divides by zero
    }
    return r;
}

One might think it is reasonable to assume that the printf statement will be executed at least once and output 0. After all, the undefined behavior is on the line after the printf. However, compilers are permitted to assume that undefined behavior never occurs, as if it did occur, it can do whatever it wants. A common optimization strategy would be to move the 5 / d computation to before the loop as d isn’t changed. If 5 / d causes the program to crash, no printfs will be executed.

Unspecified behavior

Unspecified behavior is defined in C11 §3.4.4:

use of an unspecified value, or other behavior where this International Standard provides two or more possibilities and imposes no further requirements on which is chosen in any instance.

There is then an informative example:

EXAMPLE An example of unspecified behavior is the order in which the arguments to a function are evaluated.

The C standard lists only about 50 ways in which unspecified behavior can occur. Here are a couple of examples:

// Whether two string literals result in distinct arrays
// C11 §6.4.5p7
char *a = "foo";
char *b = "foo";
printf("%d\n", a == b); // unspecified behavior
    
// The order of evaluation of subexpressions that don't
// have a sequence point between them.
// C11 §6.5
f() && g(); // f is called before g as && is a sequence point 
x = f() + g(); // unspecified which function is called first

Unlike undefined behavior, unspecified behavior has a finite number of known possibilities. Which one is chosen by the C implementation in a given instance is unspecified.

Implementation-defined behavior

Implementation-defined behavior is defined in C11 §3.4.1:

unspecified behavior where each implementation documents how the choice is made

There is then an informative example:

EXAMPLE An example of implementation-defined behavior is the propagation of the high-order bit when a signed integer is shifted right.

The C standard lists at least 100 ways in which implementation-defined behavior can occur. Here are a few examples:

// Any non-standard alternative declaration of main
// i.e. other than:
//     int main(void)
//     int main(int argc, char *argv[])
// or equivalent.
// C11 §5.1.2.2.1p1
void main(void) { // potentially allowed if the
                  // implementation documents it
    return 0;
} 
    
// The number of bits in a byte
// C11 §3.6 and §5.2.4.2.1p1
printf("%d\n", CHAR_BIT); // implementation-defined,
                              // must be at least 8
                              
// The values of the members of the
// execution character set
// C11 §5.2.1p1
printf("%d\n", 'a'); // implementation-defined
    
// The result of converting a pointer to an integer
// or vice versa
// C11 §6.3.2.3p5 and §6.3.2.3p6
char y, *x = &y;
int i = (int)x; // implementation-defined and undefined
                // if the result is out of range for int

Summary

When programming in C, it’s very important to at least be familiar with the kind of things that invoke undefined, unspecified or implementation-defined behavior. The C standard has a useful informative list toward the end in Annex J:

  • J.1 Unspecified behavior
  • J.2 Undefined behavior
  • J.3 Implementation-defined behavior

Further reading

References

  1. Ellison, C., & Rosu, G. (2012). Defining the undefinedness of C (pdf).