Operators, Expressions, and Control Flow: How C Programs Make Decisions
Programs do more than just store data; they must compute and make decisions. C uses expressions to produce values, statements to produce actions, and control flow to dictate the next execution path. Beginners often treat these topics like syntax lookup tables. But in C, the order of evaluation, integer arithmetic, short-circuit logic, and loop boundaries directly dictate both memory safety and concurrency safety.
Expressions Produce Values
An expression is any snippet of code that can be evaluated to a value.
1 + 2
x * 10
count > 0
func(3)
Expressions can be pure computations, but they can also have side effects. Assignments, function calls, and increment/decrement operators all produce side effects.
x = x + 1;
printf("%d\n", x);
A side effect implies that the program's state has mutated. Understanding side effects is a prerequisite for understanding evaluation order.
Statements Execute Actions
Statements typically end with a semicolon.
int x = 0;
x = x + 1;
return x;
Control statements aren't necessarily confined to a single line.
if (x > 0) {
x = x - 1;
}
It is highly recommended to always use curly braces {}.
This prevents control flow logic errors when adding code later.
Arithmetic Operators
Common arithmetic operations:
| Operator | Meaning |
|---|---|
+ |
Addition |
- |
Subtraction |
* |
Multiplication |
/ |
Division |
% |
Modulo (Remainder) |
Integer division truncates towards zero.
int x = 5 / 2; // 2
Division by zero is a catastrophic error. Integer division by zero yields undefined behavior. Floating-point division by zero follows IEEE 754 rules, but shouldn't be relied upon carelessly.
Assignment is Not Mathematical Equality
= denotes writing the right-hand value into the left-hand object.
int x = 1;
x = x + 2;
The meaning of the second line is:
- Read the current value of
x. - Add
2. - Write the result back into
x.
It is not the mathematical statement "x equals x plus 2."
Compound assignments:
x += 2;
x *= 3;
These are more concise, but do not sacrifice readability just to save keystrokes.
Relational Operators
Relational operators yield a true or false boolean result.
a == b
a != b
a < b
a <= b
a > b
a >= b
Note that == and = are entirely different.
if (x = 1) {
/* HIGH RISK: This is an assignment, not a comparison */
}
Compiler warnings will typically flag this issue. This is why warnings must be enabled from day one.
Logical Operators and Short-Circuiting
&& // Logical AND
|| // Logical OR
! // Logical NOT
&& and || exhibit short-circuit behavior.
if (p != NULL && p->value > 0) {
use(p);
}
If p == NULL, the right-hand side is never evaluated.
This allows the null pointer check to protect the subsequent dereference.
If the order is reversed, a dereference occurs before the check, drastically altering the risk profile.
Use Increment and Decrement Operators Simply
++i;
i++;
--i;
i--;
They are perfectly clear when used as standalone statements. They become error-prone when embedded in complex expressions.
a[i++] = i; // NOT recommended
Do not modify the same object multiple times within a single expression. Such code invites undefined behavior and sequence point ambiguities.
Bitwise Operators
Bitwise operators manipulate data directly at the binary level.
| Operator | Meaning |
|---|---|
& |
Bitwise AND |
| ` | ` |
^ |
Bitwise XOR |
~ |
Bitwise NOT |
<< |
Left Shift |
>> |
Right Shift |
Frequently used for flag masks:
enum {
READ = 1 << 0,
WRITE = 1 << 1,
EXEC = 1 << 2
};
int mask = READ | WRITE;
Prefer unsigned types for bitwise operations. Signed shifts and overflows introduce dangerous edge cases and undefined behavior.
Choosing Paths with if
if (score >= 60) {
puts("pass");
} else {
puts("fail");
}
If the condition expression evaluates to true, the first block executes; otherwise, the else block executes.
Multi-branching:
if (score >= 90) {
grade = 'A';
} else if (score >= 80) {
grade = 'B';
} else {
grade = 'C';
}
Boundaries must be crystal clear.
The difference between >= and > directly dictates classification outcomes.
switch for Discrete Branching
switch (op) {
case '+':
result = a + b;
break;
case '-':
result = a - b;
break;
default:
return -1;
}
The break keyword is crucial.
Omitting break causes execution to fall through into the next case block.
If you intend to fall through, document it explicitly with a comment or attribute.
while Loops
A while loop checks the condition first, then executes the body.
while (i < n) {
sum += values[i];
++i;
}
You must ensure the condition eventually evaluates to false. Otherwise, the program will spin in an infinite loop. In production code, loops must also account for timeouts, cancellation tokens, and resource cleanup.
do while Loops
A do while loop is guaranteed to execute at least once.
do {
read_next();
} while (has_more());
It is ideal for scenarios like "perform the action once, then check if we should continue."
Do not rewrite a standard loop into a do while merely to save a line of code.
for Loops
for loops are designed for counter-driven iterations.
for (size_t i = 0; i < n; ++i) {
sum += values[i];
}
The three components are initialization, condition, and iteration expression.
Pay forensic attention to boundaries when iterating over arrays.
The difference between i <= n and i < n is a memory violation.
break and continue
break terminates the current loop immediately.
continue skips the remainder of the current iteration and jumps to the next evaluation.
for (size_t i = 0; i < n; ++i) {
if (values[i] < 0) {
continue;
}
if (values[i] == target) {
break;
}
}
They simplify control flow. However, excessive jumping obscures resource cleanup paths and makes code harder to audit.
The Proper Place for goto
Do not use goto for standard business logic in the beginner phase.
However, in C's resource cleanup paradigms, goto cleanup is an established, idiomatic pattern.
int rc = -1;
FILE* f = fopen(path, "rb");
if (f == NULL) goto cleanup;
rc = 0;
cleanup:
if (f != NULL) fclose(f);
return rc;
Its engineering value lies in centralizing resource deallocation. It is not a substitute for structured control flow.
Be Conservative with Evaluation Order
The evaluation order of sub-expressions in C is rarely strictly left-to-right. Never write code that relies on convoluted evaluation sequences.
int x = 1;
int y = x++ + x++; // NEVER DO THIS
Break complex expressions down into multiple lines. The compiler optimizer will easily handle the performance. Human code auditors require clarity.
Engineering Risks
Common risks associated with foundational control flow:
- Off-by-one errors in loop boundaries.
- Omitting
breakinswitchstatements. - Division by zero.
- Mixing signed and unsigned types in comparisons.
- Modifying the same object multiple times in complex expressions.
- Mistyping
=for==. - Bypassing resource cleanup paths with premature
returnorbreak. - Infinite loops lacking timeout constraints or fallbacks.
These risks follow you from small scripts all the way into system-level engineering.
Summary
Operators enable programs to compute. Expressions yield values and side effects. Control flow defines the execution path. The difficulty of C is not memorizing symbols; it is understanding the evaluation rules, type conversions, boundary limits, and resource impacts hiding behind each symbol. Keeping expressions simple and control flows transparent is your primary defense against low-level bugs.