1. 最初の質問
この記事を始める前に、先日サイト運営者が面接で、ある面接官(C言語の達人)から出された問題を紹介します:
int i = 255;
i <<= 24;
i >>= 24;
質問:
- 最終的に
iはいくつになるか? iがuint型の場合、最終的なiの結果はいくつになるか?
2. C# のビット演算
C# のビット演算は、バイナリデータやビット操作を扱う際に強力なツールです。ビット演算子を使用することで、整数に対してビット単位の操作(ビットごとの AND、OR、XOR、NOT など)を行うことができます。ビット演算は、パフォーマンスの最適化、データ圧縮、ビットマスクやビットフラグの実装などに利用できます。C# のビット演算の基本原則とよくある応用シナリオを理解することで、バイナリデータをより効率的に処理し、場合によってはコードのパフォーマンスと可読性を向上させることができます。C# のビット演算を深く理解することで、プログラミングにおいてより創造性と柔軟性を発揮できるでしょう。
このセクションの内容は主に以下の記事を参考にしています:C# 中使用位运算(与、或、非 & | ^)进行数据校验 および c# 位运算符_c#位运算符-CSDN博客。
ビット演算を学ぶには、まずビット演算とは何かを明確にする必要があります。プログラム内のすべての内容は、コンピュータのメモリ上でバイナリ形式(0 または 1)で保存されます。ビット演算は、メモリ上のバイナリ数の各ビットに対して直接演算操作を行うことです。
C# では、整数型の演算オブジェクトに対してビット単位の論理演算を行うことができます。ビット単位の論理演算の意味は、演算対象の各ビットを順に取り出し、論理演算を行い、各ビットの論理演算結果が結果値の各ビットになるということです。C# がサポートするビット論理演算子を表に示します。
| 演算子 | 意味 | 演算対象の型 | 演算結果の型 | オペランド数 | 例 |
|---|---|---|---|---|---|
| ~ | ビット論理 NOT 演算、各ビットを反転 | 整数型、文字型 | 整数型 | 1 | ~a |
| & | ビット論理 AND 演算、&& 論理演算子と一致する部分がある | 同上 | 同上 | 2 | a & b |
| | | ビット論理 OR 演算、|| と類似した部分がある | 同上 | 同上 | 2 | a | b |
| ^ | ビット論理 XOR 演算 | 同上 | 同上 | 2 | a ^ b |
| << | ビット 左シフト 演算 | 同上 | 同上 | 2 | a<<4 |
| >> | ビット 右シフト 演算 | 同上 | 同上 | 2 | a>>2 |
2.1. ~:ビット論理 NOT 演算
ビット論理 NOT 演算は単項演算子で、オペランドは 1 つだけです。ビット論理 NOT 演算は、演算対象の値をビットごとに NOT 演算します。つまり、あるビットが 0 なら 1 に、1 なら 0 に変換します。
例えば、バイナリ 10010001 に対してビット論理 NOT 演算を行うと、結果は 01101110 になります。10 進数で表すと:
~145 は 110 に等しい。バイナリ 01010101 に対してビット論理 NOT 演算を行うと、結果は 10101010 になります。10 進数で表すと ~85 は 176 に等しい。
int a = 1001 0001; // 10 進数:145
int b = ~a; // b = 0110 1110、すなわち 10 進数:110
より複雑な例は、この記事 c# 位运算符_c#位运算符-CSDN博客 を参照してください:
int a = 13;
int b = -14;
Console.WriteLine(~a); // -14
Console.WriteLine(~b); // 13;
これがどのようにバイナリで動作するのかを理解するには、まずいくつかの原則を覚えておく必要があります:
| 原码* | 反码 | 补码** | 反転(~) | |
|---|---|---|---|---|
| 正数 | 符号ビット + 絶対値 | 原码と同じ | 原码と同じ | 0 と 1 を入れ替え |
| 13 | 0 1101 | 0 1101 | 0 1101 | 1 0010 |
| 负数 | 符号ビット + 絶対値 | 絶対値を反転 | 反码 + 1 | 0 と 1 を入れ替え |
| -14 | 1 1110 | 1 0001 | 1 0010 | 0 1101 |
*:符号ビットの長さは型定義に依存します。C# では int の符号ビットは 1 ビットです。 **:C# では数値は補数 (2の補数) で格納されます。
以下に、両者の原码間の変換を示します。
int b = 1 1110; // 先頭の 1 は符号ビット
反码 = 1 0001; // 符号ビットは不变
补码 = 1 0010; // 反码に 1 を加算
补码の反転 = 0 1101; // 新しい反码が結果 a になります(符号ビットも含めて反転)
int a = 0 1101;
a の补码 = 0 1101;
补码の反転 = 1 0010; // これが b の补码になります
补码から反码へ = 1 0001; // つまり 1 を減算
反码から原码へ = 1 1110; // これが結果 b の原码です
いくつか実験を繰り返した結果、次のような規則が得られました:
~(+a)= -(a+1); (正数のビット反転は、現在の数値に 1 を加えて負の符号を付けるだけ) ~(-a)= (+a-1); (負数のビット反転は、現在の数値を正数とみなして 1 を減算した結果を得る)
2.2. &:ビット論理 AND 演算
ビット論理 AND 演算は、2 つのオペランドをビットごとに AND 演算します。AND 演算の規則:1 と 1 は 1、1 と 0 は 0。
サンプルコード:
int a = 13;
int b = 14;
int result = a & b; // 12
バイナリに変換:
int a = 0000 1101;
int b = 0000 1110;
int result = 0000 1100;
& 演算子は、同じ位置の 0 と 1 を比較し、同じ位置の数字が同じならその数字を返し、そうでなければ 0 を返します。これは、&& 演算子が 2 つの bool が両方とも true なら true、そうでなければ false を返すのと似ています。これにより result が得られ、10 進数に変換すると 12 になります。
2.3. |:ビット論理 OR 演算
ビット論理 OR 演算は、2 つのオペランドをビットごとに OR 演算します。OR 演算の規則:1 OR 1 は 1、1 OR 0 は 1、0 OR 0 は 0。
サンプルコード:
int a = 13;
int b = 14;
int result = a | b; // 15
バイナリに変換:
int a = 0000 1101;
int b = 0000 1110;
int result = 0000 1111;
判定方法は同じですが、返される結果が異なります。| 演算子は、2 つのバイナリの同じ位置の 0 と 1 を判定し、どちらか一方の位置の数字が 1 であれば 1 を返します。これは || 演算子が 1 つでも true なら true を返すのと非常によく似ています。得られた結果を 10 進数に変換すると 15 になります。
2.4. ^:ビット論理 XOR 演算
ビット論理 XOR 演算は、2 つのオペランドをビットごとに XOR 演算します。XOR 演算の規則:1 XOR 1 は 0、1 XOR 0 は 1、0 XOR 0 は 0。つまり、同じなら 0、異なれば 1。
サンプルコード:
int a = 13;
int b = 14;
int result = a ^ b; // 3
バイナリに変換:
int a = 0000 1101;
int b = 0000 1110;
int result = 0000 0011;
これからわかるように、^ は同じ位置の数字を判定し、2 つの数字が同じなら(0 でも 1 でも)0 を返し、いずれかが 1 なら 1 を返します。一方、| はどちらか一方の位置が 1 なら 1 を返すので、「排他的論理和(XOR)」と呼ばれます(異なる場合に OR を返す)。
2.5. <<:ビット左シフト演算
ビット左シフト演算は、数値全体をビット単位で指定された桁数だけ左にシフトし、空いた部分には 0 を入れます。例えば、8 ビットの byte 型変数 byte a=0x65(バイナリ 01100101)を 3 ビット左シフトすると、a<<3 の結果は 0x27(バイナリ 00101000)になります。
byte a = 0110 0101;
a <<= 3;//0010 1000
最初のオペランドを、2 番目のオペランドで指定されたビット数だけ左にシフトし、空いた位置には 0 を補います。左シフトは乗算に相当します。1ビット左シフトは 2 倍、2ビットは 4 倍、3ビットは 8 倍になります。
x<<1= x*2
x<<2= x*4
x<<3= x*8
x<<4= x*16
2.6. >>:ビット右シフト演算
ビット右シフト演算は、数値全体をビット単位で指定された桁数だけ右にシフトし、空いた部分には 0 を入れます。例えば、8 ビットの byte 型変数 byte a=0x65(バイナリ 01100101)を 3 ビット右シフトすると、a>>3 の結果は 0x0c(バイナリ 00001100)になります。
byte a = 0110 0101;
a >>= 3;//0000 1100
最初のオペランドを、2 番目のオペランドで指定されたビット数だけ右にシフトし、空いた位置には 0 を補います。右シフトは除算に相当します。1ビット右シフトは 2 で割る、2ビットは 4 で割る、3ビットは 8 で割るになります。
x>>1= x/2
x>>2= x/4
x>>3= x/8
x>>4= x/16
3. まとめと問題の答え
Microsoft のドキュメント ビット演算子とシフト演算子 の 2 つの注意点を参照:
- ビット演算とシフト演算はオーバーフローを引き起こすことはなく、checked および unchecked コンテキストでも同じ結果を生成します。
- シフト演算子は
int、uint、long、ulong型に対してのみ定義されているため、演算結果は常に少なくとも 32 ビットになります。左オペランドが他の整数型(sbyte、byte、short、ushort、char)の場合は、その値がint型に変換されます。
ここで、記事冒頭の問題の答えとその解説を示して記事を締めくくります:
int i = 255; // 00000000 00000000 00000000 11111111
i <<= 24; // 11111111 00000000 00000000 00000000
i >>= 24; // 11111111 11111111 11111111 11111111;
i のデータ型を uint に変更:
uint i = 255; // 00000000 00000000 00000000 11111111
i <<= 24; // 11111111 00000000 00000000 00000000
i >>= 24; // 00000000 00000000 00000000 11111111;
int と uint でシフト後の結果が異なる理由:
- 符号付き整数の右シフト操作では、最上位ビット(符号ビット)も一緒に右シフトされます。つまり、元の数値の最上位ビットが 1 の場合、右シフト後も符号ビットが保持され、1 が埋められます。このような右シフトは算術右シフトと呼ばれます。
- 符号なし整数の右シフト操作では、符号ビットは保持されず、最上位ビットの 0 も一緒に右シフトされます。このような右シフトは論理右シフトと呼ばれます。
この記事に間違いがありましたら、ご指摘ください。文中の参考資料: