In this lesson we look at the operators C gives you, at the operator precedence table, and above all at the bitwise operations — low-level bit manipulation — along with the surprisingly powerful things they are used for in practice.

1. Arithmetic and comparison operators

C provides the full set of basic operators for computing values and comparing them.

A. Arithmetic operators

Operator Meaning Example Result
+ Addition 5 + 3 8
- Subtraction 5 - 3 2
* Multiplication 5 * 3 15
/ Division 7 / 2 3 (integer division)
% Remainder (modulo) 7 % 2 1
++ Increment by 1 a++ or ++a a = a + 1
-- Decrement by 1 a-- or --a a = a - 1

B. Comparison operators

Comparison operators return 1 (true) or 0 (false):

Operator Meaning Example (a=5, b=3) Result
== Equal to a == b 0
!= Not equal to a != b 1
> Greater than a > b 1
< Less than a < b 0
>= Greater than or equal a >= 5 1
<= Less than or equal a <= 3 0

C. Logical operators

Operator Meaning Example Result
&& Logical AND (5 > 3) && (2 < 4) 1 (both true)
|| Logical OR (5 > 3) || (2 > 4) 1 (at least one true)
! Logical NOT !(5 > 3) 0 (negating true gives false)
operators_demo.c
#include <stdio.h>

int main() {
    int a = 10, b = 3;

    // Arithmetic operators
    printf("a + b = %d\n", a + b);   // 13
    printf("a - b = %d\n", a - b);   // 7
    printf("a * b = %d\n", a * b);   // 30
    printf("a / b = %d\n", a / b);   // 3 (integer division, fraction discarded)
    printf("a %% b = %d\n", a % b);  // 1 (the remainder)

    // Note the difference: ++a vs a++
    int x = 5;
    printf("++x = %d\n", ++x);  // 6 (increment FIRST, then use the value)
    printf("x++ = %d\n", x++);  // 6 (use the value FIRST, then increment)
    printf("x = %d\n", x);      // 7 (the post-increment above has now taken effect)

    // Comparison and logical operators
    int age = 20;
    int hasID = 1; // 1 = true
    if (age >= 18 && hasID) {
        printf("Du dieu kien vao cua\n");
    }

    return 0;
}

The three lines worth remembering are in the ++ block: ++x prints 6, x++ also prints 6, but immediately afterwards x is 7. The difference is not whether the increment happens — both increment — but which value gets used on that very line.

2. The operator precedence table

Precedence decides which operator runs first inside a complex expression. Operators higher in the table bind more tightly. Full reference at cppreference.com — Operator Precedence.

Precedence Operators Description Associativity
1 (highest) () [] -> . Function call, array subscript, member access Left → Right
2 ++ -- + - ! ~ * & (type) sizeof Unary operators, casting, sizeof Right → Left
3 * / % Multiply, divide, remainder Left → Right
4 + - Add, subtract Left → Right
5 << >> Bit shift left, bit shift right Left → Right
6 < <= > >= Relational comparison Left → Right
7 == != Equality comparison Left → Right
8 & Bitwise AND Left → Right
9 ^ Bitwise XOR Left → Right
10 | Bitwise OR Left → Right
11 && Logical AND Left → Right
12 || Logical OR Left → Right
13 ?: Conditional (ternary) operator Right → Left
14 = += -= *= /= %= <<= >>= &= ^= |= Assignment and compound assignment Right → Left
15 (lowest) , Comma operator Left → Right

Common bugs caused by getting precedence wrong:

precedence_traps.c
#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30};
    int *p = arr;

    // Trap 1: *p++ vs (*p)++
    // *p++ is *(p++): read the value at p, then advance the POINTER p by 1
    // (*p)++ reads the value at p, then increments that VALUE by 1
    printf("*p = %d\n", *p);      // 10
    printf("*p++ = %d\n", *p++);  // 10, after which p points at arr[1]
    printf("*p = %d\n", *p);      // 20 (p has moved on)

    // Trap 2: & binds LESS tightly than ==
    int x = 5, y = 3;
    // Wrong: if (x & 1 == 0) really means if (x & (1 == 0)) -> if (x & 0) -> 0
    // Right: if ((x & 1) == 0)
    if ((x & 1) == 0) {
        printf("%d la so chan\n", x);
    } else {
        printf("%d is odd\n", x);      // Correct result
    }

    // Trap 3: assignment = vs comparison ==
    int n = 0;
    // if (n = 5) is always true: it assigns 5, then tests 5 != 0 -> true
    // What you meant: if (n == 5)
    if (n == 5) {
        printf("n bang 5\n");
    } else {
        printf("n is not 5\n");        // Correct result
    }

    return 0;
}

All three traps above compile cleanly, without a single warning at default settings — which is exactly what makes them dangerous. Trap 2 is the most common: x & 1 == 0 looks like "take the last bit and compare it to 0", but == runs first, so it becomes x & 0, which is always 0. This is precisely why the -Wall flag from Lesson 1 is worth turning on.

3. Bitwise operations and endianness

Computers store all data as sequences of binary bits (nothing but 0s and 1s). Bitwise operations let you reach in and work directly on the individual bits of an integer. It is an extremely powerful tool for squeezing performance out of the hardware.

A. Endianness (byte ordering)

When a multi-byte value (say a 4-byte integer) is written to RAM, in what order do its bytes go? C systems split into two conventions:

  • Little Endian: the least significant byte (LSB) is stored at the lowest memory address. Nearly all modern Intel/AMD CPUs and ARM designs are Little Endian.
  • Big Endian: the most significant byte (MSB) goes at the lowest address — the way we normally write numbers, left to right. Common in networking equipment and older processors.

We can write a short C program to find out which convention the current machine uses, by casting a pointer and reading the raw bytes:

check_endianness.c
#include <stdio.h>
int main() {
    unsigned int x = 1; // In binary: 0x00000001
    // Cast the pointer to char* to read the first byte at the lowest address
    char *c = (char*)&x;

    if (*c == 1) {
        printf("This system is Little Endian\n");
    } else {
        printf("This system is Big Endian\n");
    }
    return 0;
}

The trick here is the pointer cast: x is a 4-byte integer holding the value 1, and we read its first byte. If that byte is 1, the smallest byte was stored first — Little Endian. Ordinary x86 and ARM machines all print Little Endian; this only becomes a real problem when you send binary data across a network or read a file written by a different machine.

B. The common bitwise operators

Operator Meaning Rule
& Bitwise AND Gives 1 when both bits are 1, otherwise 0.
| Bitwise OR Gives 1 when at least one of the two bits is 1.
^ Bitwise XOR Gives 1 when the two bits differ, 0 when they are the same.
~ NOT (complement) Flips every bit: 0 becomes 1, 1 becomes 0.
<< Shift left Shifts the bits left by n places (equivalent to multiplying by 2^n).
>> Shift right Shifts the bits right by n places (equivalent to integer division by 2^n).

C. Fast bit tricks

  • Very fast odd/even test: use (n & 1). This inspects the last bit of the integer. If it is 0 the number is even, if it is 1 it is odd — considerably faster than n % 2, because it never engages the ALU's division circuitry.
  • Bit packing (many options in one value): use a bitmask to store many on/off (boolean) options inside a single variable and save RAM.
    #define READ_PERMISSION (1 << 0) // 0001
    #define WRITE_PERMISSION (1 << 1) // 0010
    #define EXEC_PERMISSION (1 << 2) // 0100

    int user_flag = READ_PERMISSION | WRITE_PERMISSION; // 0011 (read and write allowed)
  • Clearing the lowest set bit: the formula n & (n - 1). This is the famous Brian Kernighan algorithm, used to count how many 1 bits an integer contains, or to test whether a number is a power of two (it is, if the result comes out 0).

A worked example

bitwise.c
#include <stdio.h>

int main() {
    int a = 5; // In binary: 0101
    int b = 9; // In binary: 1001

    printf("a & b = %d\n", a & b);   // Ket qua: 1
    printf("a | b = %d\n", a | b);   // Ket qua: 13
    printf("a ^ b = %d\n", a ^ b);   // Ket qua: 12
    printf("~a = %d\n", ~a);         // Ket qua: -6 (theo dang bu hai)
    printf("a << 1 = %d\n", a << 1); // Result: 10 (same as 5 * 2)
    printf("b >> 1 = %d\n", b >> 1); // Result: 4 (same as 9 / 2)

    // Meo kiem tra so chan/le sieu nhanh bang Bitwise
    int n = 7;
    if ((n & 1) == 0) {
        printf("%d la so chan\n", n);
    } else {
        printf("%d la so le\n", n); // 7 & 1 = 0111 & 0001 = 0001 (khac 0) -> So le
    }

    return 0;
}

The two shift lines show why bitwise operations are fast: a << 1 gives 10, exactly 5 * 2, and b >> 1 gives 4, exactly 9 / 2 rounded down. Shifting left by one bit doubles, shifting right by one bit halves — and the CPU does this in a single cycle, more cheaply than a real multiply or divide.

4. Bitwise operations in the real world

Bitwise work is not an academic curiosity: it is used widely in practice, from graphics and computer networking to embedded programming.

A. RGB colour packing — three colour bytes in one integer

In computer graphics each coloured pixel is described by three components: Red, Green and Blue (one byte each, 0–255). Instead of keeping three separate variables, we can pack all three into a single 32-bit integer using bit shifts:

rgb_packing.c
#include <stdio.h>
#include <stdint.h>

// Pack the R, G, B bytes into one 32-bit integer: 0x00RRGGBB
uint32_t rgb_pack(uint8_t r, uint8_t g, uint8_t b) {
    return ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
}

// Unpack each colour channel back out of the packed integer
void rgb_unpack(uint32_t color, uint8_t *r, uint8_t *g, uint8_t *b) {
    *r = (color >> 16) & 0xFF;  // Shift right 16 bits, keep the low 8 bits
    *g = (color >> 8) & 0xFF;   // Shift right 8 bits, keep the low 8 bits
    *b = color & 0xFF;           // Keep the lowest 8 bits
}

int main() {
    // Orange: R=255, G=165, B=0
    uint32_t orange = rgb_pack(255, 165, 0);
    printf("Mau cam packed: 0x%06X\n", orange);  // 0xFFA500

    // Unpack it again
    uint8_t r, g, b;
    rgb_unpack(orange, &r, &g, &b);
    printf("R=%d, G=%d, B=%d\n", r, g, b);  // R=255, G=165, B=0

    // Blend two colours (average each channel)
    uint32_t white = rgb_pack(255, 255, 255);
    uint8_t r2, g2, b2;
    rgb_unpack(white, &r2, &g2, &b2);

    uint32_t blended = rgb_pack((r + r2) / 2, (g + g2) / 2, (b + b2) / 2);
    printf("Blended: 0x%06X\n", blended);   // A lighter orange

    return 0;
}

The key is the symmetry of the two operations: packing is shift left, then OR, and unpacking is shift right, then AND with 0xFF. The & 0xFF means "keep only the low 8 bits and discard everything else" — leave it out and the red channel drags the green channel along with it.

B. IP address manipulation with bitwise operations

An IPv4 address (for example 192.168.1.100) is really a 32-bit integer split into four octets of 8 bits each. We can use bitwise operations to pack it, unpack it and test subnets:

ip_bitwise.c
#include <stdio.h>
#include <stdint.h>

// Pack 4 octets into a 32-bit IP address
uint32_t ip_pack(uint8_t a, uint8_t b, uint8_t c, uint8_t d) {
    return ((uint32_t)a << 24) | ((uint32_t)b << 16)
         | ((uint32_t)c << 8)  | d;
}

// Print the IP address in dotted-decimal form
void ip_print(uint32_t ip) {
    printf("%d.%d.%d.%d",
        (ip >> 24) & 0xFF,
        (ip >> 16) & 0xFF,
        (ip >> 8) & 0xFF,
        ip & 0xFF);
}

int main() {
    uint32_t ip   = ip_pack(192, 168, 1, 100);
    uint32_t mask = ip_pack(255, 255, 255, 0);  // /24 subnet mask

    // Compute the network address with AND
    uint32_t network = ip & mask;
    // Compute the broadcast address with OR against the inverted mask
    uint32_t broadcast = ip | ~mask;

    printf("IP:        "); ip_print(ip);        printf("\n");
    printf("Mask:      "); ip_print(mask);      printf("\n");
    printf("Network:   "); ip_print(network);   printf("\n");  // 192.168.1.0
    printf("Broadcast: "); ip_print(broadcast); printf("\n");  // 192.168.1.255

    // Check whether two IPs are on the same subnet
    uint32_t ip2 = ip_pack(192, 168, 1, 200);
    if ((ip & mask) == (ip2 & mask)) {
        printf("Hai IP cung subnet!\n");
    }

    return 0;
}

This is exactly the calculation every router performs millions of times a second: AND the address with the subnet mask to get the network address, and two IPs are on the same subnet when their network addresses match. No loops, no string comparison — just one AND across 32 bits.

C. Flag registers — status flags as a bitmask

In embedded and systems programming, hardware registers commonly use individual bits to represent different states. Bitwise operations let you set, clear, test and toggle each flag:

flag_registers.c
#include <stdio.h>
#include <stdint.h>

// Define the status flags (one bit each)
#define FLAG_ACTIVE   (1 << 0)  // Bit 0: currently active
#define FLAG_ADMIN    (1 << 1)  // Bit 1: admin rights
#define FLAG_VERIFIED (1 << 2)  // Bit 2: verified
#define FLAG_BANNED   (1 << 3)  // Bit 3: banned

void print_flags(uint8_t flags) {
    printf("Flags: [%s%s%s%s] (0b",
        (flags & FLAG_ACTIVE)   ? "ACTIVE "   : "",
        (flags & FLAG_ADMIN)    ? "ADMIN "    : "",
        (flags & FLAG_VERIFIED) ? "VERIFIED " : "",
        (flags & FLAG_BANNED)   ? "BANNED "   : "");
    // Print as 8-bit binary
    for (int i = 7; i >= 0; i--) {
        printf("%d", (flags >> i) & 1);
    }
    printf(")\n");
}

int main() {
    uint8_t user_flags = 0;  // Start with no flags set

    // SET a flag: use OR (|)
    user_flags |= FLAG_ACTIVE;    // Set bit 0
    user_flags |= FLAG_VERIFIED;  // Set bit 2
    print_flags(user_flags);      // [ACTIVE VERIFIED ] (0b00000101)

    // CLEAR a flag: use AND with NOT (~)
    user_flags &= ~FLAG_VERIFIED; // Clear bit 2
    print_flags(user_flags);      // [ACTIVE ] (0b00000001)

    // TOGGLE a flag: use XOR (^)
    user_flags ^= FLAG_ADMIN;     // Turns admin on (it was off)
    print_flags(user_flags);      // [ACTIVE ADMIN ] (0b00000011)
    user_flags ^= FLAG_ADMIN;     // Turns admin off again (it was on)
    print_flags(user_flags);      // [ACTIVE ] (0b00000001)

    // TEST a flag: use AND (&)
    if (user_flags & FLAG_ACTIVE) {
        printf("User dang hoat dong\n");
    }
    if (!(user_flags & FLAG_BANNED)) {
        printf("User chua bi cam\n");
    }

    return 0;
}

📥 Download the sample source: bitwise.c

Did you know?

The operation n & 1 checks whether a number is odd or even far more quickly than the usual n % 2, because it inspects that number's last bit directly (last bit 1 means odd, 0 means even).

📝 Check your understanding — Lesson 3
Given int a = 5; (binary 0101) and int b = 9; (binary 1001), what is the decimal result of the bitwise XOR a ^ b?

Related lessons in this series

Lesson 4: Branching & loops in C Lesson 2: C syntax basics, variables, data types & I/O Back to the C series roadmap (Vietnamese)