Chapter 5: Operators and Expressions
C uses a compact set of operators that combine values into expressions. This chapter groups them into arithmetic; relational; logical; increment and decrement; assignment and compound assignment; and bitwise. You also learn how precedence and associativity affect the result; and why clear parentheses are worth using.
Arithmetic, relational, and logical operators
Arithmetic operators work on numeric types. Relational operators compare values. Logical operators combine Boolean results that are represented by integers in C; zero means false and nonzero means true.
The +, -, *, /, % arithmetic operators
As you would expect + adds; - subtracts; * multiplies; / divides; % gives the remainder for integers. With integers, / truncates toward zero. With floating types, % is not defined.
int a = 7, b = 3;
printf("%d %d %d %d %d\n",
a + b, // 10
a - b, // 4
a * b, // 21
a / b, // 2 (truncates)
a % b); // 1
double x = 7.0, y = 3.0;
printf("%.1f %.1f %.1f\n", x + y, x / y, x * y); // 10.0 2.3 21.0
INT_MAX + 1 has undefined behavior.
Integer division and % edge cases
For signed integers, C defines a / b to truncate toward zero; the remainder follows the rule (a / b) * b + (a % b) == a. If b is zero, the behavior is undefined.
printf("%d %d\n", 7 / -3, 7 % -3); // -2 1
printf("%d %d\n", -7 / 3, -7 % 3); // -2 -1
The <, <=, >, >=, ==, != relational operators
Relational operators compare two operands and produce int results; zero or one. Beware of accidental assignment; write if (a == b) rather than if (a = b).
int a = 5, b = 9;
printf("%d %d %d\n", a < b, a == b, a != b); // 1 0 1
if (0 == x) makes accidental = a compile error in some settings.
The &&, ||, ! logical operators
Logical operators work with truthy or falsy integer values. && and || use short circuit evaluation; the right operand may not be evaluated.
| Expression | Result |
!0 | 1 |
!5 | 0 |
0 && any | 0 |
nonzero || any | 1 |
int calls = 0;
int f(void){ calls++; return 0; }
int x = 1 || f(); // f() not called, short circuit
int y = 0 || f(); // f() called
printf("calls=%d x=%d y=%d\n", calls, x, y); // calls=1 x=1 y=0
Increment and decrement, assignment, and compound assignment
These operators update variables. Pre and post forms of increment and decrement have different values in expressions. Assignment stores a value; compound forms apply an operation and store the result.
Pre- and post- incrementing and decrementing
++i increments then yields the new value. i++ yields the old value then increments. The same idea applies to --.
int i = 3;
printf("%d %d %d\n", ++i, i, i++); // 4 4 4
printf("%d\n", i); // 5
i = i++ + ++i has undefined behavior.
Using = for assignment and chaining
Assignment returns the assigned value; this allows chaining and use in conditions. Use it sparingly for clarity.
int a, b, c;
a = b = c = 42;
if ((a = getchar()) != EOF) { /* use a */ }
The +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>= compound assignment operators
These combine an operation with assignment; the left operand is evaluated only once.
int x = 5;
x += 3; // 8
x *= 2; // 16
x <<= 1; // 32
Lvalues and rvalues in assignment
The left operand of = must be a modifiable lvalue. You cannot assign to an rvalue or to something declared with const.
int x;
const int y = 3;
/* 3 = x; // not allowed */
/* y = 4; // not allowed */
x = y; // allowed
Bitwise operations
Bitwise operators work on the bit patterns of integer types. Use unsigned types when you care about precise shifts and masks.
The &, |, ^, ~, <<, >> operators
& computes bitwise and; | computes bitwise or; ^ computes bitwise exclusive or; ~ flips all bits; << shifts left; >> shifts right.
unsigned u = 0b0001'0110; // 0x16
printf("and=%02X or=%02X xor=%02X not=%02X\n",
(u & 0x0F), (u | 0x01), (u ^ 0xFF), (unsigned)~u);
printf("shl=%02X shr=%02X\n", u << 1, u >> 1);
unsigned for shifts.
Working with masks and flags
Masks select or update specific bits. Define named masks to keep intent clear.
enum {
FLAG_READ = 1u << 0,
FLAG_WRITE = 1u << 1,
FLAG_EXEC = 1u << 2
};
unsigned perms = 0;
perms |= FLAG_READ | FLAG_WRITE; // set bits
perms &= ~FLAG_WRITE; // clear bit
int can_exec = (perms & FLAG_EXEC) != 0; // test bit
unsigned and provide helper functions; this keeps call sites small and expressive.
Right shift behavior
Right shift on unsigned performs a logical shift; zeros fill from the left. Right shift on signed may perform an arithmetic shift that keeps the sign bit, or a logical shift; this is implementation defined.
unsigned uu = 0xF0u; // 11110000
int ss = -16; // representation is two's complement on most systems…
printf("%u\n", uu >> 2); // 00111100
/* printf("%d\n", ss >> 2); // arithmetic or logical; implementation defined */
Operator precedence and associativity
Precedence chooses which operators group more tightly; associativity chooses how operators of the same precedence group. Parentheses always make the intent explicit.
Summary table
| Higher → lower | Operators | Associativity |
| Postfix | () [] p->m p.m x++ x-- | left |
| Unary | ++x --x + - ! ~ (type) * & sizeof | right |
| Multiplicative | * / % | left |
| Additive | + - | left |
| Shifts | << >> | left |
| Relational | < <= > >= | left |
| Equality | == != | left |
| Bitwise and | & | left |
| Bitwise xor | ^ | left |
| Bitwise or | | | left |
| Logical and | && | left |
| Logical or | || | left |
| Conditional | ?: | right |
| Assignment | = += -= *= /= %= &= ^= |= <<= >>= | right |
| Comma | , | left |
Using parentheses for clarity
Parentheses remove ambiguity and document intent. This is especially helpful when mixing relational, logical, and bitwise operators.
int a = 2, b = 3, c = 4;
int r1 = a + b * c; // 14
int r2 = (a + b) * c; // 20
int r3 = (a & b) == 2 && c; // clear grouping
Precedence versus evaluation order
Precedence does not fix the order in which subexpressions are evaluated. In C, the order of evaluation of function arguments and of many operands is unspecified; do not rely on a particular order.
int f(const char *s){ puts(s); return 1; }
/* Do not assume the call order for these arguments */
int z = f("left") + f("right"); // either print order is permitted
, is a real operator with the lowest precedence; it guarantees left to right evaluation inside a single expression. It is different from the comma that separates function arguments.
Use small expressions with clear parentheses; test limits with unsigned when doing bit tricks; keep undefined or implementation defined corners out of production code.