Proven C Book한국어 GitHub

49 Expressions and operators

What to know first

Chapter 20, Expressions · what becomes a value, and precedence
Chapter 34, Assignment and side effects · evaluation order and sequence points
Chapter 38, Arrays · subscripting and pointer arithmetic
Chapter 46, Structures · member access

Looking back

Chapter 20 said “do not memorise the table, use parentheses”, and since then operators have appeared piecemeal wherever they were needed — shifts in chapter 28, comparisons in chapter 30, assignment in chapter 34, pointer arithmetic in chapter 38. So what is left to learn here?

A. The same material through a different lens. Until now the question was “how much of this operator do I need right here?” From here on we read each operator as a contract: what it accepts (constraints on the operands), what it hands back (result type and value category), and where the contract ends (the grey zones).

That lens earns its keep in practice. Reading someone else’s code and getting stuck; needing to know why the compiler optimised something the way it did; chasing a bug that only appears in one build — all of them come down to the contract of an operator.

The need for this chapter, and its context

Part 9 is the part of “what you already learned, again and to the end”. Its first chapter is a complete survey of operators because across the preceding forty-eight chapters operators arrived a few at a time, as needed. Gather them once here and appendix A can be left as a pure lookup table.

By the end of this chapter

Operators learned piecemeal, gathered in one place. We start with what an expression carries (value, type, value category, side effects), then the full precedence and associativity table, then operator-by-operator contracts, evaluation order and sequence points, and finally the grey zones gathered. After this chapter, appendix A is a lookup sheet and nothing more.

The questions this chapter answers

  1. Does “lvalue” simply mean “on the left”?
  2. Then when does the i++ in for (i = 0; i < n; i++) happen?
  3. What, then, should be used in C?

49.1 The four things an expression carries

Every expression in C carries four things at once. Keeping them apart is most of what this chapter teaches.

What it carriesWhat that means
ValueThe result of the computation. 2 + 3 has the value 5
TypeWhich container the value sits in — fixed at compile time (chapter 23)
Value categoryWhether it is an lvalue (it designates a place). x is; x + 1 is not
Side effectsWhether it changes an object or the outside world (chapter 34)

Table 50.1

Value category sounds like jargon, but you have been using it all along. What may appear on the left of an assignment is an lvalue (chapter 34), and what you may apply & to is an lvalue (chapter 35). An array name is an lvalue that nonetheless cannot be assigned to — a special case (chapter 38).

Q. Does “lvalue” simply mean “on the left”?

A. Historically yes (left value). Today it is more accurate to read it as an expression that designates a place — appearing on the left of an assignment is one consequence of that property. *p is an lvalue even on the right-hand side, and a const int c is an lvalue that is not a modifiable lvalue, so it cannot be assigned to.

That is why the standard says “modifiable lvalue” when it means the stricter thing. You will see that phrase in the operand column for assignment and for increment below.

The notations for writing a constant — bases, prefixes, suffixes, escapes, string literals — are gathered in chapter 20, “Every way of writing a constant”, and the rule that settles an integer constant’s type is in chapter 27. This chapter deals with the operators that join those leaves.

49.2 Precedence and associativity

The higher up, the more strongly it binds. Associativity decides which side groups first when operators of the same strength stand side by side — a - b - c is (a - b) - c because it is left-associative, and a = b = 0 is a = (b = 0) because assignment is right-associative.

groupoperatorsassoc.why it associates that way
postfix() [] . -> ++(post) --(post), compound literalL→Ra.b.c only makes sense burrowing from the left
unary++ -- + - ! ~ (type) * & sizeof alignofR→Lthe nearest one binds first: - -x, *&x
multiplicative* / %L→Rthe convention of arithmetic
additive+ -L→Rsubtraction only makes sense left-associative
shift<< >>L→Ra << 1 << 2 pushes in turn
relational< <= > >=L→Rwhich is why x < y < z differs from mathematics
equality== !=L→R
bitwise AND&L→R
bitwise XOR^L→R
bitwise OR|L→R
logical AND&&L→Rshort-circuiting only works from the left
logical OR||L→Rthe same reason
conditional?:R→Lso a ? b : c ? d : e reads as a ladder
assignment= += -= *= /= %= &= ^= |= <<= >>=R→Lso that a = b = 0 makes both zero
comma,L→Rthe left is done first and discarded

Table 50.2

49.3 Places where people slip

what was writtenhow it really groupsif that was the intent
a & b == ca & (b == c)(a & b) == c
a << 1 + 2a << (1 + 2)(a << 1) + 2
*p++*(p++)(*p)++
*p.x*(p.x)(*p).x or p->x
(int)x + y((int)x) + y(int)(x + y)
a = b = 0a = (b = 0)(as it is — right-associative)
x < y < z(x < y) < zx < y && y < z
!x & y(!x) & y!(x & y)
sizeof a + 1(sizeof a) + 1sizeof(a + 1)
a ? b : c = d(a ? b : c) = d (usually an error)a ? b : (c = d)

Table 50.3

The first two lines are counted among C’s famous design scars — that the bitwise operators bind more weakly than the comparisons is a trace of early C, before it had && and ||. So parentheses are effectively mandatory around a bit test. The standard itself notes in a footnote that a<b<c does not read as it does in mathematics.

49.4 Prefix and postfix — the same job, a different value

++ and -- can go before or after. Knowing exactly how the two differ is a good part of the power to read C expressions, so they are gathered here.

49.4.1 The contract the standard sets

prefix ++x (§6.5.4.1)postfix x++ (§6.5.3.5)the same?
Operanda modifiable lvalue of real or pointer typethe samethe same
What it does to the objectadds 1adds 1the same
The value of the expressionthe value after the changethe value before the changedifferent
Is the result an lvaluenonothe same (C++ differs — below)
How it is defined++E is equivalent to (E += 1)defined separatelydifferent

Table 50.4

The only difference is the value the expression yields. What happens to the object is identical. So in a place where the value is not used — the third slot of a for, a statement that is just i++; — the two mean exactly the same thing.

A common misconception. “It says real type, so it cannot be used on integers”

What the standard calls a real type is not “floating point”. By the classification in §6.2.5 it is integer types and real floating types together, and the only thing left out is complex. So it applies to int, char, bool, double and pointers, and by the standard not to double _Complex.

Measured, GCC lets z++ (complex) through and only says “ISO C does not support ++ and -- on complex types” when -Wpedantic is on — a place it accepts as an extension (chapter 12′s grey area).

49.4.2 When the value is settled, and when memory changes

This is the heart of the section. In x++ the event of settling the value and the event of changing memory are two different events, and the standard fixes only their order.

WhatThe standard’s sentence
Postfix (§6.5.3.5p2)the value computation of the result is sequenced before the side effect of updating the stored value of the operand
Prefix (§6.5.4.1p2 → §6.5.17.1p3)++E is (E += 1), and in an assignment the side effect of updating the left operand is sequenced after the value computations of both operands
Every expression (§6.5.1p1)the value computations of the operands are sequenced before the value computation of the result

Table 50.5

How to read that matters. The standard nails down the relative order, not the moment.

A common misconception. “A postfix increment happens at the end of the statement (at the semicolon)”

A very widespread belief. Nothing in the standard says it. What is settled is only the order — “the result’s value first, the store after” — and when the store actually happens is any time before the next sequence point: possibly before the first instruction of the next statement, possibly in the middle of the same expression.

The belief is dangerous because it invites the next thought: “so if I use it twice in one expression, the order must be settled”. It is not.

i = i++ + 1;                 /* outside the contract — undefined behaviour */
a[i] = i++;                  /* outside the contract */
printf("%d %d\n", i++, i++); /* outside the contract */

Section 6.5.1p2 nails it: if a side effect on a scalar object is unsequenced relative to another side effect on it or to a value computation using it, the behaviour is undefined. Prefix and postfix are caught alike. GCC reports “operation on ‘i’ may be undefined” through -Wsequence-point (included in -Wall) — though there are many shapes it cannot catch, so do not lean on the warning alone.

Q. Then when does the i++ in for (i = 0; i < n; i++) happen?

A. The third slot is evaluated after the body of each iteration — that is the rule of the for statement, not of the postfix operator (chapter 32). Since nobody uses the result here, switching to prefix does not change one character of the meaning.

The confusing place is where the result is used, as in while (*d++ = *s++);. There, “write what is pointed at now, and move the pointers on” sits in one expression. The two ++ operators touch different objects (d and s), so it is inside the contract. Touch the same object twice and it falls outside — that is the boundary line.

49.4.3 The truth of “prefix is faster”

In practice. ++ and -- were not created for the PDP-11

The explanation that “++ was made to use the PDP-11′s auto-increment addressing mode” still circulates. Dennis Ritchie, who made C, denied it himself. In “The Development of the C Language” he wrote that people often guess so but it is historically impossible, inasmuch as there was no PDP-11 when B was developed. The PDP-7 did have a few “auto-increment” memory cells, and that probably suggested the operators to Thompson — yet those cells were not used directly in implementing them, and a stronger motivation was probably his observation that the translation of ++x was smaller than that of x=x+1. Generalising them to both prefix and postfix was Thompson’s own doing.

So the “smaller translation” motive was real — but it was a comparison of ++x with x=x+1, not of ++x with x++. Today’s received wisdom is that fact bent once in the retelling.

What about today’s compilers? Measuring settles it.

PlaceUnoptimised (-O0)Ordinary build (-O2)
for (…; i++) vs ++i — value unusedthe generated assembly does not differ by one bytethe same
a = (*b)++ vs a = ++(*b) — value used9 instructions vs 11 — the postfix one was the shorter3 vs 3, identical

Table 50.6

Two things to read out. First, for C scalars there is no speed difference. Where the value is unused the compiler emits the same code. Second, where the value is used and the code differs, that is not “postfix is slower” but “the two compute different things” — one needs the old value, the other the new.

49.4.4 Two things change in C++

Platform note. Lvalue-ness, and user-defined types

1. Is the result an lvalue? In C neither prefix nor postfix is. C++ made the prefix one an lvalue. Measured, they part like this.

CodeC (GCC)C++ (G++)
&++xlvalue required as unary '&' operand — erroraccepted
++x = 5lvalue required as left operand of assignment — erroraccepted
&x++errorerror — postfix is a value (prvalue) in C++ too

Table 50.7

So code like ++x = 5 compiles in C++ and does not in C. A place to watch in code that crosses between the two languages — and even in C++ it is convention not to write it, being hard to read.

2. Postfix on a user-defined type makes a copy. This is the real reason the “prefer prefix” convention took root in the C++ world. A postfix operator has to return the value before the change, so for a class it makes a copy of the old state, keeps it, and returns that.

Attach a counter to the copy constructor and measure: advancing one iterator 1000 times cost the prefix form 0 copies and the postfix form 1000 copies — the same under -O2 (a copy with an observable side effect cannot be optimised away).

C does not have this problem. C’s ++ attaches only to scalars, and a scalar’s “copy” is one register, which is why the difference vanishes in the measurements above. Carry the advice “use prefix” straight into C and it becomes a rule without a reason.

Q. What, then, should be used in C?

A. This book’s recommendation.

  • Where the value is unused, make prefix the default. Not for speed but for the signal it gives the reader — “the value of this expression is not used”. It is also the habit that keeps paying when you move to C++. That said, for (i = 0; i < n; i++) has been an idiom since K&R and plenty of codebases keep it. Settle it as a team and hold to it.
  • Where the value is used, write the one you need. Old value: postfix. New value: prefix. Here the computation chooses, not taste.
  • And the one real rule — never touch the same object twice in one expression. Prefix or postfix, keep that and this operator will not hurt you. Chapter 33′s “split statements when the side effects matter” says the same thing.

49.5 Operator by operator

Now each family in turn. Every table has the same columns.

Platform note. What this chapter rests on

Checked against the expressions clause (§6.5) of ISO/IEC 9899:2024 (C23). Where an edition changed a rule, that is said in place. If you need to cite a rule, cite the published standard as appendix D explains.

49.6 Postfix operators

operatoroperandsresultgrey zonein detail
a[i] subscriptone a pointer to a complete object type, the other an integeran lvalue of the pointed-at type. a[i] is *(a+i)UB: access outside the array (including following the one-past-the-end position)chapter 38
f(...) calla function, or a pointer to onethe function’s return type; not an lvalueunspecified: the order in which arguments are evaluated. UB: arguments that disagree with the prototypechapters 21, 24, 58
s.m membera struct or union value and a member namethe member’s type; an lvalue if the left side is oneUB: reading a union member other than the one last written (the common initial sequence is an exception)chapters 46, 48
p->m membera pointer to a struct or union, and a member namethe member’s type, an lvalueUB: a null or otherwise invalid pointerchapter 46
x++ post-incrementa modifiable lvalue of real or pointer typethe value before the change; not an lvalueUB: two modifications within one sequence point; signed integer overflowchapter 32
x-- post-decrementthe samethe value before the changethe samechapter 32

Table 50.8

49.7 Unary operators

operatoroperandsresultgrey zonein detail
++x --xa modifiable lvalue of real or pointer typethe value after the change++E is (E += 1) — the overflow rules are the samechapter 32
&x address-ofa function designator, the result of [] or unary *, or an lvalue that is not a bit-field and not declared registera pointer to itbreaking the constraint is a compile errorchapter 35
*p indirectiona pointer typean lvalue of the pointed-at typeUB: null, an object whose lifetime has ended, a misaligned address, or one outside its provenancechapters 35, 37, 44
+xarithmetic typethe promoted valuechapters 20, 29
-xarithmetic typethe promoted valueUB: signed integer overflow (-INT_MIN)chapter 27
~xinteger typethe bitwise complement after promotionit happens at the promoted width — mind narrow typeschapter 28
!xscalar (arithmetic or pointer)0 or 1, of type intchapter 30
(type)x castbetween scalarsa value of that type; not an lvalueimplementation-defined: pointer↔integer conversion. UB: following a misaligned pointer; converting between function and object pointerschapters 29, 37
sizeofa complete object type or an expression. Not a function type, an incomplete type, or a bit-fielda size_t valuewith a variable length array the operand is evaluated at run time; otherwise it is not evaluatedchapters 35, 38
alignofthe name of a complete object type (not an expression)a size_t valuechapter 37

Table 50.9

49.8 Arithmetic operators

operatoroperandsresultgrey zonein detail
* multiplyarithmetic typesthe common type of the usual arithmetic conversionsUB: signed integer overflowchapters 27, 29
/ dividearithmetic typesthe sameUB: a zero divisor, INT_MIN / -1chapters 28, 50
% remainderinteger types onlythe sameUB: a zero divisor, INT_MIN % -1chapter 28
+ addboth arithmetic, or a pointer to a complete object type and an integerthe common type, or the pointer typeUB: integer overflow; pointer arithmetic beyond the arraychapters 27, 38
- subtractboth arithmetic, a pointer and an integer, or two pointers into the same arrayptrdiff_t for pointer differenceUB: subtracting pointers into different arrays; a difference that does not fit ptrdiff_tchapters 27, 38

Table 50.10

Integer division truncates toward zero (settled since C99). So, as long as the quotient is representable, (a/b)*b + a%b == a holds.

49.9 Shift operators

operatoroperandsresultgrey zonein detail
E1 << E2both of integer typethe type of the promoted left operandUB: E2 negative or at least the width of the promoted E1. UB: E1 signed and negative, or signed and positive with E1 × 2^E2 not representable in the result typechapters 7, 28
E1 >> E2both of integer typethe sameUB: E2 negative or at least the width. implementation-defined: the result when E1 is signed and negativechapters 7, 28

Table 50.11

A common misconception. “C23 mandated two’s complement, so shifting negatives is defined now”

Two’s complement representation was indeed mandated (C23). The shift clause, however, is unchanged — left-shifting a signed negative value is still UB in C23, and right-shifting a negative value is implementation-defined. Most compilers do an arithmetic shift, but that is a promise of the implementation, not of the standard.

The practical rule is one line: shift on unsigned types. If a signed value must be shifted, move it to an unsigned type, shift, and move it back. And always check that the count is within 0 <= n < width.

49.10 Relational and equality operators

operatoroperandsresultgrey zonein detail
< <= > >=both real types, or two pointers to compatible object types0 or 1, of type intUB: ordering two pointers that do not belong to the same array (or object)chapters 30, 37
== !=both arithmetic, compatible pointers, one a void*, one a null pointer constant or nullptr_t, and so on0 or 1, intunspecified: whether a one-past-the-end pointer compares equal to a pointer to the object that followschapters 30, 36

Table 50.12

The two families have different contracts. Equality may be tested between different objects, while ordering only means something within one array. An object that is not an array is treated as an array of length one. For reals, +0.0 and -0.0 compare equal (chapter 50).

49.11 Bitwise and logical operators

operatoroperandsresultgrey zonein detail
& ^ |both of integer typethe common type of the usual arithmetic conversionsthey reach the sign bit, so use them on unsigned typeschapter 28
&&both scalar0 or 1, intguaranteed: if the left is 0 the right is not evaluated, and there is a sequence point between themchapter 30
||both scalar0 or 1, intguaranteed: if the left is non-zero the right is not evaluatedchapter 30

Table 50.13

49.12 Conditional, assignment, comma

operatoroperandsresultgrey zonein detail
c ? a : bc scalar. a and b both arithmetic, or compatible structs or unions, or both void, or compatible pointers, or one a null pointer constantone common type for both branchesguaranteed: a sequence point after the condition; the branch not chosen is not evaluatedchapter 33
=the left must be a modifiable lvaluethe value converted to the left’s type. Not an lvalueUB: assignment between overlapping objects (exact overlap with compatible types is allowed); two modifications within one sequence pointchapter 23
compound op=E1 op= E2as E1 = E1 op E2, except that E1 is evaluated oncethe grey zones of the operation itself (overflow, zero divisor) still applychapter 33
, commaany two expressionsthe type and value of the rightguaranteed: the left is evaluated and discarded, then a sequence point. The comma in an argument list is not this operatorchapter 33

Table 50.14

49.13 Evaluation order and sequence points

Precedence is a rule about grouping, not about the order in time (chapters 13 and 33). Order is guaranteed in exactly five places.

Nowhere else is any order guaranteed.

expressionverdict
f() + g()unspecified — which is called first is not settled
h(f(), g())unspecified — argument evaluation order
i = i++UBi is modified twice within one sequence point
a[i] = i++UB — the same reason
i++ + i++UB
f(i++, i++)UB — there is no sequence point between arguments
i++, i++fine — the comma operator has a sequence point
(i++) && (i++)fine — && has a sequence point

Table 50.15

Since C11 the standard states these rules with a sequenced-before relation rather than with sequence points, but the practical conclusion is the same — do not touch the same object twice within one expression. GCC’s -Wsequence-point catches the common cases, but not all of them.

49.14 The grey zones gathered

Only the operator-related entries, sorted by chapter 52′s three words. The full lists are in annex J of the standard.

49.14.1 Undefined behaviour (UB)

placecondition
/ %a zero divisor; INT_MIN / -1, INT_MIN % -1
+ - * ++ --signed integer overflow
<<a count that is negative or at least the width; left-shifting a signed negative; a signed positive whose result does not fit
>>a count that is negative or at least the width
* indirectionnull, an object past its lifetime, a misaligned address, a pointer outside its provenance
[]access outside the array
+ - pointer arithmetica result outside the array (one past the end included)
- between pointerspointers into different arrays
< <= > >=ordering pointers that do not belong to the same array or object
. -> on unionsreading a member other than the one last written (the common initial sequence excepted)
expressions in generalmodifying the same object twice within one sequence point, or modifying it and reading it for another purpose

Table 50.16

49.14.2 Unspecified

placewhat is not settled
subexpressionsthe evaluation order of f() + g()
function argumentsthe order among arguments
== !=whether a one-past-the-end pointer compares equal to a pointer to the next object
padding bytesthe values of a struct’s padding — the reason not to compare with memcmp (chapter 47)

Table 50.17

49.14.3 Implementation-defined

placewhat the implementation settles
>>the result of shifting a signed negative value (usually an arithmetic shift)
integer conversionthe result of converting a value that does not fit a signed type (still so in C23)
pointer ↔ integerthe result of the conversion and whether it round-trips (only the round trip through uintptr_t, where it exists, is guaranteed)
charsigned or unsigned — which splits >> and comparison (chapter 9)
bit-fieldsthe order of allocation and the padding

Table 50.18

49.15 Things that are not operators

The same characters appear in the grammar without being operators.

shapewhat it really isin detail
the comma in f(a, b)a separator of the call syntax — no sequence pointchapter 33
the comma in int a, b;a separator of declaration syntaxchapter 23
the comma in {1, 2}a separator in an initialiser listchapters 38, 46
(type){...}a compound literal — not a cast but syntax that makes an objectchapter 47
the parentheses of sizeof(int)syntax wrapping a type name — not a callchapter 35
# ##preprocessor operators — they act in a different phase of translationchapter 57
the dot in {.x = 1}designated-initialiser syntax, not member accesschapter 46
the star in int *p;declarator syntax, not indirectionchapter 60

Table 50.19

Recap

What to keepThe point
What an expression carriesValue, type, value category, side effects
PrecedenceA grouping rule, not an order of computation
AssociativityWhich side groups first among equals
Where order is guaranteedOnly &&, ||, ?:, the comma operator, and function calls
Grey zonesRead UB, unspecified and implementation-defined as distinct words
Working ruleParenthesise, and never touch the same object twice in one expression

Table 50.20

You can now read an operator as a contract. The chapters that follow go into the places where those contracts get subtlest — the mathematics of approximation (chapter 50), handling failure (chapter 51), and what happens when a contract is broken (chapter 52).