Conditionals
C’s conditional constructs are spare: if/else, the conditional operator ?:, and switch. The language has no pattern-matching, no unless, no Python-style chained comparisons, no Ruby-style guard clauses; what is available is the C ancestral form of branching that has been broadly imitated. The brevity of the surface, combined with the language’s quiet implicit conversions, accounts for several of the recurring conditional-related defects: assignment masquerading as comparison, sloppy treatment of bool-like values, switches that fall through unintentionally.
if and else
The if statement evaluates its controlling expression and executes the body if the result compares unequal to zero:
if (n > 0) {
process(n);
}
The body may be a single statement or a compound statement (block). The convention to always use braces, even for single-statement bodies, is widespread and defensible: it eliminates the class of bug introduced by adding a second statement and forgetting to add the braces, and it makes diff output cleaner.
The optional else clause attaches to the immediately preceding if:
if (x > 0) {
classify_positive(x);
} else if (x < 0) {
classify_negative(x);
} else {
classify_zero();
}
C does not have elif as a keyword; else if is two keywords, and the parser treats it as else { if … } with the braces elided. The cascading style above is the conventional way to write multi-way conditionals; a switch would be appropriate only if the discriminant were equality against integer constants.
Truthy and falsy
The controlling expression of if need not be of Boolean type. Any scalar value is acceptable; zero is treated as false, non-zero as true. The convention applies to integers, floating-point values, and pointers:
if (count) /* count != 0 */
if (p) /* p != NULL */
if (strchr(s, c)) /* not NULL — found */
The construction is concise and idiomatic, but it can obscure the type of the test. A few stylistic conventions help:
- Compare integers against zero explicitly when the value’s role is numeric:
if (count == 0). - Compare pointers against
NULLexplicitly when the value’s role is “presence or absence”:if (p == NULL). - Use the implicit form when the value’s role is a flag or a found-or-not return.
The = versus == trap
C’s expression-oriented design makes assignment a value-yielding operation, which combines with if’s flexibility about scalars to produce a classic bug:
if (x = 5) { … } /* assigns 5 to x and tests the result (always true) */
The intent was almost certainly x == 5. Modern compilers warn about the construction by default (-Wparentheses in GCC and Clang), and the conventional defence — beyond enabling the warning — is to wrap intended-assignment-in-conditional in extra parentheses:
if ((c = getc(fp)) != EOF) { … } /* explicit; suppresses the warning */
The pattern of capturing a value and testing it in one expression is genuinely useful for I/O loops; the parenthesisation is the convention that distinguishes it from the typo.
The conditional operator
The ternary operator ?: selects between two expressions:
int max = a > b ? a : b;
const char *plural = n == 1 ? "" : "s";
The operator is an expression (unlike if), which makes it composable in ways if is not. The principal uses are short selections inside larger expressions:
printf("%d item%s\n", n, n == 1 ? "" : "s");
The two result expressions are subject to the usual arithmetic conversions if both are arithmetic, and a more elaborate compatibility rule if either is a pointer. Mixing types — cond ? 1 : "two" — is a type error.
Nested conditionals are syntactically legal but quickly become unreadable. Two levels are usually the practical limit before refactoring to if/else.
switch
The switch statement compares its controlling expression against integer constants:
switch (op) {
case '+': result = a + b; break;
case '-': result = a - b; break;
case '*': result = a * b; break;
case '/':
if (b == 0) { return -1; }
result = a / b;
break;
default:
return -1;
}
The standard requires the controlling expression to have integer type (after the integer promotions); strings and floating-point values are not switchable. Each case label is followed by an integer constant expression: a literal, an enumerator, or a constant computation; non-constant expressions are not permitted.
Fallthrough
C’s switch does not implicitly break between cases. Execution enters at the matched label and continues to the next statement, which may be the body of the next case:
switch (c) {
case 'a':
case 'A':
handle_a();
break;
case 'b':
case 'B':
handle_b();
break;
}
The pattern of stacked labels with no body — case 'a': followed by case 'A': — is a deliberate, idiomatic use of fallthrough: the matched label simply continues to the next, which is the body shared by both.
Implicit fallthrough between substantive cases is a frequent source of bugs:
switch (state) {
case STATE_IDLE:
prepare(); /* missing break — falls through to STATE_RUNNING */
case STATE_RUNNING:
execute();
break;
}
Modern compilers warn (-Wimplicit-fallthrough in GCC and Clang). C23 standardised the [[fallthrough]] attribute, with which the programmer can mark intentional fallthrough explicitly:
switch (state) {
case STATE_IDLE:
prepare();
[[fallthrough]]; /* deliberate */
case STATE_RUNNING:
execute();
break;
}
The combination of -Werror=implicit-fallthrough plus [[fallthrough]] for intentional cases catches the bug at compile time.
default
The default label matches any value not covered by an explicit case. It need not appear last syntactically, although by convention it is placed at the end. If default is omitted and no case matches, the entire switch is a no-op.
When the discriminant is an enumerated type, omitting default and listing every enumerator is a useful pattern: a compiler with -Wswitch will diagnose newly added enumerators that the switch does not handle.
enum colour { RED, GREEN, BLUE };
const char *colour_name(enum colour c) {
switch (c) {
case RED: return "red";
case GREEN: return "green";
case BLUE: return "blue";
/* no default: -Wswitch will flag a new enumerator */
}
return "unknown";
}
Labelled blocks
Each case body may declare local variables; the lifetime is the enclosing switch block. Code declaring substantial local state typically wraps the case body in braces to scope the declarations:
switch (token.kind) {
case TOKEN_NUMBER: {
int value = parse_number(token.text);
emit_constant(value);
break;
}
case TOKEN_NAME: {
int symbol = lookup_symbol(token.text);
emit_load(symbol);
break;
}
}
The scoping is required if a case body declares a variable with an initialiser; without the braces, the declaration would be reachable by jumping directly to a later case, which the standard treats as undefined.
Selection idioms
Several patterns recur in idiomatic C.
The early-return guard
When a function has preconditions, the conventional form is to check them at the top and return early:
int parse(const char *input, parsed *out) {
if (input == NULL) return -1;
if (out == NULL) return -1;
if (*input == '\0') return 0;
/* main body */
}
The alternative — nesting the body inside if (input != NULL && out != NULL && *input != '\0') { … } — is harder to read and produces deeper indentation.
The goto-based cleanup
A function with multiple acquisition steps and corresponding cleanup is conventionally written with goto:
int do_work(void) {
FILE *fp = fopen("file", "r");
if (!fp) return -1;
char *buf = malloc(BUF_SIZE);
if (!buf) goto cleanup_fp;
int *table = calloc(N, sizeof *table);
if (!table) goto cleanup_buf;
/* … work … */
int result = 0;
free(table);
cleanup_buf:
free(buf);
cleanup_fp:
fclose(fp);
return result;
}
The goto-based form is the conventional C alternative to RAII; it keeps the cleanup logic in linear order with the acquisitions, and the labels match against the resources still held when each error path is taken. The construction is treated more fully in Error handling.
Lookup tables
When a multi-way conditional discriminates on a small integer, a lookup table is often clearer and faster than a switch:
static const char *day_name[] = {
"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday",
};
const char *name(int day) {
if (day < 0 || day > 6) return "?";
return day_name[day];
}
The pattern is principally useful when the cases are both numerous and uniform. For cases that perform different actions, switch remains the conventional form.