1. Opening Question
Before we start this article, here's a question given by an interviewer, a senior C language expert, during a recent interview with the site’s admin:
int i = 255;
i <<= 24;
i >>= 24;
Questions:
- What is the final value of
i? - If
iis of typeuint, what is the final value ofi?
2. C# Bitwise Operations
C# bitwise operations are a powerful tool that can play an important role when handling binary data and bit-level operations. By using bitwise operators, we can perform bit-level operations on integers, such as bitwise AND, OR, XOR, and complement. Bitwise operations can be used for performance optimization, data compression, implementing bit masks and flags, etc. Understanding and mastering the basic principles and common application scenarios of C# bitwise operations will enable us to handle binary data more efficiently and, in some cases, improve code performance and readability. Through an in-depth understanding of C# bitwise operations, we can unleash greater creativity and flexibility in programming.
The content in this section is mainly referenced from the articles: Using Bitwise Operations (AND, OR, NOT & | ^) for Data Validation in C# and C# Bitwise Operators_c# bitwise operators-CSDN Blog.
To learn bitwise operations, you first need to understand what bitwise operations are. All content in a program is stored in computer memory in binary form (i.e., 0 or 1). Bitwise operations directly operate on each bit of the binary numbers in memory.
In C#, you can perform bitwise logical operations on integer operands. The meaning of bitwise logical operations is: take each bit of the operand in turn, perform a logical operation, and each bit's logical operation result becomes the corresponding bit of the result value. The bitwise logical operators supported by C# are shown in the table.
| Operator | Meaning | Operand Types | Result Type | Operands | Example |
|---|---|---|---|---|---|
| ~ | Bitwise NOT operation, bitwise complement | Integer, character | Integer | 1 | ~a |
| & | Bitwise AND operation, similar in effect to && logical operator | Same as above | Same | 2 | a & b |
| | | Bitwise OR operation, similar to || | Same as above | Same | 2 | a | b |
| ^ | Bitwise XOR operation | Same as above | Same | 2 | a ^ b |
| << | Bitwise left shift operation | Same as above | Same | 2 | a<<4 |
| >> | Bitwise right shift operation | Same as above | Same | 2 | a>>2 |
2.1. ~: Bitwise NOT Operation
The bitwise NOT operation is unary, taking only one operand. It performs a NOT operation on each bit of the operand's value: if a bit is 0, it becomes 1; if a bit is 1, it becomes 0.
For example, performing a bitwise NOT on the binary number 10010001 yields 01101110. In decimal: ~145 equals 110. Performing a bitwise NOT on binary 01010101 yields 10101010. In decimal: ~85 equals 176.
int a = 1001 0001; // decimal: 145
int b = ~a; // b = 0110 1110, i.e., decimal: 110
Let's look at a more complex example, from the article C# Bitwise Operators_c# bitwise operators-CSDN Blog:
int a = 13;
int b = -14;
Console.WriteLine(~a); // -14
Console.WriteLine(~b); // 13;
How does this work with binary? First, you need to remember some principles:
| Original Code* | One's Complement | Two's Complement** | Complement | |
|---|---|---|---|---|
| Positive | Sign bit + absolute value | Original code | Original code | Swap 0 and 1 |
| 13 | 0 1101 | 0 1101 | 0 1101 | 1 0010 |
| Negative | Sign bit + absolute value | Invert absolute | One's complement +1 | Swap 0 and 1 |
| -14 | 1 1110 | 1 0001 | 1 0010 | 0 1101 |
*: The length of the sign bit depends on the type definition. In C#, the sign bit for an int is 1 bit. **: In C#, numeric values are stored in two's complement.
The following shows how to convert between the original codes of the two:
int b = 1 1110; // The leading 1 indicates the sign bit
One's complement = 1 0001; // Sign bit remains unchanged
Two's complement = 1 0010; // One's complement +1
Invert two's complement = 0 1101; // Obtain the new one's complement, which is the result a (invert including the sign bit)
int a = 0 1101;
a's two's complement = 0 1101;
Invert two's complement = 1 0010; // This is now b's two's complement
Two's complement to one's complement = 1 0001; // Subtract 1
One's complement to original code = 1 1110; // This is the original code of result b
After multiple experiments, a pattern emerges:
~(+a) = -(a+1); (For a positive number, bitwise NOT: add 1 to the current number and make it negative) ~(-a) = (+a-1); (For a negative number, bitwise NOT: temporarily treat it as positive, subtract 1 to get the result)
2.2. &: Bitwise AND Operation
The bitwise AND operation performs an AND on each bit of the two operands. The rule for AND: 1 AND 1 equals 1; 1 AND 0 equals 0.
Example code:
int a = 13;
int b = 14;
int result = a & b; // 12
Converted to binary:
int a = 0000 1101;
int b = 0000 1110;
int result = 0000 1100;
The & operator compares the 0 and 1 at the same position in the binary representation. When the numbers at the same position are identical, it returns that number; otherwise, it returns 0. This is similar to the && operator, which returns True only if both booleans are the same, otherwise False. So we get the result result, which when converted to decimal is 12.
2.3. |: Bitwise OR Operation
The bitwise OR operation performs an OR on each bit of the two operands. The rule for OR: 1 OR 1 equals 1; 1 OR 0 equals 1.
Example code:
int a = 13;
int b = 14;
int result = a | b; // 15
Converted to binary:
int a = 0000 1101;
int b = 0000 1110;
int result = 0000 1111;
The judgment method is the same, but the result differs. The | operator checks the 0 and 1 at the same position: if at least one of them is 1, it returns 1. This is very similar to the || operator, which returns True if at least one operand is True. Converting the result to decimal gives 15.
2.4. ^: Bitwise XOR Operation
The bitwise XOR operation performs an XOR on each bit of the two operands. The rule for XOR: 1 XOR 1 equals 0; 1 XOR 0 equals 1; 0 XOR 0 equals 0. That is: same yields 0, different yields 1.
Example code:
int a = 13;
int b = 14;
int result = a ^ b; // 3
Converted to binary:
int a = 0000 1101;
int b = 0000 1110;
int result = 0000 0011;
It can be seen that the ^ operator checks the digits at the same position. If both digits are the same (both 0 or both 1), it returns 0; if one is 1, it returns 1. In contrast, the | operator returns 1 as long as at least one digit is 1. Hence the name "exclusive OR" (returns OR only when they differ).
2.5. <<: Bitwise Left Shift Operation
The left shift operation shifts all bits of a number to the left by a specified number of positions, filling the vacated bits on the right with 0. For example, take an 8-bit byte variable byte a = 0x65 (binary 01100101). Shifting it left by 3 positions: a << 3 yields 0x27 (binary 00101000).
byte a = 0110 0101;
a <<= 3; // 0010 1000
Moves the first operand to the left by the number of bits specified by the second operand; vacated positions are filled with 0.
Left shift is equivalent to multiplication: shifting left by one bit equals multiplying by 2; shifting left by two bits equals multiplying by 4; shifting left by three bits equals multiplying by 8.
x<<1 = x*2
x<<2 = x*4
x<<3 = x*8
x<<4 = x*16
2.6. >>: Bitwise Right Shift Operation
The right shift operation shifts all bits of a number to the right by a specified number of positions, filling the vacated bits on the left with 0. For example, take an 8-bit byte variable byte a = 0x65 (binary 01100101). Shifting it right by 3 positions: a >> 3 yields 0x0c (binary 00001100).
byte a = 0110 0101;
a >>= 3; // 0000 1100
Moves the first operand to the right by the number of bits specified by the second operand; vacated positions are filled with 0.
Right shift is equivalent to integer division: shifting right by one bit equals dividing by 2; shifting right by two bits equals dividing by 4; shifting right by three bits equals dividing by 8.
x>>1 = x/2
x>>2 = x/4
x>>3 = x/8
x>>4 = x/16
3. Summary and Answers to the Questions
Refer to the Microsoft documentation Bitwise and shift operators for two important notes:
- Bitwise and shift operations never cause overflow and produce the same results in checked and unchecked contexts.
- Shift operators are only defined for
int,uint,long, andulongtypes, so the result of an operation always contains at least 32 bits. If the left operand is another integer type (sbyte,byte,short,ushort, orchar), its value is converted toint.
Here are the answers to the questions at the beginning of the article, concluding the article:
int i = 255; // 00000000 00000000 00000000 11111111
i <<= 24; // 11111111 00000000 00000000 00000000
i >>= 24; // 11111111 11111111 11111111 11111111;
Change the data type of i to uint:
uint i = 255; // 00000000 00000000 00000000 11111111
i <<= 24; // 11111111 00000000 00000000 00000000
i >>= 24; // 00000000 00000000 00000000 11111111;
The reason for the different results after shifting int and uint:
- For signed integers, the right‑shift operation also shifts the sign bit (the most significant bit) to the right. This means that if the most significant bit of the original value is 1, the sign bit is preserved after the shift, i.e., 1s are filled in. This kind of right shift is called arithmetic right shift.
- For unsigned integers, the right‑shift operation does not preserve the sign bit; instead, it shifts the 0 from the highest position as well. This kind of right shift is called logical right shift.
If there are any errors in this article, please feel free to point them out. References used in this article: