When writing for loops in C, the initialization clause often holds more power than developers realize. While it's common to initialize multiple loop counters using commas, subtle differences in syntax and operator precedence can lead to confusing results.

Take these two seemingly similar loop initializations, which produce vastly different behaviors:

// Example 1: Outputs "hello" followed by 0123456789
int i;
for(i = 0, printf("hello\n"); i < 10; i++) {
    printf("%d", i);
}

// Example 2: Outputs "hello" followed by 6789
for(int i = (0, printf("hello\n")); i < 10; i++) {
    printf("%d", i);
}

Why does the first loop start counting at 0, while the second starts at 6? The answer comes down to two key C fundamentals: operator precedence and the return value of printf.

1. Operator Precedence: Assignment (=) vs. Comma (,)

In C, the comma operator (,) has the lowest precedence of all C operators—even lower than the assignment operator (=).

When you write:

i = 0, printf("hello\n");

Because assignment has higher precedence than the comma operator, the compiler groups this expression as:

(i = 0), printf("hello\n");

Here is step-by-step what happens during the loop's initialization:

  • Step 1: The expression i = 0 evaluates first. i is set to 0.
  • Step 2: The comma operator causes the result of i = 0 to be discarded.
  • Step 3: The second expression, printf("hello\n"), executes and prints hello. Its return value is also discarded because nothing captures it.
  • Step 4: The loop condition i < 10 is checked. Since i is 0, the loop runs from 0 through 9, printing 0123456789.

2. Why Did the Parenthesized Version Start at 6?

In the second example, enclosing the expression in parentheses changes everything:

int i = (0, printf("hello\n"));

By placing parentheses around 0, printf("hello\n"), you force the comma operator to evaluate first before assigning the final result to i.

  • The expression 0 is evaluated and discarded.
  • The function printf("hello\n") is called. It prints hello to the console.
  • In C, printf returns the total number of characters printed. The string "hello\n" consists of 6 characters (h, e, l, l, o, and the newline \n).
  • The return value of the entire comma expression is therefore 6.
  • Finally, int i = 6; initializes i to 6.

Because i starts at 6, the loop only iterates for 6, 7, 8, 9, printing 6789.

3. Comma as an Operator vs. Comma as a Separator

Another common source of confusion is distinguishing between the comma operator and the comma separator:

  • Comma Separator: In declarations (such as int a = 1, b = 2;) or function arguments (such as foo(x, y)), the comma is simply syntactic punctuation. It tells the compiler to expect another identifier or argument. That is why int i = printf("hello"), 0; produces a compiler error—the compiler expects a variable name after the comma, not a literal 0.
  • Comma Operator: When used in an expression context (like the loop initialization in Example 1, or wrapped in parentheses in Example 2), the comma acts as an operator that guarantees left-to-right evaluation and yields the value of the rightmost operand.

Best Practices

While utilizing the comma operator inside a for loop's header is valid C, using it for unrelated side effects (like calling printf) harms code readability. Standard practice recommends:

  • Limiting the comma operator in for loops to coordinating multiple loop index variables:
    for (int i = 0, j = 10; i < j; i++, j--) { ... }
  • Keeping setup calls, such as logging or printing, on their own lines before the loop begins:
    printf("hello\n");
    for (int i = 0; i < 10; i++) { ... }

Summary

The code i = 0, printf("hello\n") sets i to 0 because the assignment operator takes precedence over the comma operator. The loop initialization runs once, prints hello, and allows the loop to iterate from 0 to 9 uninterrupted.