C#のビットシフト演算子を基礎から解説|<<・>>の使い方、計算例、よくある注意点まで完全理解
はじめに
C#のビットシフト演算子は、整数のビット列を左または右にずらすための演算子です。代表的なものに、左シフト演算子<<、右シフト演算子>>、符号なし右シフト演算子>>>があります。
ビットシフトは、普段の業務アプリケーションでは毎日使うものではないかもしれません。しかし、ビットフラグ、バイナリデータ、通信データ、画像処理、ゲーム開発、組み込み系、パフォーマンスを意識した低レベル処理などでは非常に重要です。
この記事では、C#のビットシフト演算子について、2進数の基礎から、<<・>>・>>>の違い、具体的な計算例、型変換の注意点、実務での使い方まで順番に解説します。
1. C#のビットシフト演算子とは
1-1. ビットシフトの基本概念:2進数の桁を左右にずらす処理
ビットシフトとは、数値を2進数で見たときに、ビットの並びを左または右にずらす処理です。
たとえば、10進数の4は2進数では次のように表せます。
C#4 = 0b0100
これを左に1ビットシフトすると、次のようになります。
C#0b0100 << 1 = 0b1000
0b1000は10進数で8です。つまり、4 << 1の結果は8になります。
逆に、8を右に1ビットシフトすると次のようになります。
C#0b1000 >> 1 = 0b0100
0b0100は10進数で4です。
このように、ビットシフトは「2進数の桁をずらす操作」と考えると理解しやすくなります。
1-2. C#で使えるシフト演算子の種類:<<・>>・>>>の違い
C#で主に使うシフト演算子は次の3つです。
| 演算子 | 名前 | 役割 |
|---|---|---|
<< | 左シフト演算子 | ビットを左にずらす |
>> | 右シフト演算子 | ビットを右にずらす |
>>> | 符号なし右シフト演算子 | 符号に関係なく、左側を0で埋めながら右にずらす |
<<は左にずらす演算子です。左に1ビットずらすと、多くの場合、値は2倍になります。
>>は右にずらす演算子です。正の整数では、右に1ビットずらすと、多くの場合、値は2で割ったような結果になります。
>>>は符号なし右シフト演算子です。>>と似ていますが、負の数を扱うときの挙動が異なります。>>>は常に左側の空いたビットを0で埋めます。MicrosoftのC#リファレンスでも、C#のシフト演算子として<<、>>、>>>が説明されています。Microsoft Learn
1-3. ビット演算子とシフト演算子の関係
ビットシフト演算子は、広い意味ではビット演算子の一種です。
C#には、シフト演算子以外にも次のようなビット演算子があります。
| 演算子 | 名前 | 役割 |
|---|---|---|
& | AND | 両方のビットが1なら1 |
| ` | ` | OR |
^ | XOR | 片方だけが1なら1 |
~ | NOT | ビットを反転する |
<< | 左シフト | ビットを左にずらす |
>> | 右シフト | ビットを右にずらす |
>>> | 符号なし右シフト | 0で埋めながら右にずらす |
ビット演算は「個々のビット」を直接操作するための仕組みです。その中でビットシフトは、ビットの位置を移動させる役割を持っています。
1-4. ビットシフトが使われる主な場面
C#のビットシフトは、次のような場面で使われます。
C#int value = 1 << 3; // 8
主な用途は次のとおりです。
| 用途 | 例 |
|---|---|
| 2の累乗の計算 | 1 << 4で16 |
| ビットフラグ管理 | 権限や状態を1つの整数で管理 |
| マスク処理 | 特定のビットだけ取り出す |
| RGBカラー処理 | R・G・Bの値を1つの整数にまとめる |
| バイナリ解析 | 通信データやファイルフォーマットを読み解く |
| ゲーム開発 | 状態管理、当たり判定、最適化など |
| 組み込み系 | レジスタ操作、ハードウェア制御など |
通常のWebアプリや業務システムでは頻繁に使わないかもしれませんが、知っておくとC#で扱える処理の幅が大きく広がります。
2. ビットシフトを理解するための2進数の基礎
2-1. 10進数と2進数の違い
普段私たちが使っている数値は10進数です。
10進数では、各桁が10倍ずつ重みを持ちます。
123 = 1×100 + 2×10 + 3×1
一方、コンピューター内部では数値は基本的に2進数で扱われます。
2進数では、各桁が2倍ずつ重みを持ちます。
0b1011 = 1×8 + 0×4 + 1×2 + 1×1
= 11
つまり、2進数の1011は10進数で11です。
ビットシフトを理解するには、この「2進数では桁の重みが2倍ずつ変わる」という考え方が重要です。
2-2. int型の値を2進数で考える方法
C#のint型は、通常32ビットの符号付き整数です。
たとえば、int型の5は、下位ビットだけを見ると次のように表せます。
5 = 0b0101
実際のintは32ビットなので、正確には次のように多くの0が前に付きます。
00000000 00000000 00000000 00000101
ビットシフトを考えるときは、この32個のビットが左右に動くと考えます。
C#int x = 5;
int y = x << 1;
Console.WriteLine(y); // 10
5を左に1ビットシフトすると、2進数では次のようになります。
00000101
↓ 左に1ビット
00001010
0b1010は10進数で10です。
2-3. ビット位置と桁の重み
2進数では、右端のビットから順に次のような重みを持ちます。
| ビット位置 | 重み | 値 |
|---|---|---|
| 0 | 2^0 | 1 |
| 1 | 2^1 | 2 |
| 2 | 2^2 | 4 |
| 3 | 2^3 | 8 |
| 4 | 2^4 | 16 |
| 5 | 2^5 | 32 |
| 6 | 2^6 | 64 |
| 7 | 2^7 | 128 |
たとえば、次の2進数を考えます。
0b1000
これはビット位置3が1なので、値は2^3 = 8です。
0b1000 = 8
そのため、1 << 3は8になります。
C#Console.WriteLine(1 << 3); // 8
2-4. C#で2進数リテラルを使う方法
C#では、0bまたは0Bを使うと2進数リテラルを書けます。
C#int a = 0b1010;
Console.WriteLine(a); // 10
桁が長くなる場合は、区切り文字としてアンダースコア_を使うと読みやすくなります。
C#int flags = 0b_0000_1010;
Console.WriteLine(flags); // 10
ビット演算やビットシフトを学ぶときは、10進数だけでなく2進数リテラルを使うと、どのビットが変化したか確認しやすくなります。
3. 左シフト演算子<<の使い方
3-1. <<の基本構文
左シフト演算子<<の基本構文は次のとおりです。
C#値 << シフト数
たとえば、3を左に1ビットシフトする場合は次のように書きます。
C#int result = 3 << 1;
Console.WriteLine(result); // 6
3は2進数で0b0011です。これを左に1ビットずらすと0b0110になり、10進数では6になります。
0011
↓ 左に1ビット
0110
3-2. 左シフトすると値がどう変化するか
左シフトでは、ビットが左へ移動し、右側の空いたビットには0が入ります。
0001 << 1 = 0010
0001 << 2 = 0100
0001 << 3 = 1000
10進数で見ると、次のようになります。
C#Console.WriteLine(1 << 1); // 2
Console.WriteLine(1 << 2); // 4
Console.WriteLine(1 << 3); // 8
Console.WriteLine(1 << 4); // 16
左に1ビットずらすと2倍、2ビットずらすと4倍、3ビットずらすと8倍になります。
ただし、これはあくまで「ビットがあふれない範囲」での考え方です。上位ビットが範囲外に出る場合は、そのビットは捨てられます。C#の左シフトでは、結果の型の範囲外に出た上位ビットは破棄され、下位の空いたビット位置は0になります。Microsoft Learn
3-3. 1 << nで2のn乗を表す仕組み
1 << nは、2のn乗を表すときによく使われます。
C#Console.WriteLine(1 << 0); // 1
Console.WriteLine(1 << 1); // 2
Console.WriteLine(1 << 2); // 4
Console.WriteLine(1 << 3); // 8
Console.WriteLine(1 << 4); // 16
なぜなら、2進数の1を左にnビットずらすと、2^nの位置にビットが立つからです。
1 << 0 = 0001 = 1
1 << 1 = 0010 = 2
1 << 2 = 0100 = 4
1 << 3 = 1000 = 8
ビットフラグを定義するときにも、この書き方はよく使われます。
C#const int Read = 1 << 0; // 1
const int Write = 1 << 1; // 2
const int Execute = 1 << 2; // 4
このように定義しておくと、各フラグが重ならず、1つの整数の中で複数の状態を管理できます。
3-4. 左シフトの計算例
いくつか具体例を見てみましょう。
C#int a = 2 << 1;
Console.WriteLine(a); // 4
2は2進数で0010です。
0010 << 1 = 0100
0100は10進数で4です。
次に、3 << 2を見てみます。
C#int b = 3 << 2;
Console.WriteLine(b); // 12
3は2進数で0011です。
0011 << 2 = 1100
1100は10進数で12です。
次のように、左シフトは「2のシフト数乗を掛ける」ような結果になります。
C#int x = 7;
Console.WriteLine(x << 1); // 14
Console.WriteLine(x << 2); // 28
Console.WriteLine(x << 3); // 56
ただし、繰り返しになりますが、ビットがあふれない場合に限ってこのように考えるのが安全です。
3-5. 左シフトであふれた上位ビットの扱い
int型は32ビットです。そのため、左シフトによって32ビットの範囲外に出たビットは破棄されます。
C#int x = 1 << 30;
Console.WriteLine(x); // 1073741824
int y = x << 1;
Console.WriteLine(y); // -2147483648
1 << 30は、intの符号ビットの1つ手前にビットが立った状態です。それをさらに左に1ビットずらすと、最上位ビット、つまり符号ビットが1になります。
intは符号付き整数なので、最上位ビットが1になると負の数として解釈されます。
C#int z = 1 << 31;
Console.WriteLine(z); // -2147483648
このように、左シフトは単純な掛け算とは異なり、ビット表現そのものを変化させます。特に符号付き整数で上位ビットを操作すると、結果が負の数になることがあります。
4. 右シフト演算子>>の使い方
4-1. >>の基本構文
右シフト演算子>>の基本構文は次のとおりです。
C#値 >> シフト数
たとえば、8を右に1ビットシフトする場合は次のように書きます。
C#int result = 8 >> 1;
Console.WriteLine(result); // 4
8は2進数で1000です。
1000 >> 1 = 0100
0100は10進数で4です。
4-2. 右シフトすると値がどう変化するか
右シフトでは、ビットが右へ移動し、右端からあふれた下位ビットは捨てられます。
1000 >> 1 = 0100
1000 >> 2 = 0010
1000 >> 3 = 0001
正の整数の場合、右に1ビットずらすと、おおむね2で割った結果になります。
C#Console.WriteLine(16 >> 1); // 8
Console.WriteLine(16 >> 2); // 4
Console.WriteLine(16 >> 3); // 2
Console.WriteLine(16 >> 4); // 1
ただし、奇数の場合は小数部分が切り捨てられたような結果になります。
C#Console.WriteLine(7 >> 1); // 3
7 / 2は整数除算では3なので、この場合は同じ結果に見えます。
4-3. 正の整数を右シフトする計算例
正の整数では、>>は比較的直感的です。
C#int x = 20;
Console.WriteLine(x >> 1); // 10
Console.WriteLine(x >> 2); // 5
20は2進数で10100です。
10100 >> 1 = 01010 = 10
10100 >> 2 = 00101 = 5
もう一つ例を見てみます。
C#int y = 9;
Console.WriteLine(y >> 1); // 4
Console.WriteLine(y >> 2); // 2
9は2進数で1001です。
1001 >> 1 = 0100 = 4
1001 >> 2 = 0010 = 2
右端から落ちたビットは捨てられるため、割り算の余りに相当する情報は失われます。
4-4. 負の整数を右シフトした場合の注意点
負の整数を>>で右シフトするときは注意が必要です。
C#int x = -8;
int y = x >> 1;
Console.WriteLine(y); // -4
一見すると、-8 / 2と同じように見えます。
しかし、内部では単純に0で埋めているわけではありません。intやlongのような符号付き整数に対して>>を使うと、左側の空いたビットには符号ビットと同じ値が入ります。
負の数は最上位ビットが1なので、右シフト後も左側が1で埋められます。
11111000 >> 1 = 11111100
これは、負の値を負のまま保つための動きです。C#の>>は、左オペランドがintまたはlongの場合は算術右シフトになり、符号ビットの値が空いた上位ビットに反映されます。Microsoft Learn
4-5. 算術右シフトと符号ビットの関係
算術右シフトとは、符号を維持するように右へシフトする方法です。
int型では、最上位ビットが符号ビットとして扱われます。
| 符号ビット | 意味 |
|---|---|
| 0 | 非負の数 |
| 1 | 負の数 |
そのため、負のintを>>で右シフトすると、左側に1が補われます。
C#int x = -16;
Console.WriteLine(x >> 1); // -8
Console.WriteLine(x >> 2); // -4
Console.WriteLine(x >> 3); // -2
これは「負の数を右にシフトしても、符号を維持する」ための挙動です。
一方、uintやulongのような符号なし整数では、符号ビットという考え方がないため、>>でも左側は0で埋められます。
5. 符号なし右シフト演算子>>>の使い方
5-1. >>>とは何か
>>>は、符号なし右シフト演算子です。
通常の>>と同じく右にビットをずらしますが、左側の空いたビットを常に0で埋める点が特徴です。
C#int x = -8;
int y = x >>> 2;
Console.WriteLine(y); // 1073741822
>>では負の数の符号を維持するために左側が1で埋められますが、>>>では左側が0で埋められます。
C#の公式リファレンスでも、>>>は常に論理シフトを実行し、左側の空いたビット位置を常に0に設定すると説明されています。Microsoft Learn
5-2. >>と>>>の違い
>>と>>>の違いは、特に負の数で明確に現れます。
C#int x = -8;
Console.WriteLine(x >> 2); // -2
Console.WriteLine(x >>> 2); // 1073741822
>>の場合、負の数では左側が1で埋められます。
11111111 11111111 11111111 11111000
>> 2
11111111 11111111 11111111 11111110
この結果は-2です。
一方、>>>の場合、左側は0で埋められます。
11111111 11111111 11111111 11111000
>>> 2
00111111 11111111 11111111 11111110
この結果は正の大きな数になります。
5-3. 負の値に>>>を使ったときの挙動
負の値に>>>を使うと、符号ビットも通常のビットとして右に移動します。そして、左側には0が入ります。
C#int x = -1;
Console.WriteLine(x >> 1); // -1
Console.WriteLine(x >>> 1); // 2147483647
-1は、intのビット表現ではすべてのビットが1です。
11111111 11111111 11111111 11111111
これを>> 1すると、左側が1で補われるため、結果はまだすべて1に近い状態で、値は-1です。
一方、>>> 1では左側が0になるため、次のようになります。
01111111 11111111 11111111 11111111
これはint.MaxValue、つまり2147483647です。
5-4. >>>を使うべきケース
>>>は、符号付き整数に対して「符号を維持せず、純粋にビット列として右にずらしたい」ときに使います。
たとえば、次のようなケースです。
C#int value = -123456;
int shifted = value >>> 8;
主な用途は次のとおりです。
| ケース | 説明 |
|---|---|
| 符号付き整数をビット列として扱う | 数値の正負ではなくビットパターンを処理したい |
| ハッシュ計算 | 上位ビットを混ぜる処理で使うことがある |
| バイナリ解析 | 符号に影響されずビットを抽出したい |
| 他言語からの移植 | Javaなどの>>>を使うコードを移植する場合 |
従来は、符号付き整数をuintにキャストしてから右シフトし、必要に応じて戻すようなコードを書くことがありました。
C#int x = -8;
int y = (int)((uint)x >> 2);
しかし、>>>を使えば意図をより直接的に表現できます。
C#int x = -8;
int y = x >>> 2;
5-5. 対応するC#バージョンの注意点
>>>はC# 11で導入された演算子です。C# 10以前の言語バージョンでは使用できません。
そのため、古いプロジェクトで>>>を書いた場合、コンパイルエラーになることがあります。>>>を使いたい場合は、プロジェクトの言語バージョンがC# 11以降になっているか確認しましょう。MicrosoftのC# 11機能仕様でも、符号なし右シフト演算子がC# 11の機能として説明されています。Microsoft Learn
プロジェクトファイルで言語バージョンを指定する場合は、たとえば次のように書きます。
XML<PropertyGroup>
<LangVersion>11.0</LangVersion>
</PropertyGroup>
ただし、通常は使用している.NET SDKに応じて適切なC#バージョンが選択されます。特別な理由がなければ、無理に固定せず、プロジェクトの環境に合わせて確認するとよいでしょう。
6. C#のビットシフト演算子の計算例
6-1. 1を左シフトする例
1を左シフトすると、2の累乗を作れます。
C#Console.WriteLine(1 << 0); // 1
Console.WriteLine(1 << 1); // 2
Console.WriteLine(1 << 2); // 4
Console.WriteLine(1 << 3); // 8
Console.WriteLine(1 << 4); // 16
Console.WriteLine(1 << 5); // 32
ビットで見ると、1の位置が左へ移動しています。
000001 = 1
000010 = 2
000100 = 4
001000 = 8
010000 = 16
100000 = 32
この性質は、フラグ定義でよく使われます。
C#const int FlagA = 1 << 0; // 1
const int FlagB = 1 << 1; // 2
const int FlagC = 1 << 2; // 4
const int FlagD = 1 << 3; // 8
6-2. 8を右シフトする例
次に、8を右シフトしてみます。
C#Console.WriteLine(8 >> 0); // 8
Console.WriteLine(8 >> 1); // 4
Console.WriteLine(8 >> 2); // 2
Console.WriteLine(8 >> 3); // 1
2進数では次のように変化します。
1000 >> 0 = 1000 = 8
1000 >> 1 = 0100 = 4
1000 >> 2 = 0010 = 2
1000 >> 3 = 0001 = 1
正の整数では、右シフトは2の累乗で割る操作に近い結果になります。
6-3. int型でのシフト演算例
int型で左シフトと右シフトを使う例です。
C#int x = 12;
Console.WriteLine(x << 1); // 24
Console.WriteLine(x << 2); // 48
Console.WriteLine(x >> 1); // 6
Console.WriteLine(x >> 2); // 3
12は2進数で1100です。
1100 << 1 = 11000 = 24
1100 << 2 = 110000 = 48
1100 >> 1 = 0110 = 6
1100 >> 2 = 0011 = 3
負のintでは、右シフト時の符号維持に注意します。
C#int x = -12;
Console.WriteLine(x >> 1); // -6
Console.WriteLine(x >>> 1); // 2147483642
6-4. uint型でのシフト演算例
uintは符号なし32ビット整数です。負の数を持たないため、右シフトでは左側が0で埋められます。
C#uint x = 0b_1000_0000_0000_0000_0000_0000_0000_0000;
uint y = x >> 1;
Console.WriteLine(y);
Console.WriteLine(Convert.ToString(y, 2).PadLeft(32, '0'));
出力イメージは次のようになります。
1073741824
01000000000000000000000000000000
uintでは符号ビットがないため、最上位ビットが1でも負の数にはなりません。
C#uint value = 1u << 31;
Console.WriteLine(value); // 2147483648
1uのようにuを付けると、uintリテラルとして扱えます。
6-5. long型・ulong型でのシフト演算例
longとulongは64ビット整数です。
C#long x = 1L << 40;
Console.WriteLine(x); // 1099511627776
1Lはlong型のリテラルです。1 << 40と書くと、左側がintとして扱われるため、意図しない結果になる可能性があります。
C#Console.WriteLine(1L << 40); // 正しく64ビットでシフト
ulongの場合は、次のように書けます。
C#ulong y = 1UL << 63;
Console.WriteLine(y); // 9223372036854775808
ulongは符号なし64ビット整数なので、最上位ビットが1でも負の数にはなりません。
6-6. 2進数表示で結果を確認するサンプルコード
ビットシフトの理解には、結果を2進数で表示すると効果的です。
C#static string ToBinary(int value)
{
return Convert.ToString(value, 2).PadLeft(32, '0');
}
int x = 8;
Console.WriteLine(ToBinary(x));
Console.WriteLine(ToBinary(x << 1));
Console.WriteLine(ToBinary(x >> 1));
出力例は次のようになります。
00000000000000000000000000001000
00000000000000000000000000010000
00000000000000000000000000000100
負の数も見てみましょう。
C#int x = -8;
Console.WriteLine(ToBinary(x));
Console.WriteLine(ToBinary(x >> 2));
Console.WriteLine(ToBinary(x >>> 2));
>>と>>>の違いを、ビット列で確認できます。
7. シフト演算で注意すべき型の扱い
7-1. byte・short・charはintに変換される
C#では、byte、short、charなどにシフト演算を使うと、基本的にintに変換されてから演算されます。
C#byte x = 1;
var y = x << 3;
Console.WriteLine(y); // 8
Console.WriteLine(y.GetType()); // System.Int32
byteをシフトしたからといって、結果もbyteになるわけではありません。
MicrosoftのC#リファレンスでも、sbyte、byte、short、ushort、charなどの値はint型に変換され、演算結果もintになることが説明されています。Microsoft Learn
7-2. シフト演算の結果の型
シフト演算の結果の型は、左側のオペランドの型に大きく依存します。
C#int a = 1;
long b = 1L;
uint c = 1u;
ulong d = 1UL;
Console.WriteLine((a << 1).GetType()); // System.Int32
Console.WriteLine((b << 1).GetType()); // System.Int64
Console.WriteLine((c << 1).GetType()); // System.UInt32
Console.WriteLine((d << 1).GetType()); // System.UInt64
一方、byteの場合はintになります。
C#byte x = 10;
var y = x << 1;
Console.WriteLine(y.GetType()); // System.Int32
そのため、結果をbyteに戻したい場合は明示的なキャストが必要です。
C#byte x = 10;
byte y = (byte)(x << 1);
Console.WriteLine(y); // 20
ただし、キャストによって上位ビットが失われる可能性があるため注意しましょう。
7-3. 複合代入演算子<<=・>>=の使い方
シフト演算には複合代入演算子もあります。
C#int x = 2;
x <<= 3;
Console.WriteLine(x); // 16
x <<= 3は、基本的には次の式と同じ意味です。
C#x = x << 3;
右シフトも同様です。
C#int y = 16;
y >>= 2;
Console.WriteLine(y); // 4
>>>にも複合代入があります。
C#int z = -8;
z >>>= 2;
Console.WriteLine(z); // 1073741822
ただし、複合代入では暗黙的な変換が絡むため、通常の代入とは少し違う挙動に見えることがあります。
C#byte x = 0b_1111_0001;
x <<= 8;
Console.WriteLine(x); // 0
この例では、演算自体はintで行われますが、最終的にbyteへ戻されるため、下位8ビットだけが残ります。
7-4. 符号付き整数と符号なし整数の違い
ビットシフトでは、符号付き整数と符号なし整数の違いが重要です。
| 型 | 符号 | 例 |
|---|---|---|
int | あり | 負の数を表せる |
uint | なし | 0以上のみ |
long | あり | 64ビットの符号付き整数 |
ulong | なし | 64ビットの符号なし整数 |
特に右シフトでは、符号付きか符号なしかで左側に入るビットが変わります。
C#int signedValue = -8;
uint unsignedValue = 0xFFFFFFF8;
Console.WriteLine(signedValue >> 2); // -2
Console.WriteLine(unsignedValue >> 2); // 1073741822
intの>>は符号を維持しますが、uintの>>は左側を0で埋めます。
7-5. checked・uncheckedとオーバーフローの考え方
C#には、算術演算のオーバーフローを検出するcheckedコンテキストがあります。
C#checked
{
int x = int.MaxValue;
// int y = x + 1; // オーバーフロー例
}
しかし、ビットシフト演算は通常の加算や乗算とは違い、ビット列を直接操作します。そのため、シフトによって上位ビットが失われても、算術オーバーフロー例外として扱われるわけではありません。
C#checked
{
int x = 1 << 31;
Console.WriteLine(x); // -2147483648
}
C#の公式リファレンスでも、ビット演算子およびシフト演算子によってオーバーフローが発生することはなく、checkedとuncheckedのコンテキストで同じ結果になることが示されています。Microsoft Learn
つまり、ビットシフトでは「オーバーフロー例外が出るか」よりも、「ビットがどのように残るか、破棄されるか」を意識する必要があります。
8. シフト数に関するC#特有のルール
8-1. シフト数が0の場合
シフト数が0の場合、値は変わりません。
C#int x = 123;
Console.WriteLine(x << 0); // 123
Console.WriteLine(x >> 0); // 123
Console.WriteLine(x >>> 0); // 123
これは、ビットをまったく移動しないためです。
8-2. シフト数が型のビット数以上の場合
C#で特に注意したいのが、シフト数が型のビット数以上の場合です。
たとえば、intは32ビットなので、1 << 32は0になりそうに見えるかもしれません。
しかし、C#ではそうなりません。
C#int x = 1 << 32;
Console.WriteLine(x); // 1
これは、intやuintのシフトでは、シフト数の下位5ビットだけが使われるためです。
32は2進数で次のように表せます。
32 = 0b100000
下位5ビットだけを見ると0です。
100000
下位5ビット = 00000
そのため、1 << 32は実質的に1 << 0として扱われます。
8-3. int・uintでは下位5ビットが使われる
intとuintのシフト演算では、シフト数の下位5ビットだけが使われます。
つまり、実際のシフト数は次のように計算されます。
C#count & 0x1F
0x1Fは10進数で31です。そのため、実際のシフト数は0から31の範囲になります。
C#Console.WriteLine(1 << 0); // 1
Console.WriteLine(1 << 1); // 2
Console.WriteLine(1 << 31); // -2147483648
Console.WriteLine(1 << 32); // 1
Console.WriteLine(1 << 33); // 2
C#のリファレンスでも、intまたはuintでは右側オペランドの下位5ビットによってシフトカウントが定義されると説明されています。Microsoft Learn
8-4. long・ulongでは下位6ビットが使われる
longとulongは64ビットなので、シフト数の下位6ビットが使われます。
実際のシフト数は次のように計算されます。
C#count & 0x3F
0x3Fは10進数で63です。そのため、実際のシフト数は0から63の範囲になります。
C#Console.WriteLine(1L << 0); // 1
Console.WriteLine(1L << 1); // 2
Console.WriteLine(1L << 63); // -9223372036854775808
Console.WriteLine(1L << 64); // 1
Console.WriteLine(1L << 65); // 2
C#のリファレンスでも、longまたはulongでは右側オペランドの下位6ビットによってシフトカウントが定義されると説明されています。Microsoft Learn
8-5. 想定外の結果を避ける書き方
シフト数が大きくなる可能性がある場合は、明示的に範囲チェックをすると安全です。
C#int ShiftLeftSafe(int value, int count)
{
if (count < 0 || count >= 32)
{
throw new ArgumentOutOfRangeException(nameof(count));
}
return value << count;
}
longの場合は、64未満かどうかを確認します。
C#long ShiftLeftSafe(long value, int count)
{
if (count < 0 || count >= 64)
{
throw new ArgumentOutOfRangeException(nameof(count));
}
return value << count;
}
特に外部入力や設定値をシフト数として使う場合は、C#の仕様に任せるのではなく、意図した範囲に収まっているか確認する方が読みやすく安全です。
9. ビットシフトの実用例
9-1. 2の累乗を高速に計算する
1 << nを使うと、2のn乗を表せます。
C#int power = 1 << 5;
Console.WriteLine(power); // 32
ただし、単に2の累乗を計算したいだけなら、可読性を考えてMath.Powや定数を使う方がよい場合もあります。
C#double power = Math.Pow(2, 5);
Console.WriteLine(power); // 32
ビットシフトを使うべきなのは、「ビット位置を指定したい」「フラグを定義したい」など、ビット操作の意図が明確な場合です。
9-2. ビットフラグを管理する
ビットシフトの代表的な実用例がビットフラグです。
C#const int Read = 1 << 0; // 1
const int Write = 1 << 1; // 2
const int Execute = 1 << 2; // 4
int permission = Read | Write;
bool canRead = (permission & Read) != 0;
bool canExecute = (permission & Execute) != 0;
Console.WriteLine(canRead); // True
Console.WriteLine(canExecute); // False
このように、複数の状態を1つの整数で管理できます。
C#では、列挙型と[Flags]属性を組み合わせることもよくあります。
C#[Flags]
enum Permission
{
None = 0,
Read = 1 << 0,
Write = 1 << 1,
Execute = 1 << 2
}
Permission permission = Permission.Read | Permission.Write;
Console.WriteLine(permission.HasFlag(Permission.Read)); // True
Console.WriteLine(permission.HasFlag(Permission.Execute)); // False
9-3. マスク処理とビット抽出
ビットシフトとAND演算を組み合わせると、特定のビットだけを取り出せます。
たとえば、整数の下位4ビットだけを取り出す場合は次のように書きます。
C#int value = 0b_1011_0110;
int lower4Bits = value & 0b_0000_1111;
Console.WriteLine(Convert.ToString(lower4Bits, 2)); // 110
指定した位置のビットを取り出したい場合は、右シフトしてからマスクします。
C#int value = 0b_1011_0110;
// 4ビット目から3ビット分を取り出す
int extracted = (value >> 4) & 0b_0111;
Console.WriteLine(Convert.ToString(extracted, 2)); // 101
このような処理は、バイナリフォーマットや通信プロトコルの解析でよく使われます。
9-4. RGBカラー値の分解・結合
RGBカラーを1つの整数として扱う場合にも、ビットシフトが便利です。
たとえば、赤、緑、青をそれぞれ8ビットとして、1つの整数にまとめます。
C#int r = 255;
int g = 128;
int b = 64;
int color = (r << 16) | (g << 8) | b;
Console.WriteLine(color); // 16744512
ビット構造は次のようになります。
RRRRRRRR GGGGGGGG BBBBBBBB
逆に、1つの整数からRGBを取り出す場合は、右シフトとマスクを使います。
C#int color = 0xFF8040;
int r = (color >> 16) & 0xFF;
int g = (color >> 8) & 0xFF;
int b = color & 0xFF;
Console.WriteLine(r); // 255
Console.WriteLine(g); // 128
Console.WriteLine(b); // 64
0xFFは下位8ビットを取り出すためのマスクです。
9-5. バイナリデータや通信データの処理
通信データやファイルフォーマットでは、複数の情報が1つの整数やバイト列に詰め込まれていることがあります。
たとえば、上位4ビットに種類、下位4ビットに値が入っているとします。
C#byte data = 0b_1010_0011;
int type = (data >> 4) & 0b_1111;
int value = data & 0b_1111;
Console.WriteLine(type); // 10
Console.WriteLine(value); // 3
このように、シフト演算を使うと、特定の位置にあるビット情報を取り出せます。
また、複数のバイトから1つの整数を作る場合にも使われます。
C#byte high = 0x12;
byte low = 0x34;
int combined = (high << 8) | low;
Console.WriteLine(combined.ToString("X4")); // 1234
9-6. ゲーム開発・組み込み系での活用例
ゲーム開発では、状態管理やレイヤー管理などでビットフラグが使われることがあります。
C#const int IsVisible = 1 << 0;
const int IsMoving = 1 << 1;
const int IsAttacking = 1 << 2;
int state = IsVisible | IsMoving;
bool moving = (state & IsMoving) != 0;
組み込み系では、ハードウェアのレジスタ操作でビット演算がよく使われます。
C#const byte EnableBit = 1 << 3;
byte register = 0;
register |= EnableBit; // 3番目のビットを立てる
register &= unchecked((byte)~EnableBit); // 3番目のビットを下げる
このようなコードでは、数値としての意味よりも、ビットの位置そのものが重要になります。
10. ビットシフト演算子でよくある間違い
10-1. 掛け算・割り算の代わりに安易に使う
左シフトは2倍、右シフトは2分の1に近い結果になるため、掛け算や割り算の代わりに使いたくなるかもしれません。
C#int x = value << 1; // value * 2 のつもり
int y = value >> 1; // value / 2 のつもり
しかし、通常の計算であれば次のように書く方が読みやすいです。
C#int x = value * 2;
int y = value / 2;
現代のコンパイラや実行環境では、単純な最適化は自動で行われることが多いため、可読性を犠牲にしてまでシフト演算を使う必要はありません。
ビットシフトは「ビットを操作している」という意図があるときに使うのが基本です。
10-2. 負の数の右シフト結果を誤解する
負の数に>>を使うと、左側が1で埋められるため、結果も負の数になります。
C#int x = -8;
Console.WriteLine(x >> 1); // -4
Console.WriteLine(x >> 2); // -2
「右シフトだから左側には0が入る」と思い込むと、負の数で予想外の結果になります。
符号を維持したくない場合は、C# 11以降で>>>を使うことを検討します。
C#int x = -8;
Console.WriteLine(x >>> 2); // 1073741822
10-3. byte型の結果がbyteになると思い込む
byteに対してシフト演算を行っても、結果は通常intになります。
C#byte x = 1;
var y = x << 2;
Console.WriteLine(y.GetType()); // System.Int32
次のように書くと、コンパイルエラーになります。
C#byte x = 1;
// byte y = x << 2; // intをbyteに暗黙変換できない
必要であればキャストします。
C#byte x = 1;
byte y = (byte)(x << 2);
ただし、値がbyteの範囲を超えると、上位ビットが失われる可能性があります。
10-4. シフト数が大きすぎる場合の挙動を見落とす
intで1 << 32と書くと、0ではなく1になります。
C#Console.WriteLine(1 << 32); // 1
これは、intのシフト数では下位5ビットだけが使われるためです。
同様に、次のような結果になります。
C#Console.WriteLine(1 << 33); // 2
Console.WriteLine(1 << 34); // 4
シフト数が外部から渡される場合は、必ず範囲チェックを行いましょう。
C#if (count < 0 || count >= 32)
{
throw new ArgumentOutOfRangeException(nameof(count));
}
10-5. 可読性を下げるコードを書いてしまう
ビットシフトは強力ですが、慣れていない人にとっては読みにくいコードになりがちです。
C#int value = (x >> 5) & 0x07;
このようなコードを書く場合は、意味のある名前を付けると読みやすくなります。
C#const int CategoryShift = 5;
const int CategoryMask = 0x07;
int category = (x >> CategoryShift) & CategoryMask;
さらに、処理の意味をメソッドに切り出すのも有効です。
C#int GetCategory(int value)
{
const int CategoryShift = 5;
const int CategoryMask = 0x07;
return (value >> CategoryShift) & CategoryMask;
}
ビットシフトでは、処理の正しさだけでなく、後から読む人が意図を理解できるかも重要です。
11. ビットシフトと他のビット演算子の組み合わせ
11-1. AND演算子&との組み合わせ
AND演算子&は、特定のビットだけを取り出すときによく使います。
C#int value = 0b_1010_1100;
int mask = 0b_0000_1111;
int result = value & mask;
Console.WriteLine(Convert.ToString(result, 2)); // 1100
上位4ビットを取り出したい場合は、右シフトと組み合わせます。
C#int value = 0b_1010_1100;
int upper = (value >> 4) & 0b_1111;
Console.WriteLine(Convert.ToString(upper, 2)); // 1010
11-2. OR演算子|との組み合わせ
OR演算子|は、特定のビットを立てるときに使います。
C#int flags = 0b_0000;
int mask = 1 << 2;
flags = flags | mask;
Console.WriteLine(Convert.ToString(flags, 2)); // 100
複合代入を使うと、次のように書けます。
C#flags |= 1 << 2;
フラグを追加するときによく使う書き方です。
C#const int Read = 1 << 0;
const int Write = 1 << 1;
int permission = 0;
permission |= Read;
permission |= Write;
11-3. XOR演算子^との組み合わせ
XOR演算子^は、特定のビットを反転するときに使えます。
C#int flags = 0b_0101;
int mask = 0b_0001;
flags ^= mask;
Console.WriteLine(Convert.ToString(flags, 2)); // 100
対象ビットが1なら0に、0なら1になります。
C#int flag = 0b_0000;
flag ^= 1 << 1; // 1にする
Console.WriteLine(Convert.ToString(flag, 2)); // 10
flag ^= 1 << 1; // 0に戻す
Console.WriteLine(Convert.ToString(flag, 2)); // 0
トグル処理で使われることがあります。
11-4. NOT演算子~との組み合わせ
NOT演算子~は、すべてのビットを反転します。
C#int x = 0b_0000_1111;
int y = ~x;
単独で使うとすべてのビットが反転するため、負の数になったり、想定より広い範囲に影響したりします。
特定のフラグを削除する場合は、ANDと組み合わせて使います。
C#const int Read = 1 << 0;
const int Write = 1 << 1;
int permission = Read | Write;
// Writeフラグを削除
permission &= ~Write;
Console.WriteLine((permission & Write) != 0); // False
~Writeによって、Writeのビットだけが0で、それ以外が1のマスクを作っています。
11-5. フラグ判定・追加・削除の実装例
ビットフラグの基本操作をまとめると、次のようになります。
C#const int Read = 1 << 0;
const int Write = 1 << 1;
const int Execute = 1 << 2;
int flags = 0;
// 追加
flags |= Read;
flags |= Write;
// 判定
bool hasRead = (flags & Read) != 0;
bool hasExecute = (flags & Execute) != 0;
// 削除
flags &= ~Write;
// 反転
flags ^= Execute;
Console.WriteLine(hasRead); // True
Console.WriteLine(hasExecute); // False
より実務的には、メソッド化しておくと使いやすくなります。
C#static bool HasFlag(int flags, int flag)
{
return (flags & flag) != 0;
}
static int AddFlag(int flags, int flag)
{
return flags | flag;
}
static int RemoveFlag(int flags, int flag)
{
return flags & ~flag;
}
static int ToggleFlag(int flags, int flag)
{
return flags ^ flag;
}
このように整理すると、ビット演算に慣れていない人でもコードの意図を追いやすくなります。
12. ビットシフト演算子に関するよくある質問
12-1. C#の<<と>>は何をする演算子ですか?
<<は左シフト演算子で、ビットを左にずらします。
C#Console.WriteLine(4 << 1); // 8
>>は右シフト演算子で、ビットを右にずらします。
C#Console.WriteLine(8 >> 1); // 4
どちらも、数値を2進数のビット列として見たときに、ビットの位置を移動させる演算子です。
12-2. 左シフトは掛け算、右シフトは割り算と同じですか?
正の整数で、ビットがあふれない範囲では、左シフトは2の累乗倍、右シフトは2の累乗で割る処理に近い結果になります。
C#Console.WriteLine(5 << 1); // 10
Console.WriteLine(5 << 2); // 20
Console.WriteLine(20 >> 1); // 10
Console.WriteLine(20 >> 2); // 5
しかし、完全に同じと考えるのは危険です。
特に次のような場合は注意が必要です。
| 注意点 | 内容 |
|---|---|
| 上位ビットのあふれ | 左シフトでビットが破棄される |
| 負の数 | 右シフトで符号ビットが影響する |
| シフト数 | intでは下位5ビットだけが使われる |
| 可読性 | 掛け算・割り算の方が意図が明確なことが多い |
通常の数値計算では*や/を使い、ビット操作の意図があるときにシフト演算を使うのがおすすめです。
12-3. >>と>>>は何が違いますか?
>>は、符号付き整数では符号を維持する右シフトです。負のintやlongでは、左側に1が入ります。
C#int x = -8;
Console.WriteLine(x >> 2); // -2
>>>は、符号に関係なく左側に0を入れる右シフトです。
C#int x = -8;
Console.WriteLine(x >>> 2); // 1073741822
正の数では同じ結果になることが多いですが、負の数では結果が大きく変わります。
12-4. byte型にシフト演算を使うとどうなりますか?
byte型にシフト演算を使うと、通常はintに変換されてから演算されます。
C#byte x = 2;
var y = x << 1;
Console.WriteLine(y); // 4
Console.WriteLine(y.GetType()); // System.Int32
結果をbyteとして扱いたい場合は、明示的なキャストが必要です。
C#byte x = 2;
byte y = (byte)(x << 1);
Console.WriteLine(y); // 4
ただし、byteの範囲は0から255なので、シフト結果が範囲を超えると情報が失われる可能性があります。
12-5. ビットシフトは実務でいつ使いますか?
ビットシフトは、次のような場面で使われます。
| 場面 | 例 |
|---|---|
| フラグ管理 | 複数の状態を1つの整数で管理 |
| バイナリ解析 | 通信データやファイル形式の解析 |
| 画像処理 | RGB値の分解・結合 |
| ゲーム開発 | レイヤー、状態、属性の管理 |
| 組み込み系 | レジスタ操作、ハードウェア制御 |
| ハッシュ処理 | ビットを混ぜる処理 |
一方で、単なる掛け算や割り算の代替として使う場面は多くありません。
ビットシフトは「数値計算のテクニック」ではなく、「ビット列を直接操作するための道具」と考えると、使いどころを判断しやすくなります。
まとめ
C#のビットシフト演算子は、整数を2進数のビット列として扱い、ビットを左右に移動させるための演算子です。
<<は左シフトで、ビットを左にずらします。ビットがあふれない範囲では、左に1ビットずらすと値は2倍になります。
>>は右シフトで、ビットを右にずらします。正の整数では2で割るような結果になりますが、intやlongの負の値では符号ビットを維持するため、左側が1で埋められます。
>>>は符号なし右シフトで、左側の空いたビットを常に0で埋めます。負の数を符号ではなくビット列として右シフトしたい場合に便利です。
また、C#ではbyteやshortなどの小さい整数型はシフト演算時にintへ変換されること、intやuintではシフト数の下位5ビット、longやulongでは下位6ビットだけが使われることにも注意が必要です。
ビットシフトは、ビットフラグ、マスク処理、RGBカラー値の分解・結合、バイナリデータ処理、ゲーム開発、組み込み系などで役立ちます。
ただし、通常の掛け算や割り算の代わりに安易に使うと、可読性が下がったり、負の数やオーバーフロー相当の挙動で予想外の結果になったりします。
C#でビットシフトを使うときは、単に「高速そうだから使う」のではなく、「ビット列を操作したい」という目的が明確な場面で使うのが大切です。

