<< Home

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
⚠️ Integer overflow is undefined behavior; do not rely on wraparound in portable C. For example, 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
💡 Many teams prefer Yoda comparisons for constants; 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.

ExpressionResult
!01
!50
0 && any0
nonzero || any1
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
⚠️ Do not modify the same scalar multiple times without an intervening sequence; 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
💡 Compound assignments may change the effective type through usual arithmetic conversions; keep operands in a known type when precision matters.

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);
⚠️ Left shifting into or past the sign bit of a signed type has undefined behavior. Prefer 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
💡 Group related flags into a single 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 OperatorsAssociativity
Postfix() [] p->m p.m x++ x--left
Unary++x --x + - ! ~ (type) * & sizeofright
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
💡 When precedence knowledge is not obvious to a fresh reader, add parentheses; readability beats cleverness.

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
⚠️ The comma operator , 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.