Lesson 04: Operators

In this lesson, we will explore the various operators available in C programming. Operators are special symbols or keywords that perform operations on operands. Understanding how to use these operators effectively is crucial for writing efficient and concise code.

Objectives

By the end of this lesson, you should be able to:

  • Understand and use arithmetic operators in C.
  • Apply increment and decrement operators.
  • Use assignment operators for value assignments.
  • Compare values using relational operators.
  • Implement logical operations with logical operators.
  • Manipulate bits using bitwise operators.
  • Utilize other special operators like the comma operator and the sizeof operator.

Arithmetic Operators

Arithmetic operators are used to perform basic mathematical operations.

Operator Description Example
+ Addition a + b
- Subtraction a - b
* Multiplication a * b
/ Division a / b
% Modulus (Remainder) a % b
#include <stdio.h> 
 
int main() { 
    int a = 10; 
    int b = 3; 
 
    printf("Addition: %d + %d = %d\n", a, b, a + b); 
    printf("Subtraction: %d - %d = %d\n", a, b, a - b); 
    printf("Multiplication: %d * %d = %d\n", a, b, a * b); 
    printf("Division: %d / %d = %d\n", a, b, a / b); 
    printf("Modulus: %d %% %d = %d\n", a, b, a % b); 
 
    return 0; 
} 
  • Addition: Adds two operands.
  • 10 + 3 = 13
  • Subtraction: Subtracts the second operand from the first.
  • 10 - 3 = 7
  • Multiplication: Multiplies two operands.
  • 10 * 3 = 30
  • Division: Divides the first operand by the second. Note that for integers, this operation results in an integer (truncated division).
  • 10 / 3 = 3
  • Modulus: Returns the remainder of the division of the first operand by the second.
  • 10 % 3 = 1

Increment and Decrement Operators

Increment and decrement operators are unary operators that increase or decrease the value of a variable by one, respectively.

Operator Description Example
++ Increment ++a or a++
-- Decrement --a or a--
#include <stdio.h> 
 
int main() { 
    int a = 5; 
 
    printf("Initial value: %d\n", a);        /* prints: 5 */ 
    printf("Pre-increment: %d\n", ++a);      /* prints: 6 */ 
    printf("Post-increment: %d\n", a++);     /* prints: 6 */ 
    printf("After post-increment: %d\n", a); /* prints: 7 */ 
    printf("Pre-decrement: %d\n", --a);      /* prints: 6 */ 
    printf("Post-decrement: %d\n", a--);     /* prints: 6 */ 
    printf("After post-decrement: %d\n", a); /* prints: 5 */ 
 
    return 0; 
} 
  • Pre-increment (++a): Increments the value of a before using it in an expression.
  • Post-increment (a++): Uses the current value of a in an expression and then increments it.
  • Pre-decrement (--a): Decrements the value of a before using it in an expression.
  • Decrements the value of a before using it in an expression.: Uses the current value of a in an expression and then decrements it.

Bitwise Operators

Bitwise operators are used to perform operations on individual bits of integer data types. These operations are crucial in low-level programming, where direct manipulation of bits is required. The common bitwise operators in C include AND &, OR |, XOR ^, NOT ~, left shift <<, and right shift >>.

Operator Description Example
& Bitwise AND a & b
| Bitwise OR a | b
^ Bitwise XOR (excessive OR) a ^ b
~ Bitwise NOT ~a
<< Left shift a << b
>> Right shift a >> b
#include <stdio.h> 
 
int main() { 
    unsigned int a = 5;   /* 0101 in binary */ 
    unsigned int b = 3;   /* 0011 in binary */ 
 
    printf("a & b: %u\n", a & b);   /* 0001 in binary, which is 1 */ 
    printf("a | b: %u\n", a | b);   /* 0111 in binary, which is 7 */ 
    printf("a ^ b: %u\n", a ^ b);   /* 0110 in binary, which is 6 */ 
    printf("~a: %u\n", ~a);         /* 1111...1010 in binary, which 
                                       is -6 in 2's complement form 
                                       or 4294967290 unsigned */ 
    printf("a << 1: %u\n", a << 1); /* 1010 in binary, which is 10 */ 
    printf("a >> 1: %u\n", a >> 1); /* 0010 in binary, which is 2 */ 
 
    return 0; 
} 
  • & Bitwise AND compares each bit of the first operand to the corresponding bit of the second operand. If both bits are 1, the resulting bit is set to 1.

    pic-0.png

  • | Bitwise OR compares each bit of the first operand to the corresponding bit of the second operand. If either bit is 1, the resulting bit is set to 1.

    pic-1.png

  • ^ Bitwise XOR compares each bit of the first operand to the corresponding bit of the second operand. If the bits are different, the resulting bit is set to 1.

    pic-2.png

  • ~ Bitwise NOT inverts all the bits of the operand.

    pic-3.png

    Note: in the example above ~5 was equal to 4294967290. This is becuase an integer is 32 bits long and not just 4 bits, so 1010 should actually be 11111111111111111111111111111010.
  • << Left shift shifts the bits of the first operand to the left by the number of positions specified by the second operand.

    pic-4.png

  • >> Right shift shifts the bits of the first operand to the right by the number of positions specified by the second operand.

    pic-5.png

    Note: In actuality a new zero is not added from the left as an integer is 32 bits long. The first bit (the 1) is removed though.

XOR and Simple Encryption

The XOR operator is a bit special and can be used for simple encryption and decryption, known as the XOR cipher. This method involves XORing the plaintext with a key to produce the ciphertext. To decrypt, the ciphertext is XORed with the same key to retrieve the original plaintext.

The XOR cipher works because of the properties of the XOR operation. Specifically, the XOR operation is its own inverse. This means that if you apply the XOR operation twice with the same key, you get back the original value. Here’s a detailed explanation:

  1. Initial XOR Operation (Encryption): When you XOR the plaintext with the key, you get the ciphertext. For example, if P is the plaintext and K is the key, then C = P ^ K where C is the ciphertext.
  2. Decrypting XOR Operation: When you XOR the ciphertext with the same key, you retrieve the original plaintext. Using the same example, P = C ^ K can be expanded as P = (P ^ K) ^ K.
  3. Property of XOR: The operation (P ^ K) ^ K simplifies to P because of the associative property of XOR.
    • P ^ K ^ K results in P ^ 0 (since K ^ K equals 0 for any K).
    • P ^ 0 equals P (since XORing any value with 0 leaves it unchanged).

pic-6.png

This property ensures that the XOR operation can securely encrypt and decrypt data with the same key, making it a simple yet effective method for encryption in certain scenarios. However, it is important to note that the XOR cipher is not suitable for secure encryption in most practical applications due to its simplicity and vulnerability to attacks if the key is reused or known.

By using a seed to generate the key, you can create a more secure implementation of the XOR cipher. This method means that the security of the encryption relies on the secrecy of the seed and the PRNG algorithm.

Using a Seed to Generate the Key for XOR Cipher

  1. Seed Initialization: A seed value is used to initialize the PRNG. This seed must be kept secret to ensure the security of the encryption.
  2. Key Generation: The PRNG generates a pseudorandom sequence of bytes that will be used as the key for encryption and decryption.
  3. Encryption: The plaintext is XORed with the key to produce the ciphertext.
  4. Decryption: The same seed is used to reinitialize the PRNG, generating the same sequence of bytes for the key. The ciphertext is then XORed with this key to recover the plaintext.

Here’s an example of how you can implement this in C:

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <time.h> 
 
void xor_encrypt_decrypt(char *input, char *output, int seed) { 
    srand(seed);  /* Initialize the PRNG with the seed */ 
    for (int i = 0; i < strlen(input); i++) { 
        output[i] = input[i] ^ (rand() % 256);  /* XOR with pseudorandom byte */ 
    } 
    output[strlen(input)] = '\0'; 
} 
 
int main() { 
    char plaintext[] = "HELLO"; 
    int seed = 12345;            /* Secret seed value, could be 
                                    generated from as password */ 
    char ciphertext[6];          /* +1 for null-terminator */ 
    char decryptedtext[6]; 
 
    xor_encrypt_decrypt(plaintext, ciphertext, seed); 
    printf("Ciphertext: "); 
    for (int i = 0; i < strlen(ciphertext); i++) { 
        printf("%02X ", (unsigned char)ciphertext[i]); 
    } 
    printf("\n"); 
 
    xor_encrypt_decrypt(ciphertext, decryptedtext, seed); 
    printf("Decrypted text: %s\n", decryptedtext); 
 
    return 0; 
} 
Note: It is never recommended to implement your own encryption algorithms for use in real-life applications. Always use cryptographically proven and tested algorithms that have been thoroughly vetted by security experts. While experimenting with encryption can be educational and fun, remember that security is a critical concern and should always rely on widely tested practices.

Assignment Operators

Assignment operators are used to assign values to variables.

Operator Description Example Equivalent to
= Assign a = b
+= Add and assign a += b a = a + b
-= Subtract and assign a -= b a = a - b
*= Multiply and assign a *= b a = a * b
/= Divide and assign a =/ b a = a / b
%= Modulus and assign a %= b a = a % b
&= Bitwise AND and assign a &= b a = a & b
|= Bitwise OR and assign a |= b a = a | b
^= Bitwise XOR and assign a ^= b a = a ^ b
<<= Left shift and assign a <<= b a = a << b
>>= Right shift and assign a >>= b a = a >> b
#include <stdio.h> 
 
int main() { 
    int a; 
    int b = 3; 
 
    a = 5; 
    a += b; 
    printf("a += b: %d\n", a); 
 
    a = 5; 
    a -= b; 
    printf("a -= b: %d\n", a); 
 
    a = 5; 
    a *= b; 
    printf("a *= b: %d\n", a); 
 
    a = 5; 
    a /= b; 
    printf("a /= b: %d\n", a); 
 
    a = 5; 
    a %= b; 
    printf("a %%= b: %d\n", a); 
 
    a = 5;  /* 101 */ 
    a &= b; /* 101 &= 011 -> 001*/ 
    printf("a &= b: %d\n", a); 
 
    a = 5;  /* 101 */ 
    a |= b; /* 101 |= 011 -> 111 */ 
    printf("a |= b: %d\n", a); 
 
    a = 5;  /* 101 */ 
    a ^= b; /* 101 ^= 011 -> 110 */ 
    printf("a ^= b: %d\n", a); 
 
    a = 5;   /* 101 */ 
    a <<= b; /* 101 <<= 3 -> 101000 */ 
    printf("a <<= b: %d\n", a); 
 
    a = 256; /* 100000000 */ 
    a >>= b; /* 100000000 >>= 3 -> 100000 */ 
    printf("a >>= b: %d\n", a); 
 
    return 0; 
} 
  • = Assigns the right-hand value to the left-hand variable.
  • += Adds the right-hand value to the left-hand variable and assigns the result to the left-hand variable.
  • -= Subtracts the right-hand value from the left-hand variable and assigns the result to the left-hand variable.
  • *= Multiplies the left-hand variable by the right-hand value and assigns the result to the left-hand variable.
  • /= Divides the left-hand variable by the right-hand value and assigns the result to the left-hand variable.
  • %= Computes the modulus of the left-hand variable by the right-hand value and assigns the result to the left-hand variable.
  • &= Performs a bitwise AND between the left-hand variable and the right-hand value and assigns the result to the left-hand variable.
  • |= Performs a bitwise OR between the left-hand variable and the right-hand value and assigns the result to the left-hand variable.
  • ^= Performs a bitwise XOR between the left-hand variable and the right-hand value and assigns the result to the left-hand variable.
  • <<= Performs a left bitwise shift on the left-hand variable by the number of positions specified by the right-hand value and assigns the result to the left-hand variable.
  • >>= Performs a right bitwise shift on the left-hand variable by the number of positions specified by the right-hand value and assigns the result to the left-hand variable.

Relational Operators

Relational operators are used to compare two values. They return either true (1) or false (0).

Operator Description Example True example False example
== Equal to a == b 1 == 1 1 == 2
!= Not equal to a != b 1 != 2 1 != 1
> Greater than a > b 2 > 1 1 > 2
< Less than a < b 1 < 2 2 < 1
>= Greater than or equal to a >= b 2 >= 2 1 >= 2
<= Less than or equal to a <= b 1 <= 2 2 <= 1
#include <stdio.h> 
 
int main() { 
    int a = 5; 
    int b = 3; 
 
    printf("a == b: %d\n", a == b); 
    printf("a != b: %d\n", a != b); 
    printf("a > b: %d\n", a > b); 
    printf("a < b: %d\n", a < b); 
    printf("a >= b: %d\n", a >= b); 
    printf("a <= b: %d\n", a <= b); 
 
    return 0; 
} 
  • ==: Checks if two operands are equal.
  • !=: Checks if two operands are not equal.
  • >: Checks if the left operand is greater than the right operand.
  • <: Checks if the left operand is less than the right operand.
  • >=: Checks if the left operand is greater than or equal to the right operand.
  • <=: Checks if the left operand is less than or equal to the right operand.

Logical Operators

Operator Description Example
&& Logical AND a && b
|| Logical OR a || b
! Logical NOT !a
#include <stdio.h> 
 
int main() { 
    int a = 5; 
    int b = 3; 
    int c = 0; 
 
    printf("a && b: %d\n", a && b); 
    printf("a && c: %d\n", a && c); 
    printf("a || c: %d\n", a || c); 
    printf("!a: %d\n", !a); 
    printf("!c: %d\n", !c); 
 
    return 0; 
} 
  • &&: Logical AND returns true if both operands are true.
  • ||: Logical OR returns true if at least one of the operands is true.
  • !: Logical NOT returns true if the operand is false and vice versa.

Other Operators

Operator Description Example
, Comma operator a, b
sizeof sizeof Operator sizeof a or sizeof(a)
?: Ternary operator a = condition ? if_true : if_false
[] Array subscript operator a[b]
& Address operator or reference operator &a
* Star operator or dereference operator *b
. Member access operator a.b
-> Member access through pointer operator a->b

Comma Operator

The comma operator (,) allows two expressions to be evaluated in a single statement, with the value of the entire statement being the value of the second expression.

#include <stdio.h> 
 
int main() { 
    int a, b;           /* both a and b are declared on one line */ 
 
    a = (b = 5, b + 2); /* b is assigned 5, and then a is assigned b + 2 */ 
 
    printf("a: %d, b: %d\n", a, b); /* a is 7, b is 5 */ 
 
    return 0; 
} 

sizeof Operator

The sizeof operator returns the size (in bytes) allocated in memory of its operand.

#include <stdio.h> 
 
int main() { 
    int a; 
    double b; 
 
    printf("Size of int: %zu bytes\n", sizeof(a)); 
    printf("Size of double: %zu bytes\n", sizeof b); 
 
    return 0; 
} 

Ternary Operator

The ternary operator (?:) is a shorthand for an if-else statement.

#include <stdio.h> 
 
int main() { 
    int a = 10, b = 20; 
    int max; 
 
    max = (a > b) ? a : b; /* If a > b, max is a; otherwise, max is b */ 
 
    printf("Max value: %d\n", max); 
 
    return 0; 
} 

Array Subscript Operator

The array subscript operator [] is used to access elements within an array. This operator allows you to reference a specific element in an array by specifying its index, which represents the element’s position within the array. Array indices in C start from 0, meaning the first element of an array is accessed with index 0, the second element with index 1, and so on.

#include <stdio.h> 
 
int main() { 
    int numbers[5] = {10, 20, 30, 40, 50}; 
 
    printf("First element: %d\n", numbers[0]);  /* Output: 10 */ 
    printf("Second element: %d\n", numbers[1]); /* Output: 20 */ 
    printf("Third element: %d\n", numbers[2]);  /* Output: 30 */ 
    printf("Fourth element: %d\n", numbers[3]); /* Output: 40 */ 
    printf("Fifth element: %d\n", numbers[4]);  /* Output: 50 */ 
 
    numbers[2] = 60; /* Modify an element of the array */ 
    printf("Modified third element: %d\n", numbers[2]); /* Output: 60 */ 
 
    return 0; 
} 

Reference Operator &

The reference operator is used to obtain the memory address of a variable. When you apply the operator to a variable, it returns the address where the variable is stored in memory. This is particularly useful in the context of pointers, where you need to store the address of a variable.

#include <stdio.h> 
 
int main() { 
    int a = 10; 
    int *ptr; 
 
    ptr = &a; /* Using the reference operator to get the address of 'a' */ 
 
    printf("The address of variable 'a' is: %p\n", ptr); 
 
    return 0; 
} 

Dereference Operator *

The dereference operator is used to access the value stored at a particular memory address. When you apply the operator to a pointer, it returns the value located at the memory address the pointer holds. This allows you to work with the actual value stored in the address, rather than the address itself.

#include <stdio.h> 
 
int main() { 
    int a = 10; 
    int *ptr; 
    ptr = &a; 
 
    printf("The value at the address stored in 'ptr' is: %d\n", *ptr); 
 
    return 0; 
} 

Member Access Operator .

The member access operator is used to access members (attributes or methods) of a structure or union directly through a variable of that structure or union type. This operator is essential for working with the individual fields of a structure.

#include <stdio.h> 
 
struct Point { 
    int x; 
    int y; 
}; 
 
int main() { 
    struct Point p1; 
    p1.x = 10; 
    p1.y = 20; 
 
    printf("Point p1: (%d, %d)\n", p1.x, p1.y); 
 
    return 0; 
} 

Member Access Through Pointer Operator ->

The member access through pointer operator is used to access members of a structure or union through a pointer to that structure or union. This operator is especially useful when you have a pointer to a structure and you want to access its members.

#include <stdio.h> 
 
struct Point { 
    int x; 
    int y; 
}; 
 
int main() { 
    struct Point p1; 
    struct Point *ptr; 
    ptr = &p1; 
 
    ptr->x = 10; 
    ptr->y = 20; 
 
    printf("Point p1: (%d, %d)\n", ptr->x, ptr->y); 
 
    return 0; 
} 

The reference and dereference operators are fundamental for working with pointers, allowing you to retrieve memory addresses and access values stored at those addresses, respectively. The member operators are essential for accessing members of structures or unions, whether directly through variables or through pointers. These operators will be covered in more detail in the lessons on structures and pointers.

Exercises

  1. Arithmetic Operators: Write a program that takes two integers from the user and demonstrates the use of each arithmetic operator on these values.
  2. Logical Operators: Write a program to evaluate logical expressions and print the results. Take different combinations of boolean variables as inputs.
  3. Bitwise Operations: Write a program to perform various bitwise operations (AND, OR, XOR, NOT, left shift, right shift) on two given integers and print the results.
  4. Bitwise XOR Cipher: Implement a program that uses the XOR cipher to encrypt and decrypt a string using a given key. The key should be used to create a hash that serves as a seed for generating the XOR-key. The program should be able to handle strings of any length (or at least 1024) via standard input.
    • Read Input String and Key
      • Prompt the user to enter a string to encrypt or decrypt.
      • Prompt the user to enter a key for the encryption/decryption process.
    • Generate Hash from Key
      • Create a simple hash from the provided key to use as a seed for the random number generator. This hash ensures that the key generates a unique sequence of random numbers. Look up algorithms online, for example: djb2.
    • Encrypt/Decrypt the String
      • Use the generated seed to initialize the random number generator.
      • XOR each character of the input string with a random number generated from the seed to encrypt or decrypt the string.
    • Output the Result
      • Print the encrypted or decrypted string.

Solutions

Example solution to ’Bitwise XOR Cipher’:

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
 
unsigned long generate_hash(const char *key) { 
    unsigned long hash = 5381; 
    int c; 
    while ((c = *key++)) { 
        hash = ((hash << 5) + hash) + c; 
    } 
    return hash; 
} 
 
void xor_cipher(char *data, unsigned long seed, size_t length) { 
    srand(seed); 
    for (size_t i = 0; i < length; ++i) { 
        data[i] ^= rand(); 
    } 
} 
 
int main() { 
    char input[1024]; 
    char key[256]; 
 
    printf("Enter the string to encrypt/decrypt: "); 
    fgets(input, sizeof(input), stdin); 
    input[strcspn(input, "\n")] = '\0'; /* Remove the newline character */ 
 
    printf("Enter the key: "); 
    fgets(key, sizeof(key), stdin); 
    key[strcspn(key, "\n")] = '\0'; /* Remove the newline character */ 
 
    unsigned long seed = generate_hash(key); 
 
    xor_cipher(input, seed, strlen(input)); 
    printf("Output encrypted string: %s\n", input); 
 
    xor_cipher(input, seed, strlen(input)); 
    printf("Output decrypted string: %s\n", input); 
 
    return 0; 
} 

If you haven’t figured it out yet, this program can be further optimized by using a loop to read input of any length without specifying a fixed buffer size (e.g., 1024). In two lessons, you will learn how to implement this enhancement.

Next Lesson

In the next lesson, we will delve into the fundamental control structures in C programming, starting with the "if...else" statements. These conditional statements allow you to control the flow of your program based on specific conditions, enabling more dynamic and responsive code.

Next Lesson