Skip to main content
C · lesson 5 of 25
C beginner Lesson 5 of 25

Operators in C

C arithmetic, bitwise, shift, logical and ternary operators with real output, including negative modulo and the precedence bugs -Wall catches.

By SkillByExample · Updated

Arithmetic Operators

The five arithmetic operators behave as you’d expect until integer division and negative numbers get involved:

#include <stdio.h>

int main(void) {
    int a = 17, b = 5;

    printf("%d + %d = %d\n",  a, b, a + b);
    printf("%d - %d = %d\n",  a, b, a - b);
    printf("%d * %d = %d\n",  a, b, a * b);
    printf("%d / %d = %d\n",  a, b, a / b);
    printf("%d %% %d = %d\n", a, b, a % b);
    printf("-%d / %d = %d\n", a, b, -a / b);
    printf("-%d %% %d = %d\n", a, b, -a % b);
    printf("(double)%d / %d = %.4f\n", a, b, (double)a / b);
    return 0;
}
$ gcc -Wall -Wextra -std=c17 -o arith arith.c
$ ./arith
17 + 5 = 22
17 - 5 = 12
17 * 5 = 85
17 / 5 = 3
17 % 5 = 2
-17 / 5 = -3
-17 % 5 = -2
(double)17 / 5 = 3.4000

Integer division throws away the fraction, truncating toward zero, so -17 / 5 is -3, not -4. % is defined so that (a / b) * b + a % b == a always holds, which forces -17 % 5 to be -2. Python gives 3 for the same expression. Code that uses % to wrap an index must handle negative inputs, for example with ((i % n) + n) % n. %% in the format string prints a literal percent sign.

Increment, Decrement, and Compound Assignment

Prefix ++x increments and then yields the new value. Postfix x++ yields the old value and then increments:

#include <stdio.h>

int main(void) {
    int x = 5;
    int a = ++x;   /* prefix: increment, then use */
    int b = x++;   /* postfix: use, then increment */
    printf("x=%d, a=%d, b=%d\n", x, a, b);

    int n = 10;
    n += 5;  printf("n += 5 -> %d\n", n);
    n -= 3;  printf("n -= 3 -> %d\n", n);
    n *= 2;  printf("n *= 2 -> %d\n", n);
    n /= 4;  printf("n /= 4 -> %d\n", n);
    n %= 4;  printf("n %%= 4 -> %d\n", n);
    return 0;
}
$ gcc -Wall -Wextra -std=c17 -o incdec incdec.c
$ ./incdec
x=7, a=6, b=6
n += 5 -> 15
n -= 3 -> 12
n *= 2 -> 24
n /= 4 -> 6
n %= 4 -> 2

a and b are both 6, but for different reasons. ++x bumped x from 5 to 6 and handed back 6. x++ handed back that same 6 and only then bumped x to 7.

Don’t Modify a Variable Twice in One Expression

#include <stdio.h>

int main(void) {
    int i = 1;
    i = i++ + ++i;   /* modifies i twice with no sequence point */
    printf("i = %d\n", i);
    return 0;
}
$ gcc -Wall -Wextra -std=c17 -o seqpoint seqpoint.c
seqpoint.c: In function 'main':
seqpoint.c:5:7: warning: operation on 'i' may be undefined [-Wsequence-point]
    5 |     i = i++ + ++i;   /* modifies i twice with no sequence point */
      |     ~~^~~~~~~~~~~
seqpoint.c:5:7: warning: operation on 'i' may be undefined [-Wsequence-point]
$ ./seqpoint
i = 4

GCC printed 4, but no answer is correct: this is undefined behavior, and another compiler or optimization level may print something else. C does not specify the order in which the operands of + are evaluated. Changing i more than once in a single expression is off-limits. Split it into separate statements.

Relational and Logical Operators

Comparisons produce the int values 1 (true) or 0 (false). && and || short-circuit: they stop evaluating as soon as the result is known.

#include <stdio.h>

int expensive_check(void) {
    printf("  (expensive_check ran)\n");
    return 1;
}

int main(void) {
    int a = 5, b = 10;
    printf("a == b: %d, a < b: %d, a >= b: %d\n", a == b, a < b, a >= b);

    int x = 0;
    if (x != 0 && 10 / x > 1) {    /* division never reached when x == 0 */
        printf("unreachable\n");
    }
    printf("guarded division skipped safely\n");

    printf("|| with true left side:\n");
    if (1 || expensive_check()) printf("  result: true\n");
    printf("|| with false left side:\n");
    if (0 || expensive_check()) printf("  result: true\n");

    printf("!0 = %d, !5 = %d, !!5 = %d\n", !0, !5, !!5);
    return 0;
}
$ gcc -Wall -Wextra -std=c17 -o logical logical.c
$ ./logical
a == b: 0, a < b: 1, a >= b: 0
guarded division skipped safely
|| with true left side:
  result: true
|| with false left side:
  (expensive_check ran)
  result: true
!0 = 1, !5 = 0, !!5 = 1

expensive_check ran only once, when the left side of || was false and the answer still depended on it. The x != 0 && guard works the same way: division by zero would crash the program, and short-circuiting guarantees it never happens. !!5 is a common idiom for turning any non-zero value into exactly 1.

Bitwise Operators

Bitwise operators work on the individual bits of integers. Standard C17 has no binary literal and no printf conversion for binary, so this example writes its values in hex and uses a small helper to print the bits:

#include <stdio.h>

void print_bits(const char *label, unsigned char v) {
    printf("%-7s = ", label);
    for (int i = 7; i >= 0; i--) {
        putchar((v >> i) & 1 ? '1' : '0');
    }
    printf(" (%3d)\n", v);
}

int main(void) {
    unsigned char a = 0xB4;   /* 1011 0100 */
    unsigned char b = 0xCA;   /* 1100 1010 */

    print_bits("a", a);
    print_bits("b", b);
    print_bits("a & b", a & b);   /* 1 where both are 1 */
    print_bits("a | b", a | b);   /* 1 where either is 1 */
    print_bits("a ^ b", a ^ b);   /* 1 where they differ */
    print_bits("~a", ~a);         /* every bit flipped */
    print_bits("a << 2", a << 2); /* shifted left, top bits lost */
    print_bits("a >> 2", a >> 2); /* shifted right, zeros shift in */
    return 0;
}
$ gcc -Wall -Wextra -Wpedantic -std=c17 -o bitwise bitwise.c
$ ./bitwise
a       = 10110100 (180)
b       = 11001010 (202)
a & b   = 10000000 (128)
a | b   = 11111110 (254)
a ^ b   = 01111110 (126)
~a      = 01001011 ( 75)
a << 2  = 11010000 (208)
a >> 2  = 00101101 ( 45)

Before any bitwise operation, a is promoted to a full int. a << 2 is really 720, and passing it to the unsigned char parameter keeps only the low 8 bits, 208. The two top bits fell off the end. 0b10110100 literals and a %b printf conversion are standardized in C23. GCC accepts them in C17 mode as an extension, but -Wpedantic flags them: “binary constants are a C23 feature or GCC extension”.

Flag Patterns: Set, Clear, Toggle, Test

These four idioms appear everywhere in systems code that packs on/off states into one integer:

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

#define STATUS_READY   (1u << 0)
#define STATUS_BUSY    (1u << 1)
#define STATUS_ERROR   (1u << 2)
#define STATUS_TIMEOUT (1u << 3)

int main(void) {
    uint8_t status = 0;

    status |= STATUS_READY | STATUS_ERROR;   /* set two bits */
    printf("after set:    0x%02X\n", status);

    status &= ~STATUS_ERROR;                 /* clear one bit */
    printf("after clear:  0x%02X\n", status);

    status ^= STATUS_BUSY;                   /* toggle a bit */
    printf("after toggle: 0x%02X\n", status);

    printf("ready? %s\n", (status & STATUS_READY) ? "yes" : "no");
    printf("error? %s\n", (status & STATUS_ERROR) ? "yes" : "no");
    return 0;
}
$ gcc -Wall -Wextra -std=c17 -o flags flags.c
$ ./flags
after set:    0x05
after clear:  0x01
after toggle: 0x03
ready? yes
error? no

0x05 is binary 0101: bits 0 (READY) and 2 (ERROR). Clearing uses &= ~mask, where the ~ builds a mask with every bit set except the one to clear. The 1u makes each constant unsigned, so shifting into the top bit of a 32-bit value can’t hit signed-overflow undefined behavior.

Shift Operators

Shifting left by N multiplies by 2^N. Shifting an unsigned value right by N divides by 2^N. Negative numbers are where it gets interesting:

#include <stdio.h>

int main(void) {
    unsigned int x = 1;
    printf("1 << 1 = %u, 1 << 4 = %u, 1 << 8 = %u\n", x << 1, x << 4, x << 8);

    unsigned int y = 256;
    printf("256 >> 1 = %u, 256 >> 4 = %u, 256 >> 8 = %u\n", y >> 1, y >> 4, y >> 8);

    int neg = -16;
    printf("-16 >> 2 = %d\n", neg >> 2);   /* implementation-defined */
    printf("-16 / 4  = %d\n", neg / 4);
    printf("-17 >> 2 = %d, -17 / 4 = %d\n", -17 >> 2, -17 / 4);
    return 0;
}
$ gcc -Wall -Wextra -std=c17 -o shifts shifts.c
$ ./shifts
1 << 1 = 2, 1 << 4 = 16, 1 << 8 = 256
256 >> 1 = 128, 256 >> 4 = 16, 256 >> 8 = 1
-16 >> 2 = -4
-16 / 4  = -4
-17 >> 2 = -5, -17 / 4 = -4

On GCC, right-shifting a negative int copies the sign bit in (an arithmetic shift), so -16 >> 2 is -4. The standard only calls this implementation-defined. Even here, >> 2 and / 4 disagree for -17: the shift rounds toward negative infinity (-5), while division truncates toward zero (-4). Use unsigned types for bit manipulation, and write / when you mean division. The compiler turns division by a power of two into a shift, plus a fix-up for negatives, on its own.

The Ternary Operator

condition ? a : b is an expression that yields one of two values, handy for short conditional values:

#include <stdio.h>

int max(int a, int b) {
    return (a > b) ? a : b;
}

const char *classify(int n) {
    return (n > 0) ? "positive" : (n < 0) ? "negative" : "zero";
}

int main(void) {
    printf("max(3, 9) = %d\n", max(3, 9));
    int values[] = {7, -2, 0};
    for (int i = 0; i < 3; i++) {
        printf("%d is %s\n", values[i], classify(values[i]));
    }
    int items = 1;
    printf("%d item%s\n", items, items == 1 ? "" : "s");
    return 0;
}
$ gcc -Wall -Wextra -std=c17 -o ternary ternary.c
$ ./ternary
max(3, 9) = 9
7 is positive
-2 is negative
0 is zero
1 item

Because ?: is an expression, not a statement, it can appear inside a printf argument list, as in the plural-s trick. The chained form in classify reads well only while it stays this short. Beyond two conditions, switch to if/else.

The Comma Operator

The comma operator evaluates its left operand, throws the result away, then evaluates and yields its right operand. Its main legitimate home is a for loop header:

#include <stdio.h>

int main(void) {
    for (int i = 0, j = 4; i < j; i++, j--) {
        printf("i=%d, j=%d\n", i, j);
    }

    int x = (printf("side effect\n"), 42);
    printf("x = %d\n", x);
    return 0;
}
$ gcc -Wall -Wextra -std=c17 -o comma2 comma2.c
$ ./comma2
i=0, j=4
i=1, j=3
side effect
x = 42

i++, j-- is the comma operator: both updates run each iteration. The comma in int i = 0, j = 4 is not. That’s declaration syntax, the same as int a, b;. The parentheses around (printf(...), 42) are required, because without them the comma would separate declarators and the line would not compile.

Operator Precedence

Higher rows bind tighter. When in doubt, add parentheses. They cost nothing and remove ambiguity for the reader too.

PrecedenceOperatorsAssociativity
1 (highest)() [] -> . postfix ++ --Left to right
2! ~ prefix ++ -- unary + - * & sizeof castsRight to left
3* / %Left to right
4+ -Left to right
5<< >>Left to right
6< <= > >=Left to right
7== !=Left to right
8&Left to right
9^Left to right
10|Left to right
11&&Left to right
12||Left to right
13?:Right to left
14= += -= etc.Right to left
15 (lowest),Left to right

Two rows cause most real bugs: shifts sit below +, and & sits below ==.

#include <stdio.h>

int main(void) {
    int flags = 0x06;   /* bits 1 and 2 set */

    printf("2 + 3 * 4       = %d\n", 2 + 3 * 4);
    printf("1 << 2 + 1      = %d\n", 1 << 2 + 1);
    printf("(1 << 2) + 1    = %d\n", (1 << 2) + 1);

    if (flags & 0x04 == 0x04) printf("buggy test: bit 2 set\n");
    else                      printf("buggy test: bit 2 NOT set\n");

    if ((flags & 0x04) == 0x04) printf("fixed test: bit 2 set\n");
    return 0;
}
$ gcc -Wall -Wextra -std=c17 -o precedence precedence.c
precedence.c: In function 'main':
precedence.c:7:40: warning: suggest parentheses around '+' inside '<<' [-Wparentheses]
    7 |     printf("1 << 2 + 1      = %d\n", 1 << 2 + 1);
      |                                        ^~
precedence.c:10:15: warning: suggest parentheses around comparison in operand of '&' [-Wparentheses]
   10 |     if (flags & 0x04 == 0x04) printf("buggy test: bit 2 set\n");
      |               ^
$ ./precedence
2 + 3 * 4       = 14
1 << 2 + 1      = 8
(1 << 2) + 1    = 5
buggy test: bit 2 NOT set
fixed test: bit 2 set

Bit 2 is set in 0x06, yet the first test says it isn’t. C parsed flags & 0x04 == 0x04 as flags & (0x04 == 0x04), which is flags & 1, and bit 0 of 0x06 is clear. The program compiled and ran with no error. Only the -Wparentheses warning (on by default under -Wall) pointed at it, which is a strong argument for never ignoring warnings.

Practice

  1. Write is_even(int n) using a bitwise operator instead of %, and test it on -3 to 3.
Solution
#include <stdio.h>

int is_even(int n) { return (n & 1) == 0; }

int main(void) {
    for (int n = -3; n <= 3; n++) {
        printf("%2d: %s\n", n, is_even(n) ? "even" : "odd");
    }
    return 0;
}
$ gcc -Wall -Wextra -std=c17 -o p1 p1.c
$ ./p1
-3: odd
-2: even
-1: odd
 0: even
 1: odd
 2: even
 3: odd

An even number’s lowest bit is always 0. That holds for negatives too, because C23 requires two’s complement and every mainstream C17 compiler already uses it. The parentheses around n & 1 matter: without them, n & 1 == 0 means n & (1 == 0), which is always 0.

  1. Write count_bits(unsigned int v) that returns how many bits are set to 1.
Solution
#include <stdio.h>

int count_bits(unsigned int v) {
    int count = 0;
    while (v) {
        count += v & 1;
        v >>= 1;
    }
    return count;
}

int main(void) {
    unsigned int tests[] = {0, 1, 7, 0xB4, 0xFFFFFFFF};
    for (int i = 0; i < 5; i++) {
        printf("%u has %d bits set\n", tests[i], count_bits(tests[i]));
    }
    return 0;
}
$ gcc -Wall -Wextra -std=c17 -o p2 p2.c
$ ./p2
0 has 0 bits set
1 has 1 bits set
7 has 3 bits set
180 has 4 bits set
4294967295 has 32 bits set

The parameter must be unsigned. With a negative int, >>= would keep shifting the sign bit back in, v would never reach 0, and the loop would never end.

  1. Convert 3725 seconds into HH:MM:SS using only / and %.
Solution
#include <stdio.h>

int main(void) {
    int total_seconds = 3725;
    int hours = total_seconds / 3600;
    int minutes = total_seconds % 3600 / 60;
    int seconds = total_seconds % 60;
    printf("%d seconds = %02d:%02d:%02d\n", total_seconds, hours, minutes, seconds);
    return 0;
}
$ gcc -Wall -Wextra -std=c17 -o p3 p3.c
$ ./p3
3725 seconds = 01:02:05

% and / share a precedence level and group left to right, so total_seconds % 3600 / 60 means (total_seconds % 3600) / 60: the leftover seconds after whole hours, turned into minutes. %02d pads each field to two digits with a leading zero.

  1. Swap two integers using XOR, with no temporary variable. Predict the result first.
Solution
#include <stdio.h>

int main(void) {
    int a = 5, b = 9;
    a ^= b;
    b ^= a;
    a ^= b;
    printf("a = %d, b = %d\n", a, b);
    return 0;
}
$ gcc -Wall -Wextra -std=c17 -o p4 p4.c
$ ./p4
a = 9, b = 5

XOR undoes itself (x ^ y ^ y == x), so three XORs swap the values. It’s a classic interview trick, but it’s not faster than a temporary on modern compilers. It also zeroes the value if both operands are the same variable, as in a swap through two pointers to the same location.

Frequently Asked Questions

What is the difference between & and &&?
& is the bitwise AND operator: it combines two integers bit by bit and always evaluates both sides. && is the logical AND operator: it treats each side as true or false, returns 1 or 0, and short-circuits, skipping the right side entirely when the left side is false.
What does the % operator do with negative numbers?
Since C99, integer division truncates toward zero and the result of a % b takes the sign of a. So -17 % 5 is -2, not 3. Python differs: its % result takes the sign of the divisor, so the same expression gives 3 there.
Why should I know bitwise operators?
Bitwise operations are essential for systems programming: setting and clearing hardware flags, packing several values into one integer, parsing binary protocols and file formats, and implementing hashing and cryptography. They also show up constantly in interview questions.
Why does flags & MASK == MASK give the wrong answer?
== has higher precedence than &, so C parses it as flags & (MASK == MASK), which is flags & 1. The test then checks bit 0 instead of the mask. Always parenthesize bitwise tests: (flags & MASK) == MASK. GCC's -Wall warns about this with -Wparentheses.
Is i = i++ + ++i defined in C?
No. Modifying the same variable twice in one expression without a sequence point between the modifications is undefined behavior, so any result, or none, is allowed. GCC warns about it with -Wsequence-point, which -Wall enables. Split the expression into separate statements.