Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
the if/else version
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
42

This 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:

Azvya Education Pvt. Ltd.VLSI Mentor
the same decision as an 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
condition ? value-if-true : value-if-false

It 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:

Azvya Education Pvt. Ltd.VLSI Mentor
which branch actually ran?
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
result 5, taken 5, not_taken 0

not_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:

Azvya Education Pvt. Ltd.VLSI Mentor
the type comes from both branches
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sizeof small                 4
sizeof (1 ? small : 2)       4
sizeof (1 ? small : 2.0)     8
value of (1 ? small : 2.0)   1.000000

Take 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.

Azvya Education Pvt. Ltd.VLSI Mentor
a decision inside an initialiser
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
retry_limit 2

Now 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:

Azvya Education Pvt. Ltd.VLSI Mentor
the same intent, written with if — this does not compile
    const int retry_limit;

    if (fast_mode) {
        retry_limit = 2;
    } else {
        retry_limit = 10;
    }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 here

Not 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:

BindingOperatorsAssociativity
……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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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:

Azvya Education Pvt. Ltd.VLSI Mentor
where does the condition end?
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
100
105

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
a classification chain
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
code 1

Right 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:

Azvya Education Pvt. Ltd.VLSI Mentor
clamping a reading
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
raw 5200 clamped 4095

Where 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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

  1. if is a statement and chooses what the program does; ?: is an operator and chooses a value.
  2. The form is condition ? value-if-true : value-if-false, and the condition is judged against zero like any other condition.
  3. Exactly one of the second and third operands is evaluated, which is observable through side effects.
  4. Both branches decide the result type together, through the usual arithmetic conversions, even though only one runs.
  5. 1 ? an_int : 2.0 has type double — the value comes from one branch, the type from both.
  6. It can go where a statement cannot, most usefully in a const initialiser, where the if/else form is a compile error.
  7. It binds looser than arithmetic and comparison, so total + flag ? x : y tests the sum.
  8. It is right-associative, so chains read as first-match-wins.
  9. The result is a value, not an object, so it cannot appear on the left of an assignment.
  10. 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
n * 2 ? 10 : 20
n - 4 ? 10 : 20
n > 2 ? n : n + 1

Exercise 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 : c is 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 const and if/else cannot.
  • 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, if says 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.