C / OPERATORS AND EXPRESSIONS
Assignment, compound assignment and sequencing
Use assignment as an expression that yields the stored value, expand compound assignment correctly, and sequence side effects with the comma operator.
What you will learn
- Predict chained assignment: d = i = 3.75 leaves i at 3 and d at 3.0, not 3.75
- Expand x op= y into x = (T)(x op y) and spot the conversion back to x's type
- Use the comma operator to run two side effects where one expression is allowed
- Parenthesize an assignment used as a value, since = binds looser than comparison
Understanding Assignment, compound assignment and sequencing
In C, = is an operator, not a statement separator, and like every operator it produces a value. That value is not the right-hand expression as you wrote it: it is whatever actually ended up in the left operand, after conversion to the left operand's type. Assignment groups right to left, so a = b = expr stores into b first and then hands a the converted contents of b, which is why a chain that passes through a narrower type quietly changes what the outer assignment receives. Unlike C++, the result is a plain value and not an lvalue, so (a = b) = c does not compile.
E1 op= E2 is defined as E1 = E1 op E2 with one difference that matters: E1 is evaluated exactly once. The arithmetic still happens in the usual promoted type, and the result is then converted back into E1's type, so every compound assignment hides a cast you never wrote. On an int n, n /= 2.0 really means n = (int)(n / 2.0), and on an unsigned char c, c += 100 computes 300 as an int and stores 300 modulo 256.
The comma operator is the sequencing tool: a, b evaluates a, discards its value, passes a sequence point, then evaluates b, and the whole expression has b's value and type. It exists so you can place two side effects where the grammar accepts only one expression, most often in the init and iteration clauses of a for loop. It has the lowest precedence of any operator, so y = 1, 2 parses as (y = 1), 2 and leaves y at 1. The commas between function arguments and between declarators are punctuation rather than this operator, so f(a, b) passes two arguments while f((a, b)) passes one.
Knowing that assignment returns the stored value is what makes idioms like while ((c = next()) != 0) work: one expression both updates c and supplies the test value.
<stdio.h>
int main(void)
{
int i;
double d;
d = i = 3.75; /* right to left: d = (i = 3.75) */
printf("i = %d, d = %.2f\n", i, d);
int n = 7;
n /= 2.0; /* n = (int)(n / 2.0), so 3.5 becomes 3 */
printf("n = %d\n", n);
unsigned char c = 200;
c += 100; /* 300 is computed as int, then stored modulo 256 */
printf("c = %d\n", c);
int x = (puts("comma: left side ran first"), 42);
printf("x = %d\n", x);
int y;
y = 1, 2; /* parses as (y = 1), 2 */
printf("y = %d\n", y);
return 0;
}
An assignment is an expression whose value is the converted value actually stored, and the comma operator is the operator that sequences two evaluations left to right.
Worked examples
Assignment used for its value
A copy loop that both stores a byte and tests the stored byte in one expression.
<stdio.h>
int main(void)
{
const char src[] = "copy";
char dst[8];
int k = 0;
while ((dst[k] = src[k]) != '\0')
k++;
printf("dst = [%s], k = %d\n", dst, k);
return 0;
}
Example explained
Line 1dst[k] = src[k] has a value, the byte just stored, so the copy and the test are the same expression.
Line 2The inner parentheses are mandatory: != binds tighter than =, so dst[k] = src[k] != '\0' would store 1 or 0.
Line 3The loop body runs only for non-zero bytes, so the terminating '\0' is copied and then ends the loop.
Line 4k is never incremented for the terminator, which is why it holds the length 4 afterwards.
Two counters in one for clause
The comma operator packing two assignments and two updates into the single slots a for loop provides.
<stdio.h>
<string.h>
int main(void)
{
char s[] = "sequenced";
size_t i, j;
for (i = 0, j = strlen(s) - 1; i < j; i++, j--) {
char t = s[i];
s[i] = s[j];
s[j] = t;
}
printf("%s\n", s);
return 0;
}
Example explained
Line 1i = 0, j = strlen(s) - 1 is one expression made of two assignments, which is how two variables fit the one init slot.
Line 2i++, j-- works the same way; the comma expression's value is discarded and only the two side effects matter.
Line 3The comma guarantees the left side is fully evaluated, sequence point included, before the right side starts.
Line 4t is needed because s[i] = s[j] destroys s[i] before it could be read back.
op= evaluates the left side once
Counting how often an index expression runs in the compound form versus the spelled-out form.
<stdio.h>
static int calls = 0;
static int idx(void)
{
calls++;
return 1;
}
int main(void)
{
int a[3] = {10, 20, 30};
a[idx()] += 5;
printf("a[1] = %d, calls = %d\n", a[1], calls);
calls = 0;
a[idx()] = a[idx()] + 5;
printf("a[1] = %d, calls = %d\n", a[1], calls);
return 0;
}
Example explained
Line 1a[idx()] += 5 evaluates the left operand exactly once, so idx runs a single time and a[1] becomes 25.
Line 2a[idx()] = a[idx()] + 5 writes the same update out by hand and therefore calls idx twice.
Line 3The order of the two idx calls is unspecified, but both do the same thing, so the printed numbers do not depend on it.
Line 4That single-evaluation rule is the practical reason to prefer op= when the left operand is a call, an index or a dereference.
Important notes
In C the result of an assignment is a value, not an lvalue, so (a = b) = c and &(a = b) are errors; C++ differs here.
The wrap in c += 100 is fully defined only because c is unsigned; converting an out-of-range result into a signed type is implementation-defined.
Common mistakes
Expecting d = i = 3.75 to leave d at 3.75: the chain carries the truncated int 3, so d becomes 3.0 and the missing fraction gets blamed on printf.
Writing y = a, b; and expecting y to receive b: comma binds looser than =, so it means (y = a), b, and b is computed and discarded with a diagnostic only under -Wall.
Typing n =- 1 when meaning n -= 1: it parses as n = -1, so the counter is overwritten instead of decremented and the code compiles cleanly.
Try it yourself
Change, predict, then run
Predict, then verify with a program, the values of i and d after int i; double d; d = i = 9 / 2.0; and the value of y after int y; y = 3, 4;. Then make y end up as 4 by changing only parentheses.
Open the C workspaceCheck your understanding
Given int i; double d; and the statement d = i = 7 / 2.0; what value does d end up holding?
- 3.5
- 3.0
- 4.0
- Nothing, because the statement does not compile: 7 / 2.0 is a double and i is an int
Show answer
7 / 2.0 is 3.5, but the value of i = 3.5 is what landed in i after conversion to int, namely 3, and that int is what d receives, so d is 3.0. 3.5 is tempting because the chain looks like it hands the original right-hand value to both variables; instead each assignment receives the converted result of the one to its right. 4.0 is wrong because double-to-int conversion truncates toward zero rather than rounding.