Polyglot
Languages C operators
C § operators

Operators

The operator surface of C is large for a language of its size: forty-five operators across thirteen levels of precedence, with associativity rules, conversion rules, and sequencing rules that interact. Most operators are familiar from the algebraic notation they imitate; the chief sources of difficulty are the ones that work on pointers, the bitwise operators, and the rules governing the order in which subexpressions are evaluated. The standard distinguishes operator precedence — which operands an operator binds — from operator evaluation order, which is the temporal order in which the operations are performed. The two coincide less often than the syntax suggests.

Arithmetic operators

The five binary arithmetic operators are +, -, *, /, and %. Division of two integers truncates toward zero; the modulus has the sign of the dividend:

 7 /  2  ==  3
-7 /  2  == -3       // since C99: truncation toward zero
 7 % -2  ==  1
-7 %  2  == -1

Division by zero is undefined behaviour for integer operands and yields the IEC 60559 result (inf, -inf, or nan) for floating-point operands when __STDC_IEC_559__ is defined.

Unary - and unary + are arithmetic. Unary + performs the integer promotions and otherwise has no effect; it is occasionally written for symmetry, more often elided.

Increment and decrement

++x and --x evaluate to the new value of x; x++ and x-- evaluate to the old value. Both forms have the side effect of modifying x:

int i = 5;
int a = ++i;     // i is 6; a is 6
int b = i++;     // i is 7; b is 6

Embedding increment within a larger expression is a frequent source of subtle bugs: the standard does not specify an order in which the side effects of independent subexpressions take place, and combinations such as arr[i] = i++; invoke undefined behaviour because the modification of i is unsequenced relative to the reading of i in the array index.

Comparison and logical operators

The six relational operators (<, <=, >, >=, ==, !=) yield int values 0 (false) or 1 (true). The two logical connectives && and || short-circuit: the right operand is evaluated only if the left does not determine the result.

if (p != NULL && p->valid) /* p->valid is read only when p is non-null */;

Unary ! yields 0 if its operand is non-zero and 1 if its operand is zero. The result type is always int.

C does not have a Boolean type below the level of int for the purposes of relational and logical operators; the result of a < b is an int, even though it can only be 0 or 1. With <stdbool.h>, _Bool is a distinct integer type that participates in the integer promotions.

Bitwise operators

Six operators act on the bit representation of integer operands.

OperatorMeaning
&Bitwise AND
|Bitwise OR
^Bitwise XOR
~Bitwise NOT (unary)
<<Left shift
>>Right shift

The operands undergo the usual arithmetic conversions before evaluation. Shifts of signed values:

  • Left-shifting a negative value is undefined.
  • Left-shifting a non-negative value where the result exceeds the type’s maximum is undefined.
  • Right-shifting a negative value is implementation-defined (most implementations use arithmetic shift, replicating the sign bit).

Idiomatic uses of bitwise operators include flag manipulation, mask construction, and the encoding of small fixed-format integers. The combination x & -x isolates the lowest set bit; x & (x - 1) clears it; popcount(x & (x - 1)) == popcount(x) - 1 is a useful check.

enum flags { READ = 1u << 0, WRITE = 1u << 1, EXEC = 1u << 2 };

unsigned mode = READ | WRITE;
if (mode & WRITE)         /* permission granted */;
mode &= ~WRITE;           /* clear the WRITE bit */
mode ^= EXEC;             /* toggle EXEC */

Assignment

The simple assignment operator = evaluates the right operand, converts it to the type of the left, stores the result, and yields the stored value as the result of the expression. Because assignment is an expression, it nests:

int a, b, c;
a = b = c = 0;        // c gets 0, then b, then a

Eleven compound assignment operators combine an operation with assignment: +=, -=, *=, /=, %=, <<=, >>=, &=, ^=, |=. Each evaluates the left operand exactly once:

arr[expensive()] += 1;     // expensive() is called once, not twice

The standard guarantees that this single-evaluation property holds even when the left operand has side effects.

Pointer operators

Five operators concern pointers:

  • &x — yields the address of x as a pointer to the type of x.
  • *p — dereferences the pointer and yields the object it points to as an lvalue.
  • p[i] — equivalent to *(p + i).
  • p->m — equivalent to (*p).m; member access through a pointer.
  • p + n, p - n, p1 - p2 — pointer arithmetic in units of the pointed-to type.

Pointer arithmetic is well-defined only within a single array (or one past its end). Subtraction yields a ptrdiff_t. The full treatment is in Pointers.

Member access

Two operators access members:

  • s.m — direct member access on a structure or union object.
  • p->m — member access through a pointer.

Both yield lvalues that may be assigned to (if the underlying member is not const) or addressed.

sizeof and alignof

The unary sizeof operator yields the size of its operand in bytes. The operand may be an expression (in which case it is not evaluated; only its type is considered) or a parenthesised type name:

sizeof(int)              // a constant; for a 32-bit int, 4
sizeof x                 // size of the type of x
sizeof arr / sizeof *arr // number of elements in arr (only when arr is a true array)

The result is of type size_t, defined in <stddef.h>. The standard guarantees sizeof(char) == 1.

C11 introduced _Alignof (with the convenience macro alignof from <stdalign.h>); C23 promoted alignof to a keyword. It yields the alignment requirement, in bytes, of its type-name operand.

The conditional operator

The ternary ?: operator selects between two expressions:

int max = (a > b) ? a : b;

Only one of the second and third operands is evaluated. The two are subject to the usual arithmetic conversions when both are arithmetic; for pointer operands, both must point to compatible types (or one must be a null pointer constant or void *).

The conditional is a useful brevity device but is not a substitute for if; nested conditionals quickly become illegible and the language has no early-exit form within an expression.

The comma operator

The comma operator evaluates the left operand, discards its value, evaluates the right operand, and yields the right operand’s value:

for (i = 0, j = n - 1; i < j; ++i, --j) /* swap */ ;

The principal use of the comma operator is in for loop initialisation and step expressions, where it permits multiple side effects in a context that allows only one expression. Outside that context the comma operator is rare; ordinary commas in argument lists and declarators are separators, not the operator.

Sequence points and unsequenced evaluations

A sequence point is a moment in evaluation at which all side effects of preceding expressions are complete and no side effects of subsequent expressions have begun. Sequence points occur:

  • At the end of a full expression (the semicolon at the end of a statement; the controlling expression of if, while, for, do, or switch; the operand of return).
  • After the evaluation of the first operand of &&, ||, ?:, and ,.
  • After the evaluation of the function-designator and arguments of a function call, but before the call itself.

Two side effects are unsequenced if no sequence point separates them. The standard prohibits two unsequenced modifications of the same scalar object, and prohibits an unsequenced modification of a scalar with a value computation that uses the same scalar:

i = i++;         // undefined: two modifications of i
a[i] = i++;      // undefined: modification of i unsequenced with value
                 //            computation of i in a[i]
printf("%d %d\n", i++, i++);   // undefined: argument-evaluation order
                               //            unspecified, modifications unsequenced

The C11 standard refined the model with the additional notion of indeterminate sequencing: function calls are indeterminately sequenced, meaning that one completes before the other but the order is unspecified. The practical advice is unchanged: do not modify the same object twice in a single expression, and do not modify an object whose value is also being read in the same expression.

Lvalues and rvalues

An lvalue is an expression that designates an object: x, *p, arr[i], s.m are lvalues. An rvalue (the standard’s term is value) is an expression that yields a value but not an object: x + 1, (int)x, the literal 42. Assignment requires a modifiable lvalue on the left; the standard excludes lvalues of array type, of incomplete type, of const-qualified type, and certain others from being modifiable.

An array used in most expression contexts undergoes array-to-pointer conversion, yielding a non-lvalue pointer. The principal reason that arrays cannot be assigned is precisely this: by the time the assignment would be applied, the array has decayed to a pointer rvalue.

Operator precedence

The full precedence table from the standard, highest to lowest:

LevelOperatorsAssociativity
1++ -- (postfix), () (call), [], ., ->, compound literal (type){…}left
2++ -- (prefix), + - (unary), ! ~, * (deref), & (addr), (type), sizeof, alignof, _Alignofright
3* / %left
4+ - (binary)left
5<< >>left
6< <= > >=left
7== !=left
8& (bitwise)left
9^left
10|left
11&&left
12||left
13?:right
14=, +=, -=, *=, /=, %=, <<=, >>=, &=, ^=, |=right
15,left

Three precedence facts are worth memorising because they cause frequent bugs:

  1. & and | (bitwise) bind less tightly than == and !=. if (x & MASK == 0) does not test what it appears to.
  2. Shifts bind less tightly than addition. 1 << n + 1 is 1 << (n + 1).
  3. Postfix ++/-- binds more tightly than *. *p++ is *(p++), not (*p)++.

When in doubt, parenthesise. The standard does not penalise redundant parentheses, and the alternative is to invite the bug.