Skip to content

Lecture 3 - Operators, Binary & Hex, Bit Manipulation

Target budget for a 150-minute session. Strings are a stretch at the end (segment 9); if time is short they move to the top of Lecture 4, just before HW1 (see strings_primer.md). Segments 7-8 (the two coding exercises) are the designated overflow.

0. Recap of Week 1

  • Last week, in brief: the edit -> compile -> run loop with clang; static typing (int, char, float, double); if/else, the three loops, and switch; printf/scanf (remember the & on scanf); and splitting code into .c/.h files built with a Makefile.
  • At the end of Lecture 2 we promised logical operators "in detail next week"
  • that is where we start today. By the end of today we will know how numbers are actually stored as bits, and how to read and manipulate those bits directly.

1. Operators in C

We have already been using operators (+, <, =) without naming them. Let us make the full set explicit. We hold off on the bitwise operators until we have covered binary numbers - they only make sense once you can see the bits.

Arithmetic (quick - mostly review)

  • + - * / %
  • Integer division truncates toward zero: 7 / 2 is 3, not 3.5. To get 3.5 at least one operand must be floating point: 7.0 / 2.
  • % is the remainder (modulo): 7 % 2 is 1. Defined for integers only. Handy idioms: n % 2 == 0 tests even; n % 10 peels off the last decimal digit.

Comparison (relational)

  • == != < > <= >=
  • Every comparison produces an int: 1 for true, 0 for false. This is why if (x) works for any integer - there is no separate boolean type in play.

Logical (the promised topic)

  • && (and), || (or), ! (not). Operands are treated as truthy/falsy: 0 is false, anything non-zero is true.
  • Short-circuit evaluation is the key idea:
  • a && b - if a is false, b is never evaluated (the answer is already false).
  • a || b - if a is true, b is never evaluated.
  • This is not just an optimization - it is a tool. The classic guard:
    if (n != 0 && total / n > threshold) { ... }
    
    If n is 0, the && stops before the division ever happens - no divide-by-zero. Order matters: put the test that protects the second operand first.

Assignment and compound assignment

  • Plain = stores a value. The compound forms are shorthand: x += 5 means x = x + 5; likewise -= *= /= %=.
  • Increment / decrement: i++ and i--. Pre- vs post- matters when used inside a larger expression: a = i++ stores the old i then increments; a = ++i increments first. On its own line they are identical - prefer that for clarity.

Two traps to put on the board

  • = vs ==. if (x = 5) does not compare - it assigns 5 to x and then tests 5, which is always true. The compiler will warn; do not ignore it.
  • Precedence. * and / bind tighter than + and -; && binds tighter than ||. When in doubt, parenthesize - (a && b) || c reads better than trusting the table.

2. Why binary? Electricity, bits, and bytes

  • A computer is built from transistors - tiny switches that are either on or off. Two clean, distinguishable states (high voltage / low voltage) are far more reliable to build and detect than ten. So the machine counts in base 2.
  • A bit is one binary digit: 0 or 1. That is the whole alphabet.
  • Bits are grouped:
  • nibble = 4 bits
  • byte = 8 bits - the standard unit of storage
  • One byte has 2^8 = 256 possible patterns, so it holds the values 0 to 255 (unsigned). An int is typically 4 bytes = 32 bits.
  • Everything is ultimately bits - integers, characters, images, this lecture. Today we focus on how integers are laid out, because that is what bit manipulation acts on.

3. Binary and hexadecimal number systems

Place value, generalized

  • Decimal is base 10: 347 = 3*100 + 4*10 + 7*1 = 3*10^2 + 4*10^1 + 7*10^0.
  • Binary is base 2, same idea with powers of 2:
    bit position:   7   6   5   4   3   2   1   0
    place value:  128  64  32  16   8   4   2   1
    
    1011 (binary) = 8 + 0 + 2 + 1 = 11 (decimal).
  • Binary -> decimal: add up the place values where a 1 sits.
  • Decimal -> binary: repeatedly subtract the largest power of 2 that fits (or repeatedly divide by 2 and read the remainders bottom-up). Do one of each on the board.

Worked examples

  • Binary -> decimal (1101 1010): add the place values where a 1 sits.
    bit:     1   1   0   1   1   0   1   0
    place: 128  64  32  16   8   4   2   1
           128 +64  +0 +16  +8  +0  +2  +0  = 218
    
  • Decimal -> binary, subtract-powers (37): take the largest power of 2 that fits, subtract, repeat.
    37 - 32 = 5    (bit 5 set)
     5 -  4 = 1    (bit 2 set)
     1 -  1 = 0    (bit 0 set)
    37 = 0010 0101 (binary)
    
  • Decimal -> binary, divide-by-2 (13): read remainders bottom-up.
    13 / 2 = 6 r 1
     6 / 2 = 3 r 0
     3 / 2 = 1 r 1
     1 / 2 = 0 r 1
    read up: 1101 = 13
    

Hexadecimal, and why we bother

  • Hex is base 16: digits 0-9 then A-F (A=10 ... F=15).
  • The payoff: one hex digit is exactly four bits (a nibble), because 16 = 2^4. That makes hex a compact, human-readable shorthand for binary.
    binary:  1111 1010
    nibbles:  F    A
    hex:     0xFA   (= 250 decimal)
    
  • Binary <-> hex by grouping: split the bits into groups of 4 (from the right) and translate each group independently. No arithmetic across groups - this is why programmers reach for hex constantly.
  • In C source, hex literals are written with a 0x prefix: 0xFF is 255.

  • Decimal -> hex (250): convert to binary, then group into nibbles.

    250 = 1111 1010 = 0xFA
    

Binary arithmetic (brief)

  • Addition works just like decimal, but you carry at 2 instead of 10:
      0 1 1   (3)
    + 0 0 1   (1)
    -------
      1 0 0   (4)
    
  • Note what happens when a carry runs off the left end of a fixed-width type: that is overflow - the result wraps. We will see this concretely with shifts.

  • Subtractions, multiplications, and divisions also work as expected, but we will not do them by hand. The key point is that the same rules apply to all bases - the base is just a lens for us humans to read the bits.


In-class exercise break - conversions worksheet

Handout: Part A, Exercises A1-A3 - pen and paper, no computer. Work in pairs.

  • Convert a handful of values decimal -> binary -> hex and back.
  • A couple of binary additions, including one that overflows 8 bits so the table conversation has a concrete example.
  • Solutions on the back of the handout; we review a few on the board.

4. Binary logic and truth tables

Before the C operators, the logic itself. Each works bit by bit:

  • AND (&): 1 only if both bits are 1.
  • OR (|): 1 if either bit is 1.
  • XOR (^): 1 if the bits differ.
  • NOT (~): flips every bit.
 A B | A&B  A|B  A^B        A | ~A
 ----+------------- -      ---+----
 0 0 |  0    0    0         0 |  1
 0 1 |  0    1    1         1 |  0
 1 0 |  0    1    1
 1 1 |  1    1    0
  • Do not confuse these with &&/||. Logical &&/|| collapse their whole operands to a single true/false and short-circuit. Bitwise &/| operate on all bits in parallel and always evaluate both sides. 1 & 2 is 0 (no bit in common); 1 && 2 is 1 (both non-zero).

5. Why bother? Where bit-level programming earns its keep

Before the C syntax, the motivation. Manipulating individual bits can feel fiddly, so it is worth seeing why a systems programmer reaches for it constantly. The recurring theme: bits let you say a lot in very little space, very fast, right where the hardware lives.

  • Packing many yes/no answers into one number (flags). Instead of eight separate bool variables, store eight on/off settings in the eight bits of one byte. This is how real C APIs take options: you OR named constants together, e.g. open(path, O_WRONLY | O_CREAT | O_APPEND). Each name is a single set bit; the function pulls them apart with masks.
  • Unix file permissions are bits you already use. chmod 0755 is octal for the permission bits rwxr-xr-x. Read = 4, write = 2, execute = 1 are just bit positions, and 4|2|1 = 7. You have been doing bit math from the shell already.
  • Talking to hardware and the network. Device registers, sensor status lines, and packet headers cram several fields into a few bytes. An IPv4 header, an RGBA color (0xRRGGBBAA), a subnet mask - all are read by shifting and masking out the field you want. Extract the red channel with (color >> 16) & 0xFF.
  • Speed. Bitwise ops are about the cheapest thing a CPU does. x << 1 is a multiply by 2; x >> 3 divides by 8; x & (N-1) is the remainder mod a power of two - all far faster than the general *, /, %. Compilers lean on this, and so do hot loops.
  • Memory efficiency (bitsets). Need to track which of 1,000,000 items are "seen"? One bit each fits in 125 KB instead of a million bytes. A bitset or bitmap is just an array of integers used as a wall of flags.
  • The building blocks of bigger systems. Hashing, checksums, compression, and cryptography are built largely from XOR and shifts. Even if you never write those, you will read code that does.

The takeaway: bit manipulation is not an academic trick - it is the everyday vocabulary of systems, drivers, networking, and graphics, which is exactly what this course is about. Now the C operators that make it happen.


6. Bit manipulation in C

The bitwise operators

  • & AND, | OR, ^ XOR, ~ NOT - exactly the truth tables above, applied to every bit of the operands at once.

Shifts

  • Left shift x << n: move bits left by n, filling with 0 on the right. Each shift multiplies by 2: 1 << 0 = 1, 1 << 1 = 2, 1 << 3 = 8.
  • Right shift x >> n: move bits right by n. For unsigned values this divides by 2 (flooring): 40 >> 1 = 20.
  • Caution: right-shifting negative signed values is implementation-defined in spirit; keep bit-twiddling to unsigned types. Prefer unsigned int when you mean "a bag of bits."
  • 1 << n gives you a value with only bit n set - the building block for everything that follows.

7. Printing hex and binary in C

  • Hex is built in: %x (lowercase), %X (uppercase), and %#x to include the 0x prefix. %o prints octal. There is also %d for decimal as always.
    unsigned int v = 250;
    printf("%d = 0x%X\n", v, v);   /* 250 = 0xFA */
    
  • There is no portable %b for binary. You print binary by hand, walking bits from the most significant down and masking off one at a time:
    void print_binary(unsigned int v) {
        for (int i = 31; i >= 0; i--) {
            char bit;
            if ((v >> i) & 1) {
                bit = '1';
            } else {
                bit = '0';  
            }
            printf("%c", bit);
        }
        printf("\n");
    }
    
  • (v >> i) & 1 slides bit i down to the bottom and keeps only that bit. This shift-then-mask is the idiom for inspecting a single bit - it returns next section.

An aside: The nested if in the code above is a bit verbose. You can also write it as a single line using the nested conditional operator/ternary operator in C:

char bit = ((v >> i) & 1) ? '1' : '0';


In-class exercise break - print binary and hex

Handout: Exercise B1 - on the computer. Solution in the separate key.

  • Read an integer from the user. Print it in decimal, hex, and binary.
  • Reuse print_binary from above for the binary line; %X handles the hex.
  • Stretch: only print the bits from the highest set bit down, so small numbers do not show 32 leading zeros.

8. Bit masks - set, clear, toggle, test

A mask is a value crafted to touch exactly the bits you care about. With 1 << n as the building block, the four standard moves (put these on the board):

unsigned int flags = 0;

flags |=  (1 << n);    /* SET   bit n  -> OR  with a 1 in that spot   */
flags &= ~(1 << n);    /* CLEAR bit n  -> AND with a 0 in that spot   */
flags ^=  (1 << n);    /* TOGGLE bit n -> XOR flips it                */
int on = (flags >> n) & 1;   /* TEST bit n -> 1 if set, else 0        */
  • Why each works, in one line each:
  • OR with 1 forces a bit on; OR with 0 leaves a bit unchanged -> set.
  • AND with 0 forces a bit off; AND with 1 leaves it unchanged -> the ~(1 << n) mask is all 1s except a 0 at n -> clear.
  • XOR with 1 flips; XOR with 0 leaves unchanged -> toggle.
  • This is how a single int can pack many on/off flags - exactly how real APIs pass options (file permissions, event masks, and so on).

In-class exercise break - bit masks

Handout: Exercise B2 - on the computer. Solution in the separate key.

  • Start from flags = 0. Set bits 1 and 3, print (binary), clear bit 1, toggle bit 0, print again at each step and check against what you predict.
  • Write a small helper int is_set(unsigned int x, int n) using the shift-and-mask idiom, and use it to report which bits are on.

9. Wrap-up

  • Operators: arithmetic/comparison/logical/assignment; logical operators short-circuit, and = is not ==.
  • Numbers are bits: base 2 because transistors are on/off; hex is base 16 and maps 1 digit <-> 4 bits, which is why we use it.
  • Bitwise (& | ^ ~) act on all bits at once - different from && ||. Shifts multiply/divide by powers of 2 and build masks via 1 << n.
  • The four mask moves - |= set, &= ~ clear, ^= toggle, >> & 1 test - are worth memorizing.

10. Stretch (only if time allows, ~20 min) - a first look at strings

  • If the room is with us and time remains, begin the C strings segment to get a head start on HW1. Otherwise this opens Lecture 4.
  • Full material is in strings_primer.md: a string is a char array ending in '\0', you loop until the terminator, and chars are just small integers (ASCII) - which connects straight back to today's "everything is numbers" theme.