Why this matters
Changing variables is what imperative programs do, and C gives you a compact toolkit for it: assignment that is itself an expression, compound operators like +=, and the famous ++. You will see count++, total += x and i = j = 0 in almost every C program. The same compactness lets you write lines whose meaning is undefined, and the C language really does refuse to say what i = i++ does. This lesson teaches the idioms and the one rule that keeps you out of trouble.
Assignment is an expression
In C, x = 5 is not just a statement. It is an expression whose value is the value assigned, and storing into x is a side effect of evaluating it. Adding a semicolon makes it an expression statement, which evaluates the expression and throws away the value.
Because it is an expression, it can appear inside other expressions:
int n = 10;
printf("%d\n", n = 20); /* stores 20 into n, prints 20 */
int x, y, z;
x = y = z = 0; /* = groups right to left: x = (y = (z = 0)) */
20
Chained assignment to initialize several variables is a common idiom. Assignment inside conditions is also common in real code (while ((c = getchar()) != EOF), which you will meet in the next module), but it needs care, because of the = versus == trap from the last lesson.
The left side of = must be something that designates a storage location, called an lvalue: a variable, an array element, a struct member, a dereferenced pointer. 5 = x and a + b = 3 are errors (lvalue required as left operand of assignment).
Compound assignment
x op= y means x = x op y, with x evaluated only once:
| compound | means |
|---|---|
x += y | x = x + y |
x -= y | x = x - y |
x *= y | x = x * y |
x /= y | x = x / y |
x %= y | x = x % y |
There are also &=, |=, ^=, <<= and >>= for the bitwise operators of module 13.
int score = 10;
score += 5; /* 15 */
score *= 2; /* 30 */
score -= 3; /* 27 */
score /= 4; /* 6, integer division */
score %= 4; /* 2 */
Note that the right side is computed as a whole first: x *= a + b means x = x * (a + b), not x * a + b.
Increment and decrement
Adding or subtracting one is so common that C has dedicated operators, and they come in two forms:
| expression | effect on i | value of the expression |
|---|---|---|
++i (prefix) | adds 1 | the new value |
i++ (postfix) | adds 1 | the old value |
--i | subtracts 1 | the new value |
i-- | subtracts 1 | the old value |
#include <stdio.h>
int main(void)
{
int i = 5;
int a = i++; /* a gets 5, then i becomes 6 */
int b = ++i; /* i becomes 7, then b gets 7 */
printf("a = %d, b = %d, i = %d\n", a, b, i);
return 0;
}
a = 5, b = 7, i = 7
When the value is not used, as in a statement count++; on its own, the two forms do exactly the same thing and compile to the same instruction. C programmers mostly write i++ out of habit. The difference only matters when the expression's value is used.
Side effects and sequence points
A side effect is a change to the state of the program: storing into a variable (=, +=, ++), writing output, modifying a file. Evaluating x + 1 has no side effect; evaluating x++ does.
C deliberately does not fully specify when side effects happen within an expression. The compiler may reorder evaluation for speed. What C does guarantee is that all side effects are complete at certain sequence points, the most important being:
- the end of a full expression (the semicolon of a statement, the condition of an
if), - after the left operand of
&&,||,?:and the comma operator, - just before a function is called (after its arguments are evaluated).
Between two sequence points, the rule is:
Do not modify a variable more than once, and do not both modify it and read it for any other purpose. Breaking this rule is undefined behavior.
int k = 1;
k = k++ + 1; /* undefined: k modified by ++ and by = */
int m = 1;
printf("%d %d\n", m, m++); /* undefined: m read and modified, unsequenced */
gcc warns about the simple cases:
main.c:6:7: warning: operation on 'k' may be undefined [-Wsequence-point]
6 | k = k++ + 1;
| ~~^~~~~~~~~
main.c:9:27: warning: operation on 'm' may be undefined [-Wsequence-point]
and on this compiler the printf happens to print 2 1, evaluating the arguments right to left. Another compiler, or another optimization level, may print 1 1. The order in which function arguments are evaluated is unspecified in C, and when one of them modifies a variable another one reads, the behavior is undefined.
The fix is always the same: split it into separate statements. x = x + 1; is fine (x is read only to compute its new value). m++; printf("%d\n", m); is fine.
The comma operator
The comma operator (different from the commas separating function arguments) evaluates its left operand, discards the value, then evaluates its right operand, with a sequence point between:
int a = (b = 3, b * 2); /* b becomes 3, a becomes 6 */
Its main legitimate use is in for loops to update two variables at once: for (i = 0, j = n - 1; i < j; i++, j--). Elsewhere it mostly makes code harder to read.
Common mistakes
- Using the postfix value when you meant prefix, or the reverse:
int next = count++;gives the old count. - Modifying a variable twice in one expression (
i = i++,a[i] = i++,f(i++, i)). Split the statement. - Expecting
x =+ 5to add. It parses asx = +5(unary plus) and simply assigns 5. The operator is+=. - Assigning to something that is not an lvalue:
error: lvalue required as left operand of assignment - Assigning to a
constvariable with a compound operator:limit += 1;givesassignment of read-only variable 'limit'.
Recap
- Assignment is an expression whose value is the stored value;
=groups right to left, sox = y = 0works. x op= yis shorthand forx = x op ywith the right side computed first.++iyields the new value,i++the old one; on their own as statements they are identical.- Side effects are only guaranteed complete at sequence points (end of statement, after the left side of
&&,||,?:, comma, before a function call). - Modifying a variable twice, or modifying and reading it, between sequence points is undefined. Split such code into separate statements.
Going further: compile int i = 1; i = i++ + ++i; with -Wall and read the warning, then look up "Order of evaluation" on cppreference.com, which lists precisely what is sequenced and what is not.