50 Expressions and operators
What to know first
Looking back
Chapter 21 said “do not memorise the table, use parentheses”, and since then operators have appeared piecemeal wherever they were needed — shifts in chapter 29, comparisons in chapter 31, assignment in chapter 35, pointer arithmetic in chapter 39. 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
By the end of this chapter
The questions this chapter answers
- Does “lvalue” simply mean “on the left”?
- Then when does the
i++infor (i = 0; i < n; i++)happen? - What, then, should be used in C?
50.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 carries | What that means |
|---|---|
| Value | The result of the computation. 2 + 3 has the value 5 |
| Type | Which container the value sits in — fixed at compile time (chapter 24) |
| Value category | Whether it is an lvalue (it designates a place). x is; x + 1 is not |
| Side effects | Whether it changes an object or the outside world (chapter 35) |
Table 50.1 — What an expression carries
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 35), and what you may apply & to is an lvalue (chapter 36). An array name is an lvalue that nonetheless cannot be assigned to — a special case (chapter 39).
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 21, “Every way of writing a constant”, and the rule that settles an integer constant’s type is in chapter 28. This chapter deals with the operators that join those leaves.
50.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.
| group | operators | assoc. | why it associates that way |
|---|---|---|---|
| postfix | () [] . -> ++(post) --(post), compound literal | L→R | a.b.c only makes sense burrowing from the left |
| unary | ++ -- + - ! ~ (type) * & sizeof alignof | R→L | the nearest one binds first: - -x, *&x |
| multiplicative | * / % | L→R | the convention of arithmetic |
| additive | + - | L→R | subtraction only makes sense left-associative |
| shift | << >> | L→R | a << 1 << 2 pushes in turn |
| relational | < <= > >= | L→R | which 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→R | short-circuiting only works from the left |
| logical OR | || | L→R | the same reason |
| conditional | ?: | R→L | so a ? b : c ? d : e reads as a ladder |
| assignment | = += -= *= /= %= &= ^= |= <<= >>= | R→L | so that a = b = 0 makes both zero |
| comma | , | L→R | the left is done first and discarded |
Table 50.2 — Associativity, by operator group
50.3 Places where people slip#
| what was written | how it really groups | if that was the intent |
|---|---|---|
a & b == c | a & (b == c) | (a & b) == c |
a << 1 + 2 | a << (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 = 0 | a = (b = 0) | (as it is — right-associative) |
x < y < z | (x < y) < z | x < y && y < z |
!x & y | (!x) & y | !(x & y) |
sizeof a + 1 | (sizeof a) + 1 | sizeof(a + 1) |
a ? b : c = d | (a ? b : c) = d (usually an error) | a ? b : (c = d) |
Table 50.3 — Where precedence groups it differently than intended
A table is only a table. Running the same thing makes it stick far faster — the demo below prints the parenthesised and unparenthesised forms side by side.
examples/ch49/precedence.c
// 우선순위·결합성·짧은 회로를 눈으로 확인한다.
// 표를 외우는 대신, 괄호를 넣은 것과 안 넣은 것을 나란히 찍어 본다.
#include <stdio.h>
static int calls = 0;
static int loud(int value) // 불릴 때마다 흔적을 남긴다
{
calls += 1;
return value;
}
int main(void)
{
int x = 6;
puts("1. precedence --- what binds tighter");
printf(" 2 + 3 * 4 = %d (same as 2 + (3 * 4))\n", 2 + 3 * 4);
printf(" (2 + 3) * 4 = %d\n", (2 + 3) * 4);
printf(" 1 << 2 + 3 = %d (same as 1 << (2 + 3): + binds tighter than <<)\n",
1 << (2 + 3));
puts("2. the classic trap --- == binds tighter than &");
printf(" x & 1 == 0 is read as x & (1 == 0) = %d\n", x & (1 == 0));
printf(" what was meant: (x & 1) == 0 = %d\n", (x & 1) == 0);
puts("3. associativity --- which side groups first");
printf(" 10 - 4 - 3 = %d (left to right: (10 - 4) - 3)\n", 10 - 4 - 3);
int a, b;
a = b = 5; // 대입은 오른쪽부터 묶인다
printf(" a = b = 5 gives a=%d b=%d (right to left)\n", a, b);
puts("4. comparison does not chain the way mathematics does");
printf(" 1 < 2 < 3 = %d ((1 < 2) is 1, and 1 < 3 is true)\n", (1 < 2) < 3);
printf(" 3 > 2 > 1 = %d ((3 > 2) is 1, and 1 > 1 is false)\n", (3 > 2) > 1);
puts("5. short circuit --- the right side may never run");
calls = 0;
if (0 && loud(1))
puts(" not reached");
printf(" after 0 && loud(1): loud() was called %d time(s)\n", calls);
calls = 0;
if (1 || loud(1))
(void)0;
printf(" after 1 || loud(1): loud() was called %d time(s)\n", calls);
return 0;
}
Output
1. precedence --- what binds tighter
2 + 3 * 4 = 14 (same as 2 + (3 * 4))
(2 + 3) * 4 = 20
1 << 2 + 3 = 32 (same as 1 << (2 + 3): + binds tighter than <<)
2. the classic trap --- == binds tighter than &
x & 1 == 0 is read as x & (1 == 0) = 0
what was meant: (x & 1) == 0 = 1
3. associativity --- which side groups first
10 - 4 - 3 = 3 (left to right: (10 - 4) - 3)
a = b = 5 gives a=5 b=5 (right to left)
4. comparison does not chain the way mathematics does
1 < 2 < 3 = 1 ((1 < 2) is 1, and 1 < 3 is true)
3 > 2 > 1 = 0 ((3 > 2) is 1, and 1 > 1 is false)
5. short circuit --- the right side may never run
after 0 && loud(1): loud() was called 0 time(s)
after 1 || loud(1): loud() was called 0 time(s)
Look closely at the fourth group. 1 < 2 < 3 is true while 3 > 2 > 1 is false. Read with the habits of mathematics both should be true, but C groups two at a time from the left and turns each comparison’s result into a number, 0 or 1, which it then hands to the next comparison. The fifth group is the short circuit of && and || — when the left side settles the answer, the right side is not evaluated at all, so anything that “must happen” placed on the right quietly disappears (chapter 33).
The first two lines of the table above 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.
50.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.
50.4.1 The contract the standard sets#
prefix ++x (§6.5.4.1) | postfix x++ (§6.5.3.5) | the same? | |
|---|---|---|---|
| Operand | a modifiable lvalue of real or pointer type | the same | the same |
| What it does to the object | adds 1 | adds 1 | the same |
| The value of the expression | the value after the change | the value before the change | different |
| Is the result an lvalue | no | no | the same (C++ differs — below) |
| How it is defined | ++E is equivalent to (E += 1) | defined separately | different |
Table 50.4 — Prefix and postfix increment, compared
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 13′s grey area).
50.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.
| What | The 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 — The standard’s sentences that set sequence points
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 33). 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.
50.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.
| Place | Unoptimised (-O0) | Ordinary build (-O2) |
|---|---|---|
for (…; i++) vs ++i — value unused | the generated assembly does not differ by one byte | the same |
a = (*b)++ vs a = ++(*b) — value used | 9 instructions vs 11 — the postfix one was the shorter | 3 vs 3, identical |
Table 50.6 — Results that differ by optimisation level
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.
50.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.
| Code | C (GCC) | C++ (G++) |
|---|---|---|
&++x | lvalue required as unary '&' operand — error | accepted |
++x = 5 | lvalue required as left operand of assignment — error | accepted |
&x++ | error | error — postfix is a value (prvalue) in C++ too |
Table 50.7 — How C and C++ rule on the same code
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 34′s “split statements when the side effects matter” says the same thing.
50.5 Operator by operator#
Now each family in turn. Every table has the same columns.
- Operands — what the standard requires. Violate it and the compiler must diagnose it (a constraint violation).
- Result — the type of the value, and whether it is an lvalue.
- Grey zone — in the three words of chapter 54. UB is undefined behaviour, unspecified means one of several possibilities with no rule saying which, and implementation-defined means the implementation chooses and documents it.
- More — the chapter that tells the story.
Platform note. What this chapter rests on
50.6 Postfix operators#
| operator | operands | result | grey zone | in detail |
|---|---|---|---|---|
a[i] subscript | one a pointer to a complete object type, the other an integer | an 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 39 |
f(...) call | a function, or a pointer to one | the function’s return type; not an lvalue | unspecified: the order in which arguments are evaluated. UB: arguments that disagree with the prototype | chapters 22, 25 and 63 |
s.m member | a struct or union value and a member name | the member’s type; an lvalue if the left side is one | UB: reading a union member other than the one last written (the common initial sequence is an exception) | chapters 47 and 49 |
p->m member | a pointer to a struct or union, and a member name | the member’s type, an lvalue | UB: a null or otherwise invalid pointer | chapter 47 |
x++ post-increment | a modifiable lvalue of real or pointer type | the value before the change; not an lvalue | UB: two modifications within one sequence point; signed integer overflow | chapter 33 |
x-- post-decrement | the same | the value before the change | the same | chapter 33 |
Table 50.8 — The contract of the arithmetic operators
50.7 Unary operators#
| operator | operands | result | grey zone | in detail |
|---|---|---|---|---|
++x --x | a modifiable lvalue of real or pointer type | the value after the change | ++E is (E += 1) — the overflow rules are the same | chapter 33 |
&x address-of | a function designator, the result of [] or unary *, or an lvalue that is not a bit-field and not declared register | a pointer to it | breaking the constraint is a compile error | chapter 36 |
*p indirection | a pointer type | an lvalue of the pointed-at type | UB: null, an object whose lifetime has ended, a misaligned address, or one outside its provenance | chapters 36, 38 and 45 |
+x | arithmetic type | the promoted value | chapters 21 and 30 | |
-x | arithmetic type | the promoted value | UB: signed integer overflow (-INT_MIN) | chapter 28 |
~x | integer type | the bitwise complement after promotion | it happens at the promoted width — mind narrow types | chapter 29 |
!x | scalar (arithmetic or pointer) | 0 or 1, of type int | chapter 31 | |
(type)x cast | between scalars | a value of that type; not an lvalue | implementation-defined: pointer↔integer conversion. UB: following a misaligned pointer; converting between function and object pointers | chapters 30 and 38 |
sizeof | a complete object type or an expression. Not a function type, an incomplete type, or a bit-field | a size_t value | with a variable length array the operand is evaluated at run time; otherwise it is not evaluated | chapters 36 and 39 |
alignof | the name of a complete object type (not an expression) | a size_t value | chapter 38 |
Table 50.9 — The contract of the relational operators
50.8 Arithmetic operators#
| operator | operands | result | grey zone | in detail |
|---|---|---|---|---|
* multiply | arithmetic types | the common type of the usual arithmetic conversions | UB: signed integer overflow | chapters 28 and 30 |
/ divide | arithmetic types | the same | UB: a zero divisor, INT_MIN / -1 | chapters 29 and 52 |
% remainder | integer types only | the same | UB: a zero divisor, INT_MIN % -1 | chapter 29 |
+ add | both arithmetic, or a pointer to a complete object type and an integer | the common type, or the pointer type | UB: integer overflow; pointer arithmetic beyond the array | chapters 28 and 39 |
- subtract | both arithmetic, a pointer and an integer, or two pointers into the same array | ptrdiff_t for pointer difference | UB: subtracting pointers into different arrays; a difference that does not fit ptrdiff_t | chapters 28 and 39 |
Table 50.10 — The contract of the logical operators
Integer division truncates toward zero (settled since C99). So, as long as the quotient is representable, (a/b)*b + a%b == a holds.
50.9 Shift operators#
| operator | operands | result | grey zone | in detail |
|---|---|---|---|---|
E1 << E2 | both of integer type | the type of the promoted left operand | UB: 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 type | chapters 6 and 29 |
E1 >> E2 | both of integer type | the same | UB: E2 negative or at least the width. implementation-defined: the result when E1 is signed and negative | chapters 6 and 29 |
Table 50.11 — The contract of the bitwise operators
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.
50.10 Relational and equality operators#
| operator | operands | result | grey zone | in detail |
|---|---|---|---|---|
< <= > >= | both real types, or two pointers to compatible object types | 0 or 1, of type int | UB: ordering two pointers that do not belong to the same array (or object) | chapters 31 and 38 |
== != | both arithmetic, compatible pointers, one a void*, one a null pointer constant or nullptr_t, and so on | 0 or 1, int | unspecified: whether a one-past-the-end pointer compares equal to a pointer to the object that follows | chapters 31 and 37 |
Table 50.12 — The contract of the assignment operators
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 52).
50.11 Bitwise and logical operators#
| operator | operands | result | grey zone | in detail |
|---|---|---|---|---|
& ^ | | both of integer type | the common type of the usual arithmetic conversions | they reach the sign bit, so use them on unsigned types | chapter 29 |
&& | both scalar | 0 or 1, int | guaranteed: if the left is 0 the right is not evaluated, and there is a sequence point between them | chapter 31 |
|| | both scalar | 0 or 1, int | guaranteed: if the left is non-zero the right is not evaluated | chapter 31 |
Table 50.13 — The contract of the member and subscript operators
50.12 Conditional, assignment, comma#
| operator | operands | result | grey zone | in detail |
|---|---|---|---|---|
c ? a : b | c scalar. a and b both arithmetic, or compatible structs or unions, or both void, or compatible pointers, or one a null pointer constant | one common type for both branches | guaranteed: a sequence point after the condition; the branch not chosen is not evaluated | chapter 34 |
= | the left must be a modifiable lvalue | the value converted to the left’s type. Not an lvalue | UB: assignment between overlapping objects (exact overlap with compatible types is allowed); two modifications within one sequence point | chapter 24 |
compound op= | E1 op= E2 | as E1 = E1 op E2, except that E1 is evaluated once | the grey zones of the operation itself (overflow, zero divisor) still apply | chapter 34 |
, comma | any two expressions | the type and value of the right | guaranteed: the left is evaluated and discarded, then a sequence point. The comma in an argument list is not this operator | chapter 34 |
Table 50.14 — The contract of the remaining operators
50.13 Evaluation order and sequence points#
Precedence is a rule about grouping, not about the order in time (chapters 14 and 34). Order is guaranteed in exactly five places.
- between the left and right of
&& - between the left and right of
|| - between the condition of
?:and the branch chosen - between the left and right of the comma operator
- between the evaluation of a call’s arguments and the execution of the function body (though the order among the arguments is unspecified)
Nowhere else is any order guaranteed.
| expression | verdict |
|---|---|
f() + g() | unspecified — which is called first is not settled |
h(f(), g()) | unspecified — argument evaluation order |
i = i++ | UB — i 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 — Verdicts on expressions seen often
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.
50.14 The grey zones gathered#
Only the operator-related entries, sorted by chapter 54′s three words. The full lists are in annex J of the standard.
50.14.1 Undefined behaviour (UB)#
| place | condition |
|---|---|
/ % | 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 |
* indirection | null, an object past its lifetime, a misaligned address, a pointer outside its provenance |
[] | access outside the array |
+ - pointer arithmetic | a result outside the array (one past the end included) |
- between pointers | pointers into different arrays |
< <= > >= | ordering pointers that do not belong to the same array or object |
. -> on unions | reading a member other than the one last written (the common initial sequence excepted) |
| expressions in general | modifying the same object twice within one sequence point, or modifying it and reading it for another purpose |
Table 50.16 — Where a constant expression is required
50.14.2 Unspecified#
| place | what is not settled |
|---|---|
| subexpressions | the evaluation order of f() + g() |
| function arguments | the order among arguments |
== != | whether a one-past-the-end pointer compares equal to a pointer to the next object |
| padding bytes | the values of a struct’s padding — the reason not to compare with memcmp (chapter 48) |
Table 50.17 — The places left unspecified
50.14.3 Implementation-defined#
| place | what the implementation settles |
|---|---|
>> | the result of shifting a signed negative value (usually an arithmetic shift) |
| integer conversion | the result of converting a value that does not fit a signed type (still so in C23) |
| pointer ↔ integer | the result of the conversion and whether it round-trips (only the round trip through uintptr_t, where it exists, is guaranteed) |
char | signed or unsigned — which splits >> and comparison (chapter 8) |
| bit-fields | the order of allocation and the padding |
Table 50.18 — The places the implementation decides
50.15 Things that are not operators#
The same characters appear in the grammar without being operators.
| shape | what it really is | in detail |
|---|---|---|
the comma in f(a, b) | a separator of the call syntax — no sequence point | chapter 34 |
the comma in int a, b; | a separator of declaration syntax | chapter 24 |
the comma in {1, 2} | a separator in an initialiser list | chapters 39 and 47 |
(type){...} | a compound literal — not a cast but syntax that makes an object | chapter 48 |
the parentheses of sizeof(int) | syntax wrapping a type name — not a call | chapter 36 |
# ## | preprocessor operators — they act in a different phase of translation | chapter 61 |
the dot in {.x = 1} | designated-initialiser syntax, not member access | chapter 47 |
the star in int *p; | declarator syntax, not indirection | chapter 65 |
Table 50.19 — Things that look like expressions
Recap
| What to keep | The point |
|---|---|
| What an expression carries | Value, type, value category, side effects |
| Precedence | A grouping rule, not an order of computation |
| Associativity | Which side groups first among equals |
| Where order is guaranteed | Only &&, ||, ?:, the comma operator, and function calls |
| Grey zones | Read UB, unspecified and implementation-defined as distinct words |
| Working rule | Parenthesise, and never touch the same object twice in one expression |
Table 50.20 — Expressions and operators — what to remember
You can now read an operator as a contract. The next chapter puts the bitwise ones to work (chapter 51) — the idioms, the traps, and the names C23 gave those idioms. After that come the places where the contracts get subtlest — the mathematics of approximation (chapter 52), handling failure (chapter 53), and what happens when a contract is broken (chapter 54).