Short Circuiting in C

Need a fast revision version? See the C Programming Cheat Sheet →

Table of Contents

  1. What is Short-Circuit Evaluation?
  2. Short-Circuiting with Logical AND (&&)
  3. Short-Circuiting with Logical OR (||)
  4. Why Short-Circuiting Matters
  5. Common Patterns & Idioms
  6. Interaction with Side Effects
  7. Short-Circuiting vs Bitwise Operators
  8. Nested Logical Expressions
  9. Common Mistakes
  10. Practice MCQs
  11. Key Takeaways

1. What is Short-Circuit Evaluation?

Short-circuit evaluation is a behavior of the logical operators && (Logical AND) and || (Logical OR) where the second operand is evaluated only if necessary to determine the final result.

In other words, the compiler “short-circuits” the evaluation as soon as the outcome is known, skipping the evaluation of the remaining expressions.

Which Operators Short-Circuit?

Only two operators in C exhibit short-circuit behavior:

OperatorNameShort-Circuits When…
&&Logical ANDFirst operand is false (0)
``

Important: The bitwise operators & and | do NOT short-circuit. They always evaluate both operands.


2. Short-Circuiting with Logical AND (&&)

Rule

For A && B:

  • If A evaluates to 0 (false), the entire expression is false, so B is never evaluated.
  • If A evaluates to non-zero (true), B must be evaluated to determine the final result.

Truth Table

AB Evaluated?A && B
0 (false)No0
non-zero (true)YesDepends on B

Example 1: Basic Short-Circuit

printf("%d", 0 && 10 / 0);

Evaluation:

  1. The first operand is 0 (false).
  2. Since 0 && anything is always 0, the second operand 10 / 0 is never evaluated.
  3. Output: 0

Note: 10 / 0 would normally cause undefined behavior (division by zero), but because of short-circuiting, it is safely skipped.

Example 2: Both Operands Evaluated

printf("%d", 1 && 5);

Evaluation:

  1. The first operand is 1 (true).
  2. The second operand must be evaluated.
  3. 5 is non-zero (true).
  4. Output: 1

Example 3: Function Call Skipped

#include <stdio.h>

int fun() {
    printf("Function Called\n");
    return 1;
}

int main() {
    printf("%d", 0 && fun());
}

Output:

0

fun() is never called because the first operand is 0.


3. Short-Circuiting with Logical OR (||)

Rule

For A || B:

  • If A evaluates to non-zero (true), the entire expression is true, so B is never evaluated.
  • If A evaluates to 0 (false), B must be evaluated to determine the final result.

Truth Table

| A | B Evaluated? | A || B | |:-:|:------------:|:------:| | non-zero (true) | No | 1 | | 0 (false) | Yes | Depends on B |

Example 1: Basic Short-Circuit

printf("%d", 5 || 10 / 0);

Evaluation:

  1. The first operand is 5 (true).
  2. Since anything || true is always 1, the second operand 10 / 0 is never evaluated.
  3. Output: 1

Example 2: Both Operands Evaluated

printf("%d", 0 || 5);

Evaluation:

  1. The first operand is 0 (false).
  2. The second operand must be evaluated.
  3. 5 is non-zero (true).
  4. Output: 1

Example 3: Function Call Skipped

#include <stdio.h>

int fun() {
    printf("Function Called\n");
    return 1;
}

int main() {
    printf("%d", 1 || fun());
}

Output:

1

fun() is never called because the first operand is 1.


4. Why Short-Circuiting Matters

1. Preventing Runtime Errors

Short-circuiting is commonly used to guard against operations that would fail under certain conditions.

Guarding Division by Zero

// Without short-circuiting (dangerous):
if (b / a > 5) { }   // Crashes if a == 0!

// With short-circuiting (safe):
if (a != 0 && b / a > 5) { }

If a is 0, the first condition a != 0 is false, so b / a is never evaluated.

Guarding NULL Pointer Dereference

// Without short-circuiting (dangerous):
if (ptr->data == 10) { }   // Crashes if ptr is NULL!

// With short-circuiting (safe):
if (ptr != NULL && ptr->data == 10) { }

If ptr is NULL, the first condition fails, and ptr->data is never accessed.

Guarding Array Bounds

if (index >= 0 && index < size && arr[index] == target) { }

If index is out of bounds, arr[index] is never accessed.

2. Performance Optimization

Short-circuiting can avoid expensive computations when the result is already determined.

if (cheap_check() && expensive_computation()) { }

If cheap_check() returns false, expensive_computation() is skipped entirely.

3. Conditional Initialization

char *name = get_name();
if (name && strlen(name) > 0) {
    printf("Name: %s\n", name);
}

If name is NULL, strlen(name) is never called (which would crash).


5. Common Patterns & Idioms

Pattern 1: Safe Division

if (divisor != 0 && dividend / divisor > threshold) { }

Pattern 2: Safe Pointer Access

if (ptr && ptr->next && ptr->next->value > 0) { }

Pattern 3: Safe String Operations

if (str && *str && isalpha(*str)) { }

Checks: (1) pointer not NULL, (2) string not empty, (3) first char is alphabetic.

Pattern 4: Default Value Assignment

int value = (ptr && ptr->valid) ? ptr->data : DEFAULT_VALUE;

Pattern 5: Early Exit in Functions

int process(int *arr, int size) {
    if (!arr || size <= 0) return -1;  // Guard clause
    // ... process array safely ...
}

Pattern 6: Feature Detection

if (supports_feature() && enable_feature()) { }

Only attempt to enable a feature if the system supports it.


6. Interaction with Side Effects

Short-circuiting becomes tricky when the skipped operand has side effects — operations that modify state (variable assignments, function calls, I/O, increment/decrement).

Example 1: Skipped Increment

int i = 0;
printf("%d", 0 && (i++));
printf("%d", i);

Output:

0
0

i++ is never executed because the first operand is 0.

Example 2: Post-increment with OR

int i = 0;
printf("%d", i++ || 5);
printf("%d", i);

Evaluation:

  1. i++ returns the original value 0, then increments i to 1.
  2. Since the first operand was 0, the second operand 5 must be evaluated.
  3. Output: 1 (from 0 || 5), then 1 (value of i).

Example 3: Pre-increment with AND

int i = 0;
printf("%d", ++i && 5);
printf("%d", i);

Evaluation:

  1. ++i increments i to 1 and returns 1.
  2. Since the first operand is 1 (true), the second operand 5 is evaluated.
  3. 1 && 5 = 1.
  4. Output: 1, then 1.

Example 4: Complex Side Effects

int i = 5;
printf("%d", i-- && 0);
printf("%d", i);

Evaluation:

  1. i-- returns 5, then decrements i to 4.
  2. First operand is 5 (true), so second operand 0 is evaluated.
  3. 5 && 0 = 0.
  4. Output: 0, then 4.

Rule of Thumb

Never rely on side effects in the second operand of && or ||. The side effect may or may not occur depending on the first operand, making code unpredictable and hard to debug.

Bad Practice:

if (condition && (x++)) { }   // x may or may not increment!

Good Practice:

if (condition) {
    x++;
}

7. Short-Circuiting vs Bitwise Operators

This is one of the most common sources of confusion in C.

Comparison Table

| Property | && / || (Logical) | & / | (Bitwise) | |----------|:---------------------:|:-------------------:| | Evaluates both operands? | No (short-circuits) | Yes (always) | | Operates on | Whole value (true/false) | Each bit individually | | Return value | Always 0 or 1 | Integer with modified bits | | Use case | Boolean logic, conditions | Bit manipulation, flags | | Side effects in 2nd operand | May be skipped | Always executed |

Example: Critical Difference

int fun() {
    printf("Called\n");
    return 1;
}

// Logical AND -- short-circuits
printf("%d", 0 && fun());   // Output: 0 (fun() NOT called)

// Bitwise AND -- does NOT short-circuit
printf("%d", 0 & fun());    // Output: 0 (fun() IS called, prints "Called")

When to Use Which?

  • Use && and || for boolean conditions and control flow.
  • Use & and | for bit manipulation, flag operations, and when you need both operands evaluated.

8. Nested Logical Expressions

When multiple logical operators are combined, short-circuiting applies at each step.

Example: Chained AND

printf("%d", 0 && 10 && 20 / 0);

Evaluation:

  1. 0 && 10 = 0 (short-circuits, 10 not evaluated)
  2. 0 && (20 / 0) = 0 (short-circuits, 20 / 0 not evaluated)
  3. Output: 0

Example: Chained OR

printf("%d", 5 || 0 || 10 / 0);

Evaluation:

  1. 5 || 0 = 1 (short-circuits, 0 not evaluated… wait!)

Actually, let me correct: || is left-associative, so:

  1. 5 || 0 — first operand is 5 (true), so result is 1, 0 is skipped.
  2. 1 || (10 / 0) — first operand is 1 (true), so 10 / 0 is skipped.
  3. Output: 1

Example: Mixed AND and OR

printf("%d", 1 || 0 && 0);

Evaluation:

  1. && has higher precedence than ||.
  2. Evaluate 0 && 0 first = 0.
  3. Then 1 || 0 = 1.
  4. Output: 1

Note: The 0 && 0 is fully evaluated because both are needed for the &&. But if it were 0 && (10/0), the division would be skipped.

Example: Complex Expression

printf("%d", 0 || 2 && 0);

Evaluation:

  1. && has higher precedence: evaluate 2 && 0 = 0.
  2. Then 0 || 0 = 0.
  3. Output: 0

9. Common Mistakes

Mistake 1: Assuming both operands are always evaluated

int x = 0;
if (x != 0 && 10 / x > 5) { }   // Safe

// But if you accidentally use & instead of &&:
if (x != 0 & 10 / x > 5) { }    // CRASH! 10/0 is evaluated

Mistake 2: Using bitwise operators for boolean logic

// Wrong: bitwise OR does not short-circuit
if (ptr == NULL | ptr->value == 0) { }   // CRASH if ptr is NULL!

// Correct: logical OR short-circuits
if (ptr == NULL || ptr->value == 0) { }  // Safe

Mistake 3: Relying on side effects in short-circuited expressions

int count = 0;
if (condition && (count++)) { }
// count may or may not be incremented -- unpredictable!

Mistake 4: Confusing precedence

// What does this evaluate to?
int result = 1 || 0 && 0;

&& has higher precedence than ||, so this is 1 || (0 && 0) = 1 || 0 = 1.

Always use parentheses for clarity:

int result = 1 || (0 && 0);

Mistake 5: Thinking || always evaluates both operands

if (is_valid(x) || process(x)) { }

If is_valid(x) returns true, process(x) is never called. If process(x) has necessary side effects, they will be missed.


10. Practice MCQs

MCQ 1

printf("%d", 0 && 10 / 0);

A. Runtime Error
B. 0
C. 1
D. Undefined

Answer: B (0)

The first operand is 0, so 10 / 0 is never evaluated. No crash occurs.


MCQ 2

printf("%d", 5 || 10 / 0);

A. Runtime Error
B. 0
C. 1
D. Undefined

Answer: C (1)

The first operand is 5 (true), so 10 / 0 is never evaluated.


MCQ 3

int i = 0;
printf("%d", i++ && 5);
printf("%d", i);

A. 0 0
B. 0 1
C. 1 1
D. 1 0

Answer: B (0 1)

i++ returns 0 (post-increment), so the second operand 5 is skipped. But i is incremented to 1.


MCQ 4

int i = 0;
printf("%d", ++i || 0);

A. 0
B. 1
C. Runtime Error
D. Undefined

Answer: B (1)

++i makes i = 1. Since 1 is true, 0 is skipped. Result is 1.


MCQ 5

int i = 0;
printf("%d", i++ || 5);
printf("%d", i);

A. 1 1
B. 0 1
C. 1 0
D. 0 0

Answer: A (1 1)

i++ returns 0, then i becomes 1. Since first operand was 0, second operand 5 is evaluated. 0 || 5 = 1.


MCQ 6

int i = 5;
printf("%d", i-- && 0);
printf("%d", i);

A. 0 4
B. 1 5
C. 0 5
D. 1 4

Answer: A (0 4)

i-- returns 5, then i becomes 4. 5 && 0 = 0.


MCQ 7

printf("%d", 0 || 0 || 10);

A. 0
B. 1
C. 10
D. Error

Answer: B (1)

0 || 0 = 0, then 0 || 10 = 1 (since 10 is non-zero).


MCQ 8

printf("%d", 5 && 6 && 7);

A. 0
B. 1
C. 7
D. 5

Answer: B (1)

5 && 6 = 1, then 1 && 7 = 1. Logical AND always returns 0 or 1.


MCQ 9

printf("%d", 0 || 2 && 0);

A. 0
B. 1
C. 2
D. Error

Answer: A (0)

&& has higher precedence: 2 && 0 = 0, then 0 || 0 = 0.


MCQ 10

printf("%d", 1 || 0 && 0);

A. 0
B. 1
C. Runtime Error
D. Undefined

Answer: B (1)

&& first: 0 && 0 = 0, then 1 || 0 = 1.


11. Key Takeaways

  • Short-circuit evaluation applies only to && and ||.
  • && skips the second operand if the first operand is false (0).
  • || skips the second operand if the first operand is true (non-zero).
  • Short-circuiting is commonly used to avoid division by zero, null pointer dereferencing, and out-of-bounds access.
  • && and || are different from the bitwise operators & and |, which always evaluate both operands.
  • Never rely on side effects (increments, function calls) in the second operand of && or || — they may be skipped.
  • && has higher precedence than ||, but always use parentheses for clarity in complex expressions.
  • Short-circuiting is not just an optimization — it is a fundamental language feature that enables safe conditional programming.

End of Notes — Continue to MCQs or Flashcards