4 Expressions

C and C++ share most of their operators, and if you have been writing C++ you will find the syntax immediately familiar. But there are important differences. C has no operator overloading — + always means arithmetic addition, never something a class author decided it should mean. The << and >> operators are strictly bitwise shifts, not I/O operations. And C uses int for boolean results — you get bool from <stdbool.h>.

This chapter walks through the operators you will use every day in C, highlights a few traps, and ends with the precedence table you will want to bookmark.

4.1 Assignment

The = operator assigns a value to a variable. In C, assignment is an expression — it produces a value, which is the value being assigned. This lets you chain assignments:

int a, b, c;
a = b = c = 0;   // all three are now 0

The chain works right to left: c gets 0, then b gets the value of that assignment (also 0), then a gets the same.

Because assignment is an expression, you can (and sometimes will) use it inside other expressions. A common pattern is assigning and testing a return value in one step:

int ch;
while ((ch = getchar()) != EOF) {
    putchar(ch);
}

Trap: Because = is assignment and == is comparison, a common mistake is writing if (x = 5) when you mean if (x == 5). The first assigns 5 to x and then evaluates as true (since 5 is nonzero). Modern compilers warn about this with -Wall, but it is still one of the most famous bugs in C:

int x = 0;
if (x = 5) {
    printf("This always runs!\n"); // x is 5, which is true
}

Some programmers write the constant on the left — if (5 == x) — so that if (5 = x) would be a compiler error. This is called a Yoda condition.

4.2 Arithmetic Operators

The arithmetic operators work on numeric types just like in C++:

Operator Operation Example Result
+ addition 3 + 4 7
- subtraction 10 - 3 7
* multiplication 6 * 7 42
/ division 17 / 5 3
% remainder 17 % 5 2

Integer division truncates toward zero. This means 17 / 5 gives 3, not 3.4. Toward zero matters for negatives: -17 / 5 is -3.4 truncated to -3, not floored to -4. If you want a floating-point result, at least one operand must be a floating-point type:

printf("%d\n", 17 / 5);       // 3
printf("%d\n", -17 / 5);      // -3
printf("%f\n", 17.0 / 5);     // 3.400000

The % operator gives the remainder after integer division. The result of % has the same sign as the dividend (the left operand):

printf("%d\n",  17 %  5);   //  2
printf("%d\n", -17 %  5);   // -2
printf("%d\n",  17 % -5);   //  2
printf("%d\n", -17 % -5);   // -2

Wut: The % operator is often called “modulo,” but it is technically the remainder operator. For positive numbers, remainder and modulo are the same. For negative numbers, they differ. In mathematics, modulo always returns a non-negative result. In C, % preserves the sign of the dividend. If you need a true modulo that always returns a non-negative value, you need to adjust the result yourself:

/* assumes m > 0 */
int mod(int a, int m) {
    int r = a % m;
    return r < 0 ? r + m : r;
}

4.3 Comparison and Logical Operators

Comparison operators produce 1 for true and 0 for false. The result type is int, not bool:

Operator Meaning
== equal to
!= not equal to
< less than
> greater than
<= less than or equal to
>= greater than or equal to

Logical operators combine boolean expressions:

Operator Meaning
&& logical AND
|| logical OR
! logical NOT

The logical operators work just like AND, OR, and NOT in English. Consider a little red Corvette. “Is it red and not a Ford?” is true. “Is it red or a Ford?” is true. “Is it red and a Corvette?” is true. “Is it white and a Corvette?” is not.

Any nonzero operand counts as true, and the result is always exactly 1 or 0:

a b a && b a || b !a
0 0 0 0 1
0 nonzero 0 1 1
nonzero 0 0 1 0
nonzero nonzero 1 1 0

Note that && and || normalize their result: 5 && 3 is 1, not 5.

Both && and || use short-circuit evaluation, just like C++. With &&, if the left side is false, the right side is never evaluated. With ||, if the left side is true, the right side is skipped:

int *p = NULL;
if (p != NULL && *p > 0) {
    // safe — *p is only evaluated if p is not NULL
}

When logical operators mix in one expression, ! is evaluated first, then &&, then ||. That means a || b && c is a || (b && c), not (a || b) && c.

Tip: Use parentheses when mixing && and || in one expression. Not everyone remembers the precedence rules, and a || (b && c) is clearer than a || b && c even though they mean the same thing.

4.3.1 No Built-in bool

In C++, bool is a built-in type. In C, you use int where 0 is false and anything nonzero is true. The _Bool keyword and the <stdbool.h> convenience header give you bool, true, and false:

#include <stdbool.h>

bool is_valid = true;
if (is_valid) {
    printf("All aboard the Crazy Train!\n");
}

Without <stdbool.h>, you will see code like this:

int done = 0;    // 0 means false
while (!done) {
    // ... do work ...
    done = 1;    // nonzero means true
}

Tip: In C, any nonzero value is true. The number 42, the character 'A', and the pointer 0x7fff are all true. Only 0 (and NULL for pointers) is false. This is why you can write if (ptr) instead of if (ptr != NULL) — they mean the same thing.

Because truth in C is just an int, C programmers long ago invented the !!x idiom to normalize a value to exactly 0 or 1: the inner ! turns any nonzero value into 0 (and 0 into 1), and the outer ! flips it back.

int flags = 0x40;
int has_flag = !!(flags & 0x40);   // exactly 1, not 0x40

You will meet !! constantly in real C: flag tests like the one above, macros that must yield a clean 0 or 1, and assignments to one-bit bitfields, where storing 0x40 directly would silently truncate to 0. The idiom migrated into C++ as well, but C is where it was born and where it earns its keep.

4.4 Bitwise Operators

Bitwise operators work on the individual bits of integer values. &, |, and ~ are the bit-level cousins of the logical &&, ||, and !: they apply AND, OR, and NOT to each bit of a number instead of to whether the whole number is zero or nonzero. Just for fun, the bitwise operators throw in ^, exclusive OR (XOR). In C++, << and >> are commonly used for stream I/O. In C, they are exclusively bit shift operators.

Operator Operation Example Result
& bitwise AND 0xF0 & 0x3C 0x30
| bitwise OR 0xF0 | 0x0F 0xFF
^ bitwise XOR 0xFF ^ 0x0F 0xF0
~ bitwise NOT ~0x00 0xFF...FF
<< left shift 1 << 3 8
>> right shift 16 >> 2 4

To see how each operator works, take two small unsigned char values and look at their bits:

unsigned char x = 6;      // 00000110
unsigned char y = 'C';    // 01000011 (ASCII 67)

You will find that when talking about bits, the term set bit indicates a bit that is 1 and clear bit or unset bit indicates a bit is 0. Bit positions are usually numbered from the right. The least significant bit — the one that represents the number 1 in x — is bit 0. The most significant bit — the one that represents the number 128 in x — is bit 7 since a character is only 8 bits. So, x has two bits set — bits 1 and 2 — and y has bits 0, 1, and 6 set.

4.4.1 Bitwise AND (&)

Each result bit is set in bit positions where both x AND y operands have the bit set:

 00000110
&01000011
---------
 00000010

x & y is 2 — bit 1 is the only position where both x and y have a set bit so the rest of the resulting bits are cleared.

4.4.2 Bitwise OR (|)

Each result bit is set in bit positions where either x OR y has the bit set:

 00000110
|01000011
---------
 01000111

x | y is 71 — bits 0, 1, 2, and 6 are set, which happens to be ASCII 'G'.

4.4.3 Bitwise XOR (^)

Each result bit is set in bit positions where exactly one of x and y has the bit set — one or the other, but not both:

 00000110
^01000011
---------
 01000101

x ^ y is 69 — ASCII 'E'. Bit 1 is set in both x and y, so it is cleared in the result.

4.4.4 Bitwise NOT (~)

~ is unary: it takes a single operand and flips every bit — set bits are cleared, and clear bits are set:

~00000110
---------
 11111001

As 8 bits that reads as 249, but print ~x with %d and you get -7 — C promotes x to int before flipping, so the 24 high bits flip too and the result is negative.

4.4.5 Left Shift (<<)

<< slides every bit toward the high end, filling in with clear bits at the low end; each position shifted doubles the value:

00000110 << 2
-------------
00011000

x << 2 is 24 — four times 6 — the set bits move from positions 1 and 2 up to positions 3 and 4.

4.4.6 Right Shift (>>)

>> slides every bit toward the low end; bits that fall off the right side are discarded:

01000011 >> 4
-------------
00000100

y >> 4 is 4 — only bit 6 survives, landing at bit 2; the set bits at positions 0 and 1 fall off the right side.

4.4.7 Flag Manipulation and Masking

One of the most common uses of bitwise operators in C is manipulating flags — individual bits within an integer that each represent an on/off setting. The process of extracting specific flags from a number is called masking. For example, to track whether a person is hot we can use bit 0, but we also need to know if they like it hot, so we use bit 1 for that. Maybe we also need to know if they are able to continue, so we use bit 2 for that.

The following defines a number for each flag with the corresponding bit set.

const int FLAG_HOT = 1 << 0;
const int FLAG_LIKE_HOT = 1 << 1;
const int FLAG_CAN_GO_ON = 1 << 2;

int people_state[3]; // there are 3 people

people_state[0] = FLAG_HOT; will set the state of the first person; they don’t like hot so they can’t go on. The second person is hot and they like it so they can go on — people_state[1] = FLAG_HOT | FLAG_LIKE_HOT | FLAG_CAN_GO_ON;. Note that the | can be counterintuitive because we said AND. We use OR because we are setting separate bits. The third person is not hot and they don’t like it hot but they still can’t go on, so people_state[2] = 0; since none of the conditions are true.

Now the first person is ready to go on; they still don’t like it hot, but they are willing to try. Just like earlier, we can use OR to set bits. people_state[0] = people_state[0] | FLAG_CAN_GO_ON; will take the current state of the first person and set their can-go-on flag. Note, if the can-go-on flag had already been set, that line of code wouldn’t have changed anything.

Now the second person can’t go on any more even though they like the heat. We need to clear the can-go-on-flag. Did people_state[1] = people_state[1] & ~FLAG_CAN_GO_ON; spring to your mind? We use AND to clear a bit, but we clear a bit by ANDing the bit with 0. So to clear the FLAG_CAN_GO_ON bit in people_state[1] we need to AND it with a number that has all the bits set except the FLAG_CAN_GO_ON bit; that number is ~FLAG_CAN_GO_ON. This may be more clear in binary:

~0000 0000 0000 0000 0000 0000 0000 0100 <- FLAG_CAN_GO_ON
----------------------------------------
 1111 1111 1111 1111 1111 1111 1111 1011 <- flips the bits
                                     ^
                                     +----- this is the bit to clear
                                            in people_state[1]

But wait, what if we also want to track the temperature they feel in Fahrenheit? (Perhaps the temperature is different in different areas.) That can be encoded in the state as well. We need seven bits to encode temperatures up to 127 degrees. We have plenty of unused bits, so let’s put the temperature in bits 3 to 9.

Now the masking is more interesting! We need to extract 7 bits rather than just one for a flag. To get 7 set bits, we can start with the number 1 << 7, which is 10000000 in binary or 128 in decimal. If we subtract 1 from that number, we have to do a bunch of borrows and end up with 1111111. Remember this trick, you will use it a lot! So our mask bits are (1 << 7) - 1. Now we have to shift them over 3 to get them to the right place.

const int MASK_TEMPERATURE = ((1 << 7) - 1) << 3;

Here is a short program to set up three people and print who is hot and what their temperatures are:

#include <stdio.h>

const int FLAG_HOT = 1 << 0;
const int FLAG_LIKE_HOT = 1 << 1;
const int FLAG_CAN_GO_ON = 1 << 2;
const int MASK_TEMPERATURE = ((1 << 7) - 1) << 3;

int main(void) {
    int people_state[3];

    people_state[0] = FLAG_HOT | FLAG_CAN_GO_ON | (101 << 3);
    people_state[1] = FLAG_HOT | FLAG_LIKE_HOT | (104 << 3);
    people_state[2] = 75 << 3;

    for (int i = 0; i < 3; i++) {
        int temp = (people_state[i] & MASK_TEMPERATURE) >> 3;

        if (people_state[i] & FLAG_HOT) {
            printf("person %d is hot at %d degrees\n", i, temp);
        } else {
            printf("person %d is fine at %d degrees\n", i, temp);
        }
    }
    return 0;
}

The pattern is straightforward:

  • Set a bit: flags |= BIT;
  • Clear a bit: flags &= ~BIT;
  • Toggle a bit: flags ^= BIT;
  • Check a bit: if (flags & BIT)

Tip: Shifting 1 to create bit masks — (1 << n) — is a common idiom in C for hardware registers, permission flags, and option bitmasks. It is clearer than writing raw hex values because you can see exactly which bit position you are targeting.

4.5 Compound Assignment Operators

Compound assignment operators combine an arithmetic or bitwise operation with assignment. They work exactly as in C++:

Operator Equivalent to
a += b a = a + b
a -= b a = a - b
a *= b a = a * b
a /= b a = a / b
a %= b a = a % b
a &= b a = a & b
a |= b a = a | b
a ^= b a = a ^ b
a <<= b a = a << b
a >>= b a = a >> b

These are not just shortcuts — they express intent more clearly. When you write count += 1, the reader knows you are incrementing count. When you write count = count + 1, the reader has to verify that the same variable appears on both sides.

4.6 Increment and Decrement

The ++ and -- operators increment or decrement a variable by one. They come in prefix and postfix forms:

int x = 5;
int a = ++x;   // prefix: x becomes 6, then a gets 6
int b = x++;   // postfix: b gets 6 (current value), then x becomes 7

In a standalone statement, x++ and ++x do the same thing — increment x. The difference only matters when the result is used in a larger expression.

Trap: Do not modify a variable more than once in the same expression. The result is undefined behavior:

int i = 3;
int result = i++ + ++i;   // UNDEFINED BEHAVIOR — do not!

The compiler is free to evaluate the sub-expressions in any order, and different compilers (or even the same compiler with different optimization levels) may produce different results. If you need multiple modifications, use separate statements.

4.7 The Ternary Operator

The ternary operator ? : is a compact alternative to if/else for simple value selection:

int volume = 11;
const char *verdict = (volume > 10) ? "Loco" : "Tranquilo";
printf("Volume %d: %s\n", volume, verdict);   // Volume 11: Loco

The syntax is condition ? value_if_true : value_if_false. The ternary operator is an expression, so it produces a value that can be used in assignments, function arguments, or anywhere a value is expected:

printf("Track %d is %s\n", track,
       (track % 2 == 0) ? "even" : "odd");

Tip: The ternary operator is great for simple one-line decisions. If your condition or either branch is complex, use a regular if/else instead. Readability matters more than cleverness.

4.8 Operator Precedence

When multiple operators appear in an expression, C evaluates them according to a precedence table. Here are the most important levels, from highest (binding tightest) to lowest:

Precedence Operators Description
1 () [] -> . grouping, subscript, member access
2 ! ~ ++ -- + - * & (type) sizeof unary operators
3 * / % multiplication, division, remainder
4 + - addition, subtraction
5 << >> bitwise shifts
6 < <= > >= relational
7 == != equality
8 & bitwise AND
9 ^ bitwise XOR
10 | bitwise OR
11 && logical AND
12 || logical OR
13 ? : ternary
14 = += -= etc. assignment
15 , comma

4.8.1 Common Precedence Traps

The most dangerous precedence surprise is that the bitwise &, ^, and | operators bind more loosely than comparison operators:

// WRONG — this checks (x) & (0x04 == 0x04), which is (x) & (1)
if (x & 0x04 == 0x04) { ... }

// RIGHT — parentheses fix the precedence
if ((x & 0x04) == 0x04) { ... }

Similarly, || is evaluated after && (AND before OR, matching mathematical convention) — as you saw in the logical operators section, parenthesize when you mix them.

Tip: When in doubt, use parentheses. No one will fault you for writing (a & b) == c instead of relying on precedence rules. The few extra characters make your intent unmistakable and save the next reader (who might be you) from having to look up the precedence table.

4.9 Try It: Expressions Starter

#include <stdio.h>

int main(void) {
    // Assignment chaining
    int a, b, c;
    a = b = c = 1980;
    printf("a=%d b=%d c=%d\n", a, b, c);

    // Integer division and remainder
    printf("17 / 5 = %d\n", 17 / 5);
    printf("17 %% 5 = %d\n", 17 % 5);
    printf("-17 %% 5 = %d\n", -17 % 5);

    // Boolean values are just ints
    printf("(10 > 5) = %d\n", 10 > 5);
    printf("(10 < 5) = %d\n", 10 < 5);

    // Bitwise flag manipulation
    unsigned int flags = 0;
    flags |= (1 << 0);                                   // set bit 0
    flags |= (1 << 2);                                   // set bit 2
    printf("flags = 0x%02X\n", flags);                   // 0x05
    printf("bit 1 set? %d\n", (flags & (1 << 1)) != 0);  // 0
    printf("bit 2 set? %d\n", (flags & (1 << 2)) != 0);  // 1

    // Ternary operator
    int vol = 11;
    printf("Volume: %s\n", (vol > 10) ? "Muy alto" : "Normal");

    // Compound assignment
    int total = 100;
    total += 50;
    total -= 20;
    total *= 2;
    printf("total = %d\n", total);   // 260

    return 0;
}

4.10 Key Points

  • Assignment is an expression in C — it produces a value, enabling chaining (a = b = c = 0) and assignment within conditions.
  • Integer division truncates toward zero. The % operator gives the remainder, which preserves the sign of the dividend.
  • C uses int for boolean results: 0 is false, nonzero is true. Include <stdbool.h> for bool, true, and false.
  • Bitwise << and >> are shifts only — they are not overloaded for I/O as in C++.
  • Use | to set bits, & to check bits, ^ to toggle bits, and & ~ to clear bits.
  • Bitwise &, ^, and | have lower precedence than comparison operators. Always use parentheses when mixing them.
  • Never modify a variable more than once in the same expression — the result is undefined behavior.

4.11 Exercises

  1. Think about it: In C++, you can overload operators to give +, <<, ==, and others custom meanings for your classes. C does not allow operator overloading. What advantage does this give you when reading unfamiliar C code? Can you think of a situation where operator overloading would have been genuinely useful in C?

  2. What does this print?

    int x = 10;
    int y = x++ + ++x;
    printf("%d %d\n", x, y);

    (Be careful — is the answer even defined?)

  3. Calculation: What is the result of each of these expressions?

    25 / 4
    25 % 4
    -25 % 4
    (1 << 4) | (1 << 1)
    0xFF & 0x0F
  4. Where is the bug?

    int status = 0x07;
    if (status & 0x04 == 0x04) {
        printf("Bit 2 is set\n");
    }
  5. What does this print?

    int a = 5, b = 10;
    a ^= b;
    b ^= a;
    a ^= b;
    printf("a=%d b=%d\n", a, b);
  6. Where is the bug?

    int count = 0;
    if (count = 0) {
        printf("El contador es cero\n");
    } else {
        printf("El contador no es cero\n");
    }
  7. Write a program that takes an unsigned int and prints its value in binary (most significant bit first). Use bitwise operators to test each bit. Test it with the values 0, 1, 255, and 1024.