C Part I · Module 4 · Decisions and Control Flow
The Conditional Operator
The previous chapter ended on a limitation rather than an achievement. An if statement selects which statements run, which means it chooses what the program does — but it is a statement itself, so it cannot be used anywhere a value is required. You cannot pass an if to printf, and you cannot initialise a variable with one. C has exactly one operator that makes a choice and hands back a value, and this chapter is about it. It is often introduced as a compact way to write an if/else, which undersells it and encourages the one habit that makes it unreadable. The useful way to see it is as the expression form of a decision, with a result type that is decided by both of its branches together even though only one of them ever runs.
Foundation14 min readCPart IFundamentalsControl Flow
Part I · Module 4 · Chapter 4.2 · The Conditional Operator
1. Choosing a Value, Not a Path
Suppose you want the larger of two numbers. With Chapter 4.1's tools:
#include <stdio.h>
int main(void)
{
int a = 17;
int b = 42;
int larger;
if (a > b) {
larger = a;
} else {
larger = b;
}
printf("%d\n", larger);
return 0;
}Output:
42This works, and there is nothing wrong with it. But look at what it had to do. The variable larger has to be declared first, with no value, and then assigned in one of two places. The decision and the thing being decided are in different statements. There are four lines of machinery around one idea.
The conditional operator expresses the same idea as a single expression:
int larger = (a > b) ? a : b;Read it as: evaluate a > b; if that is true the whole expression is a, otherwise it is b.
Both versions print 42. The difference is not length. It is that the second version produces a value, and a value can go places a statement cannot.
2. The Form and the Rule
condition ? value-if-true : value-if-falseIt is C's only operator taking three operands, which is why you will also hear it called the ternary operator. That name describes its arity and tells you nothing about what it does, so this chapter uses conditional operator, which is the name the standard uses.
The rule:
The first operand is evaluated and compared with zero, by the same rule Chapter 2.10 established. If it is non-zero the second operand is evaluated and becomes the result. Otherwise the third operand is evaluated and becomes the result. Exactly one of the second and third operands is evaluated.
That last sentence should sound familiar. Chapter 3.6 made the same promise about && and ||, and it is provable in the same way — put a side effect in each branch and see which object changed:
#include <stdio.h>
int main(void)
{
int taken = 0;
int not_taken = 0;
int result = (1 > 0) ? (taken = 5) : (not_taken = 9);
printf("result %d, taken %d, not_taken %d\n", result, taken, not_taken);
return 0;
}Output:
result 5, taken 5, not_taken 0not_taken still holds 0. The assignment that would have set it to 9 was never evaluated, so its side effect never happened.
This matters for the same reason it mattered in Chapter 3.6: it is a guarantee, not an optimisation, so correct code is allowed to depend on the unselected branch not running.
3. The Type Rule
Here is the part that is genuinely worth your attention, and the part most introductions skip.
An expression has a type as well as a value — Chapter 3.1 opened Module 3 with that, and Chapter 3.12 closed it by showing how C decides the type when two operands disagree. The conditional operator is the clearest possible application of that work:
When the second and third operands both have arithmetic type, the usual arithmetic conversions are applied to them, and the common type that results is the type of the whole conditional expression.
Both branches decide the type together. This happens regardless of which branch is actually evaluated, because a type is a compile-time property of the expression and the branch is selected at run time. The compiler must give the expression one type before it can know which way the condition will go.
sizeof makes it visible:
#include <stdio.h>
int main(void)
{
int small = 1;
printf("sizeof small %zu\n", sizeof small);
printf("sizeof (1 ? small : 2) %zu\n", sizeof (1 ? small : 2));
printf("sizeof (1 ? small : 2.0) %zu\n", sizeof (1 ? small : 2.0));
printf("value of (1 ? small : 2.0) %f\n", 1 ? small : 2.0);
return 0;
}Output on a typical desktop implementation, where int is 4 bytes and double is 8:
sizeof small 4
sizeof (1 ? small : 2) 4
sizeof (1 ? small : 2.0) 8
value of (1 ? small : 2.0) 1.000000Take the third and fourth lines together, because between them they are the whole lesson.
The condition is 1, so the selected branch is small, an int holding 1. Yet the expression's size is 8, not 4 — because the other branch has type double, and the usual arithmetic conversions bring both to double. The int value 1 is converted, and the expression's type is double.
The fourth line confirms it from the other direction. %f expects a double, and it is the correct specifier here — the expression really does have type double, which is why it prints 1.000000 rather than misbehaving. The value came from the int branch; the type came from both.
The same reasoning explains a subtler case. If one branch is unsigned int and the other is int, the usual arithmetic conversions apply exactly as Chapter 3.12 described them — including the step that can convert the signed operand to unsigned. The conditional operator does not get its own rules; it borrows the ones you already know.
4. Where It Earns Its Place
If the operator were only a shorter if, there would be little to recommend it. It is not. There are positions in a program where a statement is not allowed at all, and in those positions this operator is the only way to make a decision.
The sharpest example is initialising a const object. Chapter 2.11 established that a const object must not be modified — which means it must get its value in its initialiser, because there is no later opportunity.
#include <stdio.h>
int main(void)
{
int fast_mode = 1;
const int retry_limit = fast_mode ? 2 : 10;
printf("retry_limit %d\n", retry_limit);
return 0;
}Output:
retry_limit 2Now try to express the same thing with if and else. The object has to be declared before the decision, so it has to be declared without a value and assigned afterwards:
const int retry_limit;
if (fast_mode) {
retry_limit = 2;
} else {
retry_limit = 10;
}warning: default initialization of an object of type 'const int' leaves the
object uninitialized
error: cannot assign to variable 'retry_limit' with const-qualified type
'const int'
note: variable 'retry_limit' declared const hereNot a style complaint — an error, and the program does not build. The warning above it is the other half of the problem: a const object that was never initialised can never be given a value, so it is permanently stuck holding whatever Chapter 2.3 warned you about reading.
The usual workaround is to drop the const, and that is a real loss. const is how you record in the code that a value is fixed once determined, and the compiler enforces it. The conditional operator lets you keep that promise and still decide the value.
The same argument applies anywhere a value is required rather than a statement: an argument in a call, part of a larger expression, the initialiser of any object you would rather declare and define in one place.
5. Precedence: Lower Than Almost Everything
Chapter 3.10 built a table of binding strength. The conditional operator slots in near the bottom — tighter than assignment, looser than every operator above it:
| Binding | Operators | Associativity |
|---|---|---|
| … | …as in Chapter 3.10… | |
&& | left | |
|| | left | |
?: | right | |
| loosest | = += -= *= /= %= | right |
Being looser than arithmetic is usually what you want, because it means the condition can be written without parentheses:
int larger = a > b ? a : b;a > b binds tighter than ?:, so this groups as (a > b) ? a : b, which is the intent. The parentheses in §1 were for the reader, not the compiler.
But the same looseness is a trap when the conditional is embedded in a larger expression:
#include <stdio.h>
int main(void)
{
int total = 5;
int flag = 1;
printf("%d\n", total + flag ? 100 : 200);
printf("%d\n", total + (flag ? 100 : 200));
return 0;
}Output:
100
105The first line almost certainly does not say what its author meant. Because + binds tighter than ?:, it groups as (total + flag) ? 100 : 200 — the sum 6 is used as the condition, which is non-zero, so the result is 100 and total plays no part in the value at all. The second line is the intended reading, and it needed parentheses to get there.
This is Chapter 3.10's general lesson in a specific place: parentheses change grouping, and grouping is what decides which operand belongs to which operator.
6. Chaining, and When to Stop
The conditional operator is right-associative, which makes a chain read naturally:
#include <stdio.h>
int main(void)
{
int level = 55;
int code = (level < 25) ? 0
: (level < 75) ? 1
: 2;
printf("code %d\n", code);
return 0;
}Output:
code 1Right associativity means the third operand of the first conditional is the entire second conditional — exactly as if it were written (level < 25) ? 0 : ((level < 75) ? 1 : 2). So this behaves like Chapter 4.1's else if chain: tried in order, stopping at the first match. The layout above, with the ? and : aligned down the left, is the conventional way to make that structure visible.
A realistic use of the same shape is clamping a value into a valid window:
#include <stdio.h>
int main(void)
{
const int floor_value = 0;
const int ceiling_value = 4095;
int raw = 5200;
int clamped = (raw < floor_value) ? floor_value
: (raw > ceiling_value) ? ceiling_value
: raw;
printf("raw %d clamped %d\n", raw, clamped);
return 0;
}Output:
raw 5200 clamped 4095Where the line is
Two restrictions are worth adopting deliberately.
The result is a value, not an object. (a > b ? a : b) = 0; is not valid C. The conditional expression produces the value of one of its branches; it does not hand you back the object that branch named. If you want to assign to one of two objects, you need if and else.
Use it for choosing a value, not for choosing an action. This is legal and is bad code:
flag ? printf("yes\n") : printf("no\n");Nothing is being chosen here except which side effect happens, and the result value is computed and thrown away. An if/else states that intent directly and reads better. The honest test is whether you would want the result if you kept it. If the answer is no, you wanted a statement.
Likewise, a chain of two or three is readable; a chain of six is not, and a chain with conditionals nested inside the condition is worse. if/else scales in a way a single expression does not.
7. What You Should Be Able to Say Now
ifis a statement and chooses what the program does;?:is an operator and chooses a value.- The form is
condition ? value-if-true : value-if-false, and the condition is judged against zero like any other condition. - Exactly one of the second and third operands is evaluated, which is observable through side effects.
- Both branches decide the result type together, through the usual arithmetic conversions, even though only one runs.
1 ? an_int : 2.0has typedouble— the value comes from one branch, the type from both.- It can go where a statement cannot, most usefully in a
constinitialiser, where theif/elseform is a compile error. - It binds looser than arithmetic and comparison, so
total + flag ? x : ytests the sum. - It is right-associative, so chains read as first-match-wins.
- The result is a value, not an object, so it cannot appear on the left of an assignment.
- Use it to choose a value. If you are choosing an action, write
if.
8. Exercises
Exercise 1 — predict the output. Do not compile yet.
int x = 7;
int y = 3;
printf("%d\n", x > y ? x : y);
printf("%d\n", x < y ? x : y);
printf("%d\n", x ? y : x);Exercise 2 — predict the type. Give the value of each, and say what type the expression has and why:
printf("%zu\n", sizeof (0 ? 1 : 2));
printf("%zu\n", sizeof (0 ? 1 : 2.0f));
printf("%zu\n", sizeof (0 ? 1.0 : 2));Note that the condition is 0 in all three, so the first branch never runs. Explain why that does not affect any of the three answers.
Exercise 3 — trace the side effects. Say what is printed and what each object holds afterwards:
int p = 0;
int q = 0;
int r = (0) ? (p = 1) : (q = 2);
printf("%d %d %d\n", r, p, q);Exercise 4 — find the grouping. Rewrite each with explicit parentheses showing how the compiler groups it, then give the value when n holds 4:
n * 2 ? 10 : 20
n - 4 ? 10 : 20
n > 2 ? n : n + 1Exercise 5 — explain the error. A colleague reports that this does not compile and asks you to "just remove the const". Explain what the compiler is objecting to, and give a fix that keeps the const.
const int timeout;
if (urgent) {
timeout = 5;
} else {
timeout = 60;
}Exercise 6 — repair the type. Compiled with warnings enabled, this produces format specifies type 'int' but the argument has type 'double'. Identify the actual type of the conditional expression, explain why the specifier does not match it and what the standard says about running it anyway, then give two different repairs — one that changes the expression and one that changes the printf.
int scale = 2;
printf("%d\n", scale > 1 ? scale : 0.5);Exercise 7 — rewrite for clarity. Convert this to if/else and say which version you would ship:
ready ? printf("go\n") : printf("wait\n");Exercise 8 — choose the tool. For each, say whether you would use ?: or if/else, and why: (a) initialising a const upper bound from a mode flag; (b) printing one of two messages; (c) selecting which of two counters to increment; (d) passing either a measured value or a default to a calculation.
9. Summary
- The conditional operator
a ? b : cis the expression form of a decision — the only operator in C that selects between two values. - The condition is judged against zero, and exactly one of the two branches is evaluated.
- Both branches determine the result type through the usual arithmetic conversions, whichever one runs.
- It can appear wherever a value is required, which is why it can initialise a
constandif/elsecannot. - It binds looser than arithmetic and comparison and is right-associative, so chains work and embedded uses need parentheses.
- Its result is a value, not an object.
- Choose it for choosing values. For choosing actions,
ifsays it better.
Both constructs so far test a condition and pick one of two outcomes; a multi-way decision costs a chain of them, and every test in that chain is evaluated in turn until one matches. When all the tests are comparing the same expression against a list of constants, C offers something structurally different — a single dispatch on a value, with one famous behaviour that catches every beginner exactly once.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
