Introduction
Bitwise operators work at the bit level and perform operations on individual bits of data. These operators are particularly useful for low-level programming, system programming, embedded systems, and optimization tasks. They manipulate data at the most fundamental level - the binary representation of numbers.
Understanding bitwise operations is crucial for memory optimization, flag manipulation, cryptography, and performance-critical applications where direct bit manipulation provides speed advantages over arithmetic operations.
Key Concepts
Bit: Smallest unit of data (0 or 1) Binary Representation: How numbers are stored as sequences of bits Bit Position: Location of a bit in a binary number (0-indexed from right) Bit Manipulation: Directly modifying individual bits in data
Binary Number System Review
#include <stdio.h>
void printBinary(int n) {
for (int i = 7; i >= 0; i--) {
printf("%d", (n >> i) & 1);
}
printf(" (%d)\n", n);
}
int main() {
printf("Decimal -> Binary representation:\n");
printBinary(5); // 00000101
printBinary(10); // 00001010
printBinary(15); // 00001111
printBinary(7); // 00000111
return 0;
}
Bitwise AND (&)
Basic AND Operation
#include <stdio.h>
int main() {
int a = 12; // 1100 in binary
int b = 10; // 1010 in binary
int result = a & b; // 1000 in binary (8 in decimal)
printf("a = %d (binary: 1100)\n", a);
printf("b = %d (binary: 1010)\n", b);
printf("a & b = %d (binary: 1000)\n", result);
return 0;
}
Practical Uses of AND
#include <stdio.h>
int main() {
// Check if a number is even (last bit is 0)
int number = 15;
if ((number & 1) == 0) {
printf("%d is even\n", number);
} else {
printf("%d is odd\n", number);
}
// Clear specific bits (masking)
int value = 255; // 11111111
int mask = 240; // 11110000 (clear last 4 bits)
int cleared = value & mask; // 11110000 (240)
printf("Cleared last 4 bits: %d\n", cleared);
return 0;
}
Bitwise OR (|)
Basic OR Operation
#include <stdio.h>
int main() {
int a = 12; // 1100 in binary
int b = 10; // 1010 in binary
int result = a | b; // 1110 in binary (14 in decimal)
printf("a = %d (binary: 1100)\n", a);
printf("b = %d (binary: 1010)\n", b);
printf("a | b = %d (binary: 1110)\n", result);
return 0;
}
Setting Specific Bits
#include <stdio.h>
int main() {
int value = 8; // 00001000
int mask = 4; // 00000100
printf("Original value: %d\n", value);
// Set bit at position 2
value = value | mask; // 00001100 (12)
printf("After setting bit 2: %d\n", value);
// Set multiple bits
int flags = 0; // 00000000
flags |= 1; // Set bit 0: 00000001
flags |= 4; // Set bit 2: 00000101
flags |= 16; // Set bit 4: 00010101
printf("Flags with multiple bits set: %d\n", flags);
return 0;
}
Bitwise XOR (^)
Basic XOR Operation
#include <stdio.h>
int main() {
int a = 12; // 1100 in binary
int b = 10; // 1010 in binary
int result = a ^ b; // 0110 in binary (6 in decimal)
printf("a = %d (binary: 1100)\n", a);
printf("b = %d (binary: 1010)\n", b);
printf("a ^ b = %d (binary: 0110)\n", result);
return 0;
}
XOR Properties and Applications
#include <stdio.h>
int main() {
// XOR with itself gives 0
int x = 25;
printf("%d ^ %d = %d\n", x, x, x ^ x);
// XOR with 0 gives original number
printf("%d ^ 0 = %d\n", x, x ^ 0);
// Toggle specific bits
int value = 15; // 00001111
int toggle = 5; // 00000101
printf("Original: %d\n", value);
value ^= toggle; // Toggle bits: 00001010 (10)
printf("After toggle: %d\n", value);
value ^= toggle; // Toggle again: 00001111 (15)
printf("After second toggle: %d\n", value);
return 0;
}
Simple Encryption with XOR
#include <stdio.h>
int main() {
char message[] = "HELLO";
int key = 123; // Simple XOR key
printf("Original message: %s\n", message);
// Encrypt
for (int i = 0; message[i] != '\0'; i++) {
message[i] ^= key;
}
printf("Encrypted: ");
for (int i = 0; message[i] != '\0'; i++) {
printf("%c", message[i]);
}
printf("\n");
// Decrypt (XOR again with same key)
for (int i = 0; message[i] != '\0'; i++) {
message[i] ^= key;
}
printf("Decrypted: %s\n", message);
return 0;
}
Bitwise NOT (~)
Basic NOT Operation
#include <stdio.h>
int main() {
unsigned char a = 5; // 00000101 in binary
unsigned char result = ~a; // 11111010 in binary (250 in decimal)
printf("a = %d (binary: 00000101)\n", a);
printf("~a = %d (binary: 11111010)\n", result);
// With integers (32-bit)
int b = 10;
printf("b = %d\n", b);
printf("~b = %d\n", ~b); // Two's complement representation
return 0;
}
Left Shift (<<)
Basic Left Shift
#include <stdio.h>
int main() {
int value = 5; // 00000101 in binary
printf("Original: %d\n", value);
printf("Left shift by 1: %d\n", value << 1); // 00001010 (10)
printf("Left shift by 2: %d\n", value << 2); // 00010100 (20)
printf("Left shift by 3: %d\n", value << 3); // 00101000 (40)
// Left shift is equivalent to multiplication by 2^n
printf("\nUsing left shift for multiplication:\n");
printf("%d * 2 = %d\n", value, value << 1);
printf("%d * 4 = %d\n", value, value << 2);
printf("%d * 8 = %d\n", value, value << 3);
return 0;
}
Right Shift (>>)
Basic Right Shift
#include <stdio.h>
int main() {
int value = 40; // 00101000 in binary
printf("Original: %d\n", value);
printf("Right shift by 1: %d\n", value >> 1); // 00010100 (20)
printf("Right shift by 2: %d\n", value >> 2); // 00001010 (10)
printf("Right shift by 3: %d\n", value >> 3); // 00000101 (5)
// Right shift is equivalent to division by 2^n
printf("\nUsing right shift for division:\n");
printf("%d / 2 = %d\n", value, value >> 1);
printf("%d / 4 = %d\n", value, value >> 2);
printf("%d / 8 = %d\n", value, value >> 3);
return 0;
}
Practical Applications
Bit Manipulation Functions
#include <stdio.h>
// Set a specific bit
int setBit(int num, int position) {
return num | (1 << position);
}
// Clear a specific bit
int clearBit(int num, int position) {
return num & ~(1 << position);
}
// Toggle a specific bit
int toggleBit(int num, int position) {
return num ^ (1 << position);
}
// Check if a specific bit is set
int checkBit(int num, int position) {
return (num >> position) & 1;
}
int main() {
int number = 10; // 00001010
printf("Original number: %d\n", number);
// Set bit 0
number = setBit(number, 0);
printf("After setting bit 0: %d\n", number); // 11
// Clear bit 1
number = clearBit(number, 1);
printf("After clearing bit 1: %d\n", number); // 9
// Toggle bit 2
number = toggleBit(number, 2);
printf("After toggling bit 2: %d\n", number); // 13
// Check if bit 3 is set
if (checkBit(number, 3)) {
printf("Bit 3 is set\n");
} else {
printf("Bit 3 is not set\n");
}
return 0;
}
Counting Set Bits
#include <stdio.h>
int countSetBits(int n) {
int count = 0;
while (n) {
count += n & 1;
n >>= 1;
}
return count;
}
// Brian Kernighan's algorithm (more efficient)
int countSetBitsOptimized(int n) {
int count = 0;
while (n) {
n &= (n - 1); // Clear the lowest set bit
count++;
}
return count;
}
int main() {
int number = 15; // 00001111
printf("Number: %d\n", number);
printf("Set bits (method 1): %d\n", countSetBits(number));
printf("Set bits (optimized): %d\n", countSetBitsOptimized(number));
return 0;
}
Power of Two Check
#include <stdio.h>
int isPowerOfTwo(int n) {
return n > 0 && (n & (n - 1)) == 0;
}
int main() {
int numbers[] = {1, 2, 3, 4, 8, 15, 16, 32, 33};
int size = sizeof(numbers) / sizeof(numbers[0]);
for (int i = 0; i < size; i++) {
if (isPowerOfTwo(numbers[i])) {
printf("%d is a power of 2\n", numbers[i]);
} else {
printf("%d is not a power of 2\n", numbers[i]);
}
}
return 0;
}
Common Mistakes and Solutions
Operator Precedence Issues
// Wrong - bitwise operators have different precedence than arithmetic
if (x & mask == 0) { // Evaluated as: x & (mask == 0)
// Wrong logic
}
// Correct - use parentheses
if ((x & mask) == 0) { // Evaluated as: (x & mask) == 0
// Correct logic
}
Signed vs Unsigned Right Shift
// Be careful with signed numbers and right shift
int negative = -8;
printf("Signed right shift: %d\n", negative >> 1); // May not divide by 2
// Use unsigned for predictable behavior
unsigned int positive = 8;
printf("Unsigned right shift: %u\n", positive >> 1); // Always divides by 2
Important Points
- Bit Positions: Numbered from 0 (rightmost) to n-1 (leftmost)
- Performance: Bitwise operations are among the fastest CPU operations
- Portability: Results may vary on different architectures for signed numbers
- Overflow: Left shifting can cause overflow; right shifting can lose precision
- Use Cases: Flags, permissions, optimization, embedded programming
- Precedence: Bitwise operators have lower precedence than arithmetic operators
Summary
Bitwise operators provide powerful tools for direct bit manipulation in C programming. The AND (&), OR (|), XOR (^), and NOT (~) operators work on individual bits, while left shift (<<) and right shift (>>) operators move bits positions. These operators are essential for system programming, embedded development, cryptography, and performance optimization. Common applications include setting/clearing flags, checking specific bits, fast multiplication/division by powers of 2, and implementing efficient algorithms. Understanding binary representation and operator precedence is crucial for correct usage. While bitwise operations offer significant performance benefits, they require careful consideration of signed vs unsigned behavior and potential portability issues across different architectures.
Part of BCA Programming with C Course (UGCOA22J201)