Operators
Arithmetic, relational, logical, assignment, increment, bitwise, conditional, and size operators, plus precedence, associativity, and expression evaluation.
Expressions are the sentences of computation. Operators combine values, but their symbols alone do not tell the whole story: operand types determine arithmetic, precedence determines grouping, and evaluation rules determine when side effects happen. Reliable C code makes all three clear.
Expression Model
In
total = price * quantity + delivery;
price, quantity, and delivery are operands. *, +, and = are operators. Precedence groups the multiplication before addition, and assignment stores the final converted value in total.
An expression normally has both a type and a value. Some expressions also have side effects, such as modifying an object or performing I/O. Side effects make evaluation-order rules important.
Arithmetic Operators
The binary arithmetic operators are +, -, *, /, and %. Unary + and - apply to one operand.
int sum = 8 + 3; /* 11 */
int difference = 8 - 3; /* 5 */
int product = 8 * 3; /* 24 */
int quotient = 8 / 3; /* 2 */
int remainder = 8 % 3; /* 2 */
Integer division discards the fractional part toward zero:
-8 / 3 /* -2 */
-8 % 3 /* -2; quotient*divisor + remainder equals dividend */
Division or remainder by zero is undefined behaviour. Guard the divisor before evaluating the operation:
if (divisor != 0) {
quotient = dividend / divisor;
}
With floating operands, / performs floating-point division. % is only for integer operands; <math.h> provides fmod for a floating remainder operation.
Arithmetic first applies promotions and common-type conversions. Thus 5 / 2 and 5.0 / 2 produce different values.
Relational Operators
The relational operators <, <=, >, and >= compare ordered values. Equality operators == and != test equality and inequality. Their result is an int: 1 when true and 0 when false.
bool is_adult = age >= 18;
bool is_outside = value < minimum || value > maximum;
Do not confuse assignment with equality:
if (status = READY) { /* assigns; probably a defect */
/* ... */
}
if (status == READY) { /* compares */
/* ... */
}
Strong compiler warnings usually detect the first pattern.
Mathematical chained comparisons do not translate directly:
/* Wrong for testing 0 <= x <= 10. */
if (0 <= x <= 10) { /* ... */ }
/* Correct. */
if (0 <= x && x <= 10) { /* ... */ }
The first expression computes 0 <= x, yielding 0 or 1, then compares that small integer with 10; the result is almost always true.
Logical Operators
! means logical not, && means logical and, and || means logical or. Any scalar zero is false; any nonzero scalar is true. Results are 0 or 1.
if (!is_locked && attempts < MAX_ATTEMPTS) {
allow_attempt();
}
Short-Circuit Evaluation
Logical operators evaluate left to right and stop as soon as the result is known:
- In
left && right,rightis evaluated only ifleftis true. - In
left || right,rightis evaluated only ifleftis false.
This allows safe guards:
if (denominator != 0 && numerator / denominator > 10) {
/* division happens only after the nonzero check */
}
Order the checks from required precondition to dependent expression. Reversing them defeats the guard.
Do not use bitwise & and | as substitutes. They do not short-circuit and operate on individual bits.
Assignment Operators
Simple assignment evaluates the right operand, converts its value to the left object’s type, and stores it:
balance = deposit - fee;
Compound assignments combine an operation with assignment:
total += value;
mask |= FLAG_READY;
index %= capacity;
left op= right is similar in purpose to left = left op right, but the left operand is evaluated only once and conversion details can differ. This matters when the left expression is complex. Prefer a simple named left operand when possible.
Assignment expressions have values, enabling chains:
a = b = 0;
This groups right-to-left. Use chains only when they remain immediately clear; assignments embedded deeply inside conditions are harder to audit.
Increment Operators
Prefix and postfix ++ add one; -- subtracts one.
int i = 4;
int before = ++i; /* i becomes 5; before is 5 */
int j = 4;
int after = j++; /* after is 4; j becomes 5 */
When the expression value is unused, i++ and ++i have the same final effect on an ordinary integer:
for (int i = 0; i < 10; i++) {
/* ... */
}
Avoid expressions that modify the same scalar object more than once without sequencing, or both modify and independently read it without sequencing:
i = i++; /* undefined behaviour */
values[i] = i++; /* undefined behaviour */
Make state transitions explicit:
values[i] = i;
i++;
Shorter is not better when it makes the execution contract uncertain.
Bitwise Operators
Bitwise operators work on integer representations:
| Operator | Meaning |
|---|---|
& | bitwise AND |
| ` | ` |
^ | bitwise XOR |
~ | bitwise complement |
<< | left shift |
>> | right shift |
Use unsigned types for deliberate bit manipulation so shifts and representations are easier to reason about.
Flags
Independent Boolean options can occupy individual bits:
#include <stdbool.h>
enum Permission {
PERM_READ = 1U << 0,
PERM_WRITE = 1U << 1,
PERM_SHARE = 1U << 2
};
unsigned int permissions = PERM_READ | PERM_WRITE;
bool may_write = (permissions & PERM_WRITE) != 0U;
permissions |= PERM_SHARE; /* set */
permissions &= ~PERM_WRITE; /* clear */
permissions ^= PERM_READ; /* toggle */
The explicit comparison with zero makes the predicate visible.
Shifts
For an unsigned value, left shift by a valid count behaves like multiplication by a power of two modulo the type’s range, while right shift behaves like division by a power of two. The shift count must be non-negative and smaller than the promoted left operand’s width.
Left-shifting a negative signed value is undefined, and signed shifts have additional hazards. Prefer an unsigned left operand:
unsigned int bit = 1U << position;
Validate position before shifting.
Bitwise code should state its representation contract. It is inappropriate where ordinary arithmetic or Boolean variables communicate intent more clearly.
Conditional Operator
The conditional operator selects one of two expressions:
int absolute = value < 0 ? -value : value;
It first evaluates the condition, then exactly one of the second and third operands. It is useful for compact value selection:
const char *label = score >= 50 ? "pass" : "fail";
Avoid nesting conditional operators when an if statement would expose the decision structure more clearly. Also note that negating the minimum representable signed integer can overflow; even the compact absolute-value example requires a range precondition.
Sizeof Operator
sizeof reports the size of a type or object in bytes and yields a value of type size_t:
printf("int occupies %zu bytes\n", sizeof(int));
int readings[12];
size_t count = sizeof readings / sizeof readings[0];
Parentheses are required around a type name but not around an expression. Many codebases use them consistently for readability.
Usually, the expression operand is not evaluated:
size_t size = sizeof(i++); /* i is normally not incremented */
Variable-length arrays are an important exception: their size can be evaluated at runtime. Do not hide side effects inside a sizeof operand regardless.
For an array object, sizeof array gives total array storage. After an array parameter adjusts to a pointer, sizeof parameter gives pointer size, not the caller’s array size. Chapter 7 explores this boundary.
Operator Precedence
Precedence determines how an expression is grouped when parentheses do not say otherwise. For example:
a + b * c
groups as a + (b * c) because multiplication has higher precedence.
A useful partial order, from higher to lower, is:
- postfix operations such as calls, indexing, and postfix increment;
- unary operations such as
!,~, prefix increment, andsizeof; - multiplication, division, remainder;
- addition, subtraction;
- shifts;
- relational operators;
- equality operators;
- bitwise AND, XOR, OR;
- logical AND, OR;
- conditional
?:; - assignments;
- comma operator.
Do not write code that requires readers to memorise the entire table. Parentheses should clarify intent:
if ((flags & PERM_WRITE) != 0U) {
/* ... */
}
Operator Associativity
Associativity determines grouping among operators at the same precedence level. Most binary arithmetic operators associate left-to-right:
a - b - c /* groups as (a - b) - c */
Assignments associate right-to-left:
a = b = c /* groups as a = (b = c) */
Associativity is not evaluation order. f() + g() + h() groups left-to-right, but C does not generally require these calls to execute left-to-right.
Expression Evaluation
C often leaves operand evaluation order unspecified so implementations can generate efficient code. In:
result = left() + right();
either function may be called first. Correctness must not depend on their relative order.
Some operators do impose sequencing:
&&evaluates and completes its left operand before any right operand evaluation.||does the same.- the first operand of
?:is evaluated before the selected branch. - the comma operator sequences its left operand before its right operand.
Function arguments are not guaranteed to be evaluated left-to-right:
printf("%d %d\n", next_value(), next_value());
If output depends on which call happens first, split the operations:
int first = next_value();
int second = next_value();
printf("%d %d\n", first, second);
This is clearer and portable.
Expression Trace
Trace the expression:
int x = 3;
int y = 8;
bool accepted = x > 0 && (y / x) >= 2;
| Step | Expression | Result |
|---|---|---|
| 1 | x > 0 | 1 |
| 2 | left side of && is true, so continue | — |
| 3 | y / x | 2 by integer division |
| 4 | 2 >= 2 | 1 |
| 5 | 1 && 1 | 1 |
| 6 | conversion to bool | true |
If x were zero, step 3 would be skipped. The short-circuit order enforces the division precondition only if the guard is written as x != 0 && y / x >= 2.
Permission Case
A bit mask stores several independent Boolean facts in one unsigned integer. Each fact occupies one bit; masks name those positions so the code speaks in permissions rather than unexplained numbers.
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
int main(void) {
const unsigned int can_read = 1U << 0;
const unsigned int can_write = 1U << 1;
const unsigned int can_share = 1U << 2;
unsigned int permissions = 0U;
permissions |= can_read;
permissions |= can_write;
bool readable = (permissions & can_read) != 0U;
bool shareable = (permissions & can_share) != 0U;
printf("read=%d write=%d share=%d\n",
readable,
(permissions & can_write) != 0U,
shareable);
permissions ^= can_write;
permissions |= can_share;
permissions &= ~can_read;
printf("mask=%u, bits-per-unsigned=%zu\n",
permissions,
sizeof permissions * CHAR_BIT);
return EXIT_SUCCESS;
}
The state changes can be followed in binary:
initial 000
set read 001
set write 011
toggle write 001
set share 101
clear read 100
|= sets selected bits without disturbing others. ^= toggles them. &= ~mask clears them. The complement operator changes every bit, including higher bits not shown in the three-bit diagram; the following & ensures only the named bit is cleared from the existing value.
Unsigned types make shifts and bit operations easier to reason about. Even then, a shift count must be less than the width of the promoted left operand. Constructing 1U << position from user input therefore requires a check such as position < sizeof(unsigned int) * CHAR_BIT before the shift occurs.
Expression Trees
Precedence can be understood as an implicit tree. For:
result = base + count * step > limit && enabled;
the grouping is conceptually:
=
result &&
/ \
> enabled
/ \
+ limit
/ \
base *
/ \
count step
The tree explains grouping but not necessarily execution order. The multiplication must contribute a value to the addition, and the addition to the comparison. However, independent operand evaluations may still occur in any order allowed by C. && is special: its complete left side is evaluated first, and the right side is skipped if the left result is false.
Rewrite an expression when the tree hides a domain concept:
int projected = base + count * step;
bool below_limit = projected > limit;
bool accepted = below_limit && enabled;
Here below_limit is badly named because the comparison actually tests “greater than.” Giving intermediate results names does more than shorten a line: it gives reviewers a chance to notice that semantic mismatch. A corrected name or corrected operator becomes obvious.
Arithmetic Preconditions
Operators have contracts even though their syntax is compact:
- integer
/and%require a nonzero divisor; - signed division of the minimum value by
-1is not representable and is undefined; - signed addition, subtraction, and multiplication require a representable result;
- a shift count must be non-negative and smaller than the promoted left operand’s width;
- left-shifting a signed value has additional representability constraints;
- pointer arithmetic, introduced later, must stay within one array object or one past it.
Short-circuiting can enforce a precondition only when the guard is on the left:
if (divisor != 0 && dividend / divisor == quotient) {
puts("relationship holds");
}
This version is not equivalent:
if (dividend / divisor == quotient && divisor != 0) {
/* division already happened */
}
Operator precedence cannot rescue a reversed safety check. Sequencing and grouping are separate properties.
Side-Effect Ledger
Whenever an expression reads and modifies state, write a ledger before trusting it:
int index = 2;
int old = index++;
int next = index;
| Statement | value produced | index afterward |
|---|---|---|
old = index++ | 2 | 3 |
next = index | 3 | 3 |
Now compare values[index] = index++;. The expression both reads index to select an array element and modifies index without a sequencing relationship that makes the two uses safe. Parentheses do not add sequencing. Split it into statements whose state changes have an explicit order.
Dense expressions are not more advanced merely because they are shorter. Strong C code uses expressions for coherent calculations and statements to expose consequential state transitions.
Truth Tables
Logical operators convert scalar operands to truth: zero is false and any nonzero value is true. Their result is exactly integer 0 or 1.
| a truth | b truth | a && b | a || b |
|---:|---:|---:|---:|
| false | false | 0 | 0 |
| false | true | 0 | 1 |
| true | false | 0 | 1 |
| true | true | 1 | 1 |
Bitwise operators act independently on bits and do not normalize:
6 = 110 binary
3 = 011 binary
6 & 3 = 010 = 2
6 && 3 = 1
Both can appear in conditions because both results are scalar, which makes accidentally typing & for && especially dangerous: the code may compile and sometimes choose the same branch. Short-circuit behaviour also disappears, so a guarded right operand may execute unsafely.
De Morgan’s laws help negate compound predicates:
!(a && b) is equivalent to !a || !b
!(a || b) is equivalent to !a && !b
For a value required to be inside [minimum, maximum]:
bool inside = value >= minimum && value <= maximum;
bool outside = value < minimum || value > maximum;
The second is the direct negation of the first, assuming the comparisons themselves have ordinary ordered values. Floating not-a-number values require extra care because every ordered comparison with NaN is false.
Compound Conversion
Compound assignment is broadly similar to applying an operator and assigning back, but the left operand is evaluated once and conversion back to its type is built into the operation:
small += amount;
is conceptually related to:
small = small + amount;
but is not a license to ignore narrowing. If small has a small integer type, it is promoted for addition, then the result converts back. A value outside the destination range can change unexpectedly according to the destination conversion rules.
The single evaluation matters for complex left operands:
values[next_index()] += amount;
The compound form calls next_index once. Expanding it textually as values[next_index()] = values[next_index()] + amount would call it twice and could select different elements. This is a language semantic, unlike a macro that merely repeats tokens.
Conditional Type
The conditional operator selects one of two expressions, but the unselected branch is not evaluated:
double ratio = count == 0 ? 0.0 : (double)total / count;
Only when count != 0 does division occur. The second and third operands still participate in rules that determine the conditional expression’s resulting type. Here 0.0 and the division both have type double.
Use ?: for one coherent value choice. When branches perform several effects, declare objects, or need explanatory steps, if-else communicates control more clearly. Nesting conditional operators quickly hides grouping and policy.
Shift Checklist
Before a shift such as value << count, establish:
count is non-negative
count is less than the width of the promoted left operand
left operand is unsigned when bit movement is intended
discarded high bits are acceptable
the mask uses the intended width, for example UINT32_C(1)
Right shift of unsigned values inserts zeros. Right shift of a negative signed value is implementation-defined in C17, so it is not a portable substitute for division. Left shift of signed values has representability constraints that make unsigned masks the clearer choice.
Bit numbering conventions must be stated. “Bit 0” normally means the least-significant bit, but protocol diagrams may number transmitted positions differently. Name masks from domain meaning and encode/decode at the boundary.
Evaluation Audit
When a line contains several calls, increments, assignments, or volatile accesses, split it before trying to memorize every ordering rule. Ask:
- Which subexpressions are value computations only?
- Which change observable state?
- Which sequencing relationships does the language guarantee?
- Can two unsequenced operations touch the same scalar object?
- Would a named temporary reveal intent or an error check?
This audit is particularly useful in contest code where compressed expressions seem attractive. Saving three lines is not worth undefined behaviour that appears only under optimization or on a different compiler.
Division and Remainder
For representable signed integer division, C truncates the quotient toward zero. The remainder satisfies:
(a / b) * b + (a % b) == a
when b != 0 and the division itself is representable. Therefore:
| Expression | Result |
|---|---|
17 / 5 | 3 |
17 % 5 | 2 |
-17 / 5 | -3 |
-17 % 5 | -2 |
17 / -5 | -3 |
17 % -5 | 2 |
The remainder follows the dividend’s sign or is zero. It is not always a non-negative mathematical modulus. To normalize a remainder into [0, modulus) for a positive modulus:
int remainder = value % modulus;
if (remainder < 0) {
remainder += modulus;
}
Validate modulus > 0 first. Writing (value % modulus + modulus) % modulus is compact but may overflow in the addition for extreme values and performs an unnecessary second remainder.
One signed corner is easy to miss: INT_MIN / -1 has a mathematical result one greater than INT_MAX on the usual range shape, so both / and the corresponding % case are undefined. A nonzero-divisor check alone is not the full precondition for arbitrary signed operands.
Boolean Storage
Assigning any scalar to bool normalizes zero to false and nonzero to true:
bool available = item_count;
This is valid but often less expressive than item_count != 0. The comparison states the intended predicate and keeps a later type change from silently altering meaning. Logical operators already produce 0 or 1, so condition ? true : false is usually redundant.
Bit masks should remain unsigned integers rather than bool; normalization would discard every bit except the truth of “any bit set.” Representation follows the question being asked.
Evaluation Hazards
- Using
=instead of==: enable warnings and write conditions deliberately. - Writing mathematical chained comparisons: connect two complete comparisons with
&&. - Expecting floating division from an integer expression: convert an operand before division.
- Confusing bitwise and logical operators: only
&&and||short-circuit. - Shifting by an invalid count: validate the count and prefer unsigned operands.
- Memorising precedence instead of showing intent: add clarifying parentheses.
- Confusing associativity with execution order: isolate side effects into statements.
- Modifying one object repeatedly in one unsequenced expression: split the state changes.
Expression Reasoning
- Operand types and conversions determine the meaning of arithmetic.
- Relational and logical operators produce
0or1. &&,||, and?:evaluate only the operands needed for the result.- Compound assignment evaluates its left operand once.
- Bitwise operations are best expressed on unsigned integers with documented masks.
sizeofyieldssize_tand normally does not evaluate its expression operand.- Precedence controls grouping; associativity resolves equal precedence; neither generally promises evaluation order.
- Explicit statements are safer than dense expressions with interacting side effects.
Operator Problems
Read Expressions
- Distinguish precedence, associativity, evaluation order, and sequencing.
- Why is
denominator != 0 && numerator / denominator > 2safe while the reversed order is not? - Explain the difference between
&and&&.
Evaluate by Hand
- Evaluate
17 / 5,17 % 5,-17 / 5, and-17 % 5under C17 rules. - Starting with
int n = 4, predicta,b, andnaftera = n++; b = ++n;. - Parenthesise
a | b == c && daccording to C precedence, then rewrite it for human clarity.
Repair Expressions
- Correct
if (1 <= choice <= 5). - Explain every problem with
array[i] = i++;, then rewrite it as sequenced statements. - Find the division precondition defect in
if (total / count > limit && count != 0).
Construct Logic
- Write a leap-year predicate using comparison and logical operators. Test century boundaries such as 1900 and 2000.
- Implement functions that set, clear, toggle, and test a permission bit in an
unsigned intmask. Reject invalid bit positions. - Write a program that decomposes a non-negative number of seconds into hours, minutes, and seconds using
/and%.
Defend the Result
- Explain why replacing every
ifwith?:would damage readability even though both can express selection. - Find three expressions in an existing C program whose intent would be clearer with named intermediate values or parentheses. Refactor them without changing behaviour.