# 整数
始めに書いておくがこの章はユーモア欠落症患者によって書かれており極めて退屈だ。しかし、整数の詳細はすべてのプログラマーが理解すべきものだ。心して読むとよい。
# 整数リテラル
整数リテラルとは整数の値を直接ソースファイルに記述する機能だ。本書ではここまで何の説明もなくリテラルを使っていた。例えば以下のように。
int main()
{
int a = 123 ;
int b = 0 ;
int c = -123 ;
}
ここでは、'123'
, '0'
がリテラルだ。'-123'
というのは演算子operator -
に整数リテラル123
を適用したものだ。リテラルは123
だけだ。ただしこれは細かい詳細なのでいまはそれほど気にしなくてもよい。
# 10進数リテラル
10進数リテラルは最も簡単で我々が日常的に使っている数の表記方法と同じものだ。接頭語は何も使わず数字には0
, 1
, 2
, 3
, 4
, 5
, 6
, 7
, 8
, 9
が使える。
// 10進数で123
int decimal = 123 ;
ただし、10進数リテラルの先頭を0
にしてはならない。これは8進数リテラルになってしまう。
// 10進数で83
int octal = 0123 ;
# 2進数リテラル
2進数リテラルは接頭語'0b'
, '0B'
から始まる。数字には0
, 1
を使うことができる。
// 10進数で5
int binary = 0b1010 ;
// 0bと0Bは同じ
int a = 0B1010 ;
# 8進数リテラル
8進数リテラルは接頭語'0'
から始まる。数字には0
, 1
, 2
, 3
, 4
, 5
, 6
, 7
を使うことができる。
// 10進数で83
int octal = 0123 ;
// 10進数で342391
int a = 01234567 ;
# 16進数リテラル
16進数リテラルは接頭語'0x'
, '0X'
から始まる。数字には0
, 1
, 2
, 3
, 4
, 5
, 6
, 7
, 8
, 9
, a
, b
, c
, d
, e
, f
, A
, B
, C
, D
, E
, F
が使える。ローマ字の大文字と小文字は意味が同じだ。a
, b
, c
, d
, e
, f
がそれぞれ10
, 11
, 12
, 13
, 14
, 15
を意味する。
// 10進数で291
int hexadecimal = 0x123 ;
// 0xと0Xは同じ
int a = 0X123 ;
// 10進数で10
int b = 0xa ;
// 10進数で15
int c = 0xf ;
# 数値区切り
長い整数リテラルは読みにくい。例えば10000000
と100000000
はどちらが大きくて具体的にどのくらいの値なのかがわからない。C++には整数リテラルを読みやすいように区切ることのできる数値区切りという機能がある。整数リテラルはシングルクオート文字('
)で区切ることができる。
int main()
{
int a = 1000'0000 ;
int b = 1'0000'0000 ;
}
区切り幅は何文字でもよい。
int main()
{
int a = 1'22'333'4444'55555 ;
}
10進数整数リテラル以外でも使える。
int main()
{
auto a = 0b10101010'11110000'00001111 ;
auto b = 07'7'5 ;
auto c = 0xde'ad'be'ef ;
}
# 整数の仕組み
# 情報の単位
0から100までの整数を表現するには101種類の状態を表現できる必要がある。コンピューターはどうやって整数を表現しているのかをここで学ぶ。
情報の最小単位はビット(bit)だ。ビットは2種類の状態を表現できる。たとえばbool
型はtrue
/false
という2種類の状態を表現できる。
しかし、2種類の状態しか表現できない整数は使いづらい。0もしくは1しか表現できない整数とか、100もしく1000しか表現できない整数は使い物にならない。
また、ビットという単位も扱いづらい。コンピューターは膨大な情報を扱うので、ビットをいくつかまとめたバイト(byte)を単位として情報を扱っている。1バイトが何ビットであるかは環境により異なる。本書では最も普及している1バイトは8ビットを前提にする。
1ビットは2種類の状態を表現できるので、1バイトの中の8ビットは$2^8 = 256$種類の状態を表現できる。2バイトならば16ビットとなり、$2^{16} = 65536$種類の状態を表現できる。
# 1バイトで表現された整数
整数の表現方法について理解するために、1バイトで表現された整数を考えよう。
1バイトは8ビットであり256種類の状態を表現できる。整数を0から正の方向の数だけ表現したいとすると、0から255までの値を表現できることになる。
その場合、1バイトの整数の中の8ビットはちょうど2進数8桁で表現できる。
// 0
auto zero = 0b00000000 ;
// 255
auto max = 0b11111111 ;
一番左側の桁が最上位桁で、一番右側の桁が最下位桁だ。これを最上位ビット、最下位ビットともいう。
正数だけを表現するならば話は簡単だ。1バイトの整数は0から255までの値を表現できる。これを符号なし整数(unsigned integer)という。
では負数を表現するにはどうしたらいいだろう。正数と負数を両方扱える整数表現のことを、符号付き整数(signed integer)という。1バイトは256種類の状態しか表現できないので、もし$-1$を表現したい場合、$-1$から254までの値を扱えることになる。
$-1$しか扱えないのでは実用的ではないので、負数と正数を同じ種類ぐらい表現したい。256の半分は128だが、1バイトで表現された整数は$-128$から128までを表現することはできない。0があるからだ。0を含めると、1バイトの整数は最大で$-128$から127までか、$-127$から128までを表現できる。どちらかに偏ってしまう。
では実際に1バイトで負数も表現できる整数表現を考えてみよう。
# 符号ビット
誰でも思いつきそうな表現方法に、符号ビットがある。これは最上位ビットを符号の有無を管理するフラグとして用いることにより、下位7ビットの値の符号を指定する方法だ。
符号ビット表現では$-1$と1は以下のように表現できる。
// 1
0b0'0000001
// -1
0b1'0000001
最上位ビットが0であれば正数、1であれば負数だ。
この一見わかりやすい表現方法には問題がある。まず表現できる値の範囲は$-127$から$+127$だ。先ほど、1バイトで正負になるべく均等に値を割り振る場合、$-128$から$+127$、もしくは$-127$から$+128$までを扱えると書いた。しかし符号ビット表現では$-127$から$+127$しか扱えない。残りの1はどこにいったのか。
答えはゼロにある。符号ビット表現ではゼロに2通りの表現がある。$+0$と$-0$だ。
// +0
0b0'0000000
// -0
0b1'0000000
$+0$も$-0$もゼロには違いない。しかし符号ビットが独立して存在しているために、ゼロが2種類ある。
符号ビットは電子回路で実装するには複雑という問題もある。
# 1の補数
1の補数は負数を絶対値を2進数で表したときの各ビットを反転させた値で表現する。たとえば$-1$は1(0b00000001
)の1の補数の0b11111110
で表現される。
// -1
0b11111110
// -2
0b11111101
$-1$と$-2$を足すと結果は$-3$だ。この計算を1の補数で行うとどうなるか。
まず1の補数表現による$-1$と$-2$を足す。
11111110
+) 11111101
-----------
1'11111011
この結果は9ビットになる。この整数は8ビットなので、9ビット目を表現することはできない。ただし1の補数表現の計算では、もし9ビット目が繰り上がった場合は、演算結果に1を足す取り決めがある。
11111011
+) 1
-----------
11111100
1の補数による$-3$は3の各ビットを反転したものだ。3は0b00000011
で、そのビットを反転させたものは0b11111100
だ。上の計算結果は$-3$の1の補数表現になった。
もう1つ例を見てみよう。5と$-2$を足すと3になる。
00000101
+) 11111101
-----------
1'00000010
繰り上がりが発生したので1を足すと
00000010
+) 1
-----------
00000011
3になった。
1の補数は引き算も足し算で表現できるので電子回路での実装が符号ビットよりもやや簡単になる。
ただし、1の補数にも問題がある。0の表現だ。0というのは0b00000000
だが1の補数では$-x$は$x$の各ビット反転ということを適用すると、$-0$は0b11111111
になる。すると、符号ビット表現と同じく、$+0$と$-0$が存在することになる。したがって、1の補数8ビットで表現できる範囲は$-127$から$+127$になる。
# 2の補数
符号ビットと1の補数による負数表現にある問題は、2の補数表現で解決できる。
2の補数表現による負数は1の補数表現の負数に、繰り上がり時に足すべき1を加えた値になる。
$-1$は1の補数表現では、1(0b00000001
)の各ビットを反転させた値になる(0b11111110
)。2の補数表現では、1の補数表現に1を加えた値になるので、0b11111111
になる。
同様に、$-2$は0b11111110
に、$-3$は0b11111101
になる。
2の補数表現の$-1$と$-2$を足すと以下のようになる。
11111111
+) 11111110
-----------
1'11111101
9ビット目の繰り上がりを無視すると、計算結果は0b11111101
になる。これは2の補数表現による$-3$と同じだ。
5と$-2$の計算も見てみよう。
00000101
+) 11111110
-----------
1'00000011
結果は3(0b00000011
)だ。
2の補数表現は引き算も足し算で実装できる上に、ゼロの表現方法は1つで、$+0$と$-0$が存在しない。8ビットの2の補数表現された整数の範囲は$-128$から$+127$になる。とても便利な負数の表現方法なのでほとんどのコンピューターで採用されている。
# 整数型
C++にはさまざまな整数型が存在する。C++はCから引き継いだ歴史的な経緯により、整数型の文法がわかりにくくなっている。
基本的には、符号付き整数型と符号なし整数型に分かれている。
符号付き整数型としては、signed char
, short int
, int
, long int
, long long int
が存在する。符号付き整数型は負数を表現できる。
符号なし整数型としては、unsigned char
, unsigned short int
, unsigned int
, unsigned long int
, unsigned long long int
が存在する。符号なし整数型は負数を表現できない。
# int型
int型
は最も基本となる整数型だ。C++で数値を扱う場合、多くはint
型になる。
int x = 123 ;
整数リテラルの型は通常はint
型になる。
// int
auto x = 123 ;
unsigned int型
は符号のないint
型だ。
unsigned int x = 123 ;
整数リテラルの末尾にu
/U
と書いた場合、unsigned int
型になる。
// int
auto x = 123 ;
// unsigned int
auto y = 123u ;
特殊なルールとして、単にsigned
と書いた場合、それはint
になる。unsigned
と書いた場合は、unsigned int
になる。
// int
signed a = 1 ;
// unsigned int
unsigned b = 1 ;
signed int
と書いた場合、int型
になる。signed int
はint
の冗長な書き方だ。
# long int型
long int型
はint型
以上の範囲の整数を扱える型だ。具体的な整数型の値の範囲は実装依存だが、long int型
はint型
の表現できる整数の範囲はすべて表現でき、かつint型
以上の範囲の整数型を表現できるかもしれない型だ。
unsigned long int型
は符号なしのlong int
だ。
long int a = 123 ;
unsigned long int b = 123 ;
特殊なルールとして、単にlong
と書いた場合、それはlong int
になる。unsigned long
と書いた場合、unsigned long int
になる。
// long int
long a = 1 ;
// unsigned long int
unsigned long b = 1 ;
通常、int
を省略して単にlong
と書くことが多い。
整数リテラルの値がint型
で表現できない場合、long型
になる。例えば、int型
で100億を表現できないが、long型
では表現できる実装の場合、以下の変数a
はlong型
になる。
// 100億
auto a = 100'0000'0000 ;
整数リテラルの値がlong
では表現できないがunsigned long
では表現できる場合、unsigned long型
になる。
整数リテラルの末尾にl
/L
と書いた場合、値にかかわらずlong型
になる。
// int
auto a = 123 ;
// long
auto b = 123l ;
// long
auto c = 123L ;
符号なし整数型を意味するu
/U
と組み合わせることもできる。
// unsigned long
auto a = 123ul ;
auto b = 123lu ;
順番と大文字小文字の組み合わせは自由だ。
# long long int型
long long int型
はlong int型
以上の範囲の整数を扱える型だ。long
と同じくlong long
はlong long int
と同じで、unsigned long long int
もある。
// long long int
long long a = 1 ;
// unsigned long long int
unsigned long long b = 1 ;
整数リテラルの値がlong型
でも表現できないときは、long long
が使われる。long long
でも表現できない場合はunsigned long long
が使われる。
整数リテラルの末尾にll
/LL
と書くとlong long int型
になる。
// long long int
auto a = 123ll ;
// long long int
auto b = 123LL ;
// unsigned long long int
auto c = 123ull ;
# short int型
short int型
はint型
より小さい範囲の値を扱う整数型だ。long
, long long
と同様に、unsigned short int
型もある。単にshort
と書くと、short int
と同じ意味になる。
整数リテラルでshort int
型を表現する方法はない。
# char型
char型
はやや特殊で、char
, signed char
, unsigned char
の3種類の型がある。signed char
とchar
は別物だ。char型
は整数型であり、あとで説明するように文字型でもある。char型
の符号の有無は実装ごとに異なる。
# 整数型のサイズ
整数型を含む変数のサイズは、sizeof演算子
で確認することができる。sizeof(T)
はT
に型名や変数名を入れることで、サイズを取得することができる。
int main()
{
std::cout << sizeof(int) << "\n"s ;
int x{} ;
std::cout << sizeof(x) ;
}
sizeof演算子
はstd::size_t型
を返す。vector
の章でも出てきたこの型は実装依存の符号なし型であると定義されている。単位はバイトだ。
以下が各種整数型のサイズを出力するプログラムだ。
int main()
{
auto print = []( std::size_t s )
{ std::cout << s << "\n"s ; } ;
print( sizeof(char) ) ;
print( sizeof(short) ) ;
print( sizeof(int) ) ;
print( sizeof(long) ) ;
print( sizeof(long long ) ) ;
}
このプログラムを筆者の環境で実行した結果が以下になる。
1
2
4
8
8
どうやら筆者の環境では、char
が1バイト、short
が2バイト、int
が4バイト、long
とlong long
が8バイトのようだ。この結果は環境ごとに異なるので読者も自分でsizeof
演算子をさまざまな型に適用して試してほしい。
# 整数型の表現できる値の範囲
整数型の表現できる値の最小値と最大値はstd::numeric_limits<T>
で取得できる。最小値は::min()
を、最大値は::max()
で得られる。
int main()
{
std::cout
<< std::numeric_limits<int>::min() << "\n"s
<< std::numeric_limits<int>::max() ;
}
実行結果
-2147483648
2147483647
どうやら筆者の環境ではint
型は$−21億4748万3648$から21億4748万3647までの範囲の値を表現できるようだ。
unsigned int
はどうだろうか。
int main()
{
std::cout
<< std::numeric_limits<unsigned int>::min() << "\n"s
<< std::numeric_limits<unsigned int>::max() ;
}
実行結果
0
4294967295
どうやら筆者の環境ではunsigned int
型は0から42億9496万7295までの範囲の値を表現できるようだ。sizeof(int)
が4バイトであり、1バイトが8ビットの筆者の環境では自然な値だ。符号なしの4バイト整数型は0から$2^{32}-1$までの範囲の値を表現できる。符号付き4バイト整数型は$-2^{31}$から$2^{31}-1$までの範囲の値を表現できる。
整数の最小値を$-1$したり、最大値を$+1$した場合、何が起こるのだろうか。
符号なし整数型の場合は簡単だ。最小値$-1$は最大値になる。最大値$+1$は最小値になる。
int main()
{
unsigned int min = std::numeric_limits<unsigned int>::min() ;
unsigned int max = std::numeric_limits<unsigned int>::max() ;
unsigned int min_minus_one = min - 1u ;
unsigned int max_plus_one = max + 1u ;
std::cout << min << "\n"s << max << "\n"s
<< min_minus_one << "\n"s << max_plus_one ;
}
8ビットの符号なし整数型があるとして、最小値は0b00000000
(0)になるが、この値を$-1$すると0b11111111
(255)となり、これは最大値になる。逆に、最大値である0b11111111
(255)に$+1$すると0b00000000
(0)となり、これは最小値になる。
これを数学的に厳密に書くと、「符号なし整数は算術モジュロ$2^n$の法に従う。ただし$n$は整数を表現する値のビット数である」となる。
符号付き整数型の場合、挙動は定められていない。ただし、一般に普及している2の補数表現の場合は、以下のような挙動になることが多い。
符号付き整数型の最小値を$-1$すると最大値になり、最大値を$+1$すると最小値になる。
int main()
{
int min = std::numeric_limits<int>::min() ;
int max = std::numeric_limits<int>::max() ;
int min_minus_one = min - 1 ;
int max_plus_one = max + 1 ;
std::cout << min << "\n"s << max << "\n"s
<< min_minus_one << "\n"s << max_plus_one ;
}
これはなぜか。2の補数表現の8ビットの符号付き整数の最小値は0b10000000
($-128$)だが、これを$-1$すると0b01111111
(127)となり、これは最大値となる。逆に最大値0b01111111
(127)を$+1$すると0b10000000
($-128$)となり、これは最小値となる。
# 整数型の変換
整数型にはここで紹介しただけでも、さまざまな型がある。同じ型同士を使った方がよい。
以下は型が一致している例だ。
int main()
{
int a = 123 ;
long b = 123l ;
long long c = 123ll ;
unsigned int d = 123u ;
}
以下は型が一致していない例だ。
int main()
{
// intからshort
short a = 123 ;
// longからint
int b = 123l ;
// intからunsigned int
unsigned int c = 123 ;
// unsigned intからint
int d = 123u ;
}
代入や演算で整数型が一致しない場合、整数型の変換が行われる。
整数型の変換で注意すべきこととしては、変換元の値を変換先の型で表現できない場合の挙動だ。
たとえばshort
型とint
型の表現できる最大値を調べるプログラムを書いてみよう。
int main()
{
std::cout << "short: "s << std::numeric_limits<short>::max() << "\n"s
<< "int: "s << std::numeric_limits<int>::max() ;
}
これを実行すると筆者の環境では以下のようになる。
short: 32767
int: 2147483647
どうやら筆者の環境ではshort
型は約3万、int
型は約21億ぐらいの値を表現できるようだ。
では約3万までしか表現できないshort
型に4万を代入しようとするとどうなるのか。これは1つ前の整数型の表現できる値の範囲で説明したものと同じことが起こる。
int main()
{
short x = 40000 ;
std::cout << x ;
}
このプログラムを実行した結果は実装ごとに異なる。例えば筆者の環境では以下のようになる。
-25536
整数型の変換は暗黙的に行われるが、明示的に行うこともできる。明示的な変換にはstatic_cast<T>(e)
を使う。static_cast
は値e
を型T
の値に変換する。
int main()
{
int x = 123 ;
short y = static_cast<short>(x) ;
}