整数型 | Programming Place Plus 新C++編

トップページ新C++編

このページの概要 🔗

このページでは、整数型について取り上げます。すでに int、unsigned int、char、bool といった整数型が登場していますが、これらのほかにも多数ある整数型をまとめて紹介します。これらの型の大きさ、表現できる値の範囲に関することや、これらの型による演算がどのようなルールで行われるのかについて取り上げていきます。

このページの解説は C++14 をベースとしています

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



整数型 🔗

前のページで列挙型を説明し、基底型として任意の整数型を指定できることに触れました。現時点で登場している整数型は、int、unsigned int、char、bool の4つですが、これ以外にも多くの整数型が存在します。このページでは残りの整数型の大部分を紹介します。

整数型に多くの種類があるのは、表現したい数の範囲によって使い分けるためです。広い範囲の整数が表現できなければならないときには、メモリの消費量が大きくなってしまってでも、大きな整数型を使う必要性があります。反対に、表現したい数の範囲が狭いのなら、小さな整数型を選ぶことによって、メモリを節約できます。

しかし、int と unsigned int の不用意な混在は避けたほうがいい(「符号無し整数」のページを参照)のと同じで、さまざまな整数型を不用意に混在させない方がいいです。型の変換ルールは非常に複雑であり、正しく取り扱うには相当の慎重さが求められます。とはいえ、int型で表現しきれない数を扱うには、より大きな型を選択することは避けられませんから、ある程度の理解は必要です。

C++ には以下の整数型が定義されています。

列挙型は整数型ではありません。

【C++20】char8_t が加わりました。

(signed) の部分は、ソースコードに記述するときには省略できます。signed char の場合は signed を省略すると、char型になるので省略できません。

同様に、(int) の部分も省略できます。

上記の型のうち、signed char、short int、int、long int、long long int の 5つを、標準符号付き整数型 (standard signed integer types) と呼びます。また、unsigned char、unsigned short int、unsigned int、unsigned long int、unsigned long long int の 5つを、標準符号無し整数型 (standard unsigned integer types) と呼びます。そして両方を合わせたものを、標準整数型 (standard integer types) と呼びます。

これらに加えて、処理系が独自の整数型を定義していることがあり、拡張整数型 (extended integer types) と呼ばれます。その目的は多くの場合、標準整数型では表現できないさらに大きな整数型を提供することにあります。拡張整数型にも符号の有無があって、符号付きのものを拡張符号付き整数型 (extended signed integer types)、符号無しのものを拡張符号無し整数型 (extended unsigned integer types) と呼びます。

単に符号付き整数型といった場合、標準符号付き整数型と拡張符号付き整数型を含みます。単に符号無し整数型といった場合は、標準符号無し整数型と拡張符号無し整数型を含みます。

ある符号付き整数型に unsiged キーワードを付加した型を、「対応する符号無し整数型」と表現することがあります。対応する符号無し整数型の大きさは、元の符号付き整数型と必ず一致します。

型の分類表が APPENDIX にあります。

char、signed char、unsigned char 🔗

char型の大きさは、ほとんどの場合 8ビットです。

char型に符号があるかどうかは処理系定義です[1]そのため、char型を小さな整数型のつもりで使うと、ほかの処理系で失敗する恐れがあります。小さな整数型として使いたいときは、signed char型unsigned char型にして、符号の有無を明確にするべきです。

char型、signed char型、unsigned char型の大きさは同じで、すべて別の型です。

// 以下の3つはすべて異なる型
char c {};
signed char sc {};
unsigned char uc {};

なお、これら3種類の文字型を合わせて、狭い文字型(ナロー文字型) (narrow character types) と呼びます。

【C++17】よく、バイト列を表現するために unsigned char型の配列が使われますが、C++17 で、単なるバイトであることを明確にする std::byte型[2]が追加されています。

short 🔗

short型は、小さめの整数型であることを期待して使う型です。unsigned キーワードを付加できます。

// 以下の4つは同等
short si {};
short int si {};
signed short si {};
signed short int si {};

// 以下の2つは同等
unsigned short usi {};
unsigned short int usi {};

short型で表現できる値として -32767~32767 が、unsigned short型で表現できる値として 0~65535 が保証されています[3]この範囲を表現するためには 16ビットが必要であることから、short/unsigned short型の大きさは最低でも 16ビットです。

int型で保証されている値の範囲もまったく同じであるため、short型が「int型と比較して小さい整数」ではない可能性があることには注意が必要です。メモリの節約のつもりで short型を使っても意味がないかもしれません。

現代の処理系では short型が 16ビット、int型が 32ビットであることが多いですが、古い処理系ではいずれも 16ビットのものがあります。

int 🔗

int型は、もっとも基本的な整数型です。unsigned キーワードを付加できます。

// 以下の3つは同等
int i {};
signed int i {};
signed i {};

// 以下の2つは同等
unsigned int ui {};
unsigned ui {};

通常、int型がもっとも効率的に処理できるようになっているため、優先してこの型を選ぶと良いです。int、unsigned int 以外の整数型は、何らかの目的があるときにだけ選択します。

int型で表現できる値として -32767~32767 が、unsigned int型で表現できる値として 0~65535 が保証されています[3]この範囲を表現するためには 16ビットが必要であることから、int/unsigned int型の大きさは最低でも 16ビットです。int型が、short型よりも小さいということはありません。

現代の処理系では int型は 32ビットであることが多いですが、古い処理系では 16ビットのものがあります。

long 🔗

long型は、大きめの整数型であることを期待して使う型です。unsigned キーワードを付加できます。

// 以下の4つは同等
long li {};
long int li {};
signed long li {};
signed long int li {};

// 以下の2つは同等
unsigned long uli {};
unsigned long int uli {};

long型で表現できる値として -2,147,483,647~2,147,483,647 が、unsigned long型で表現できる値として 0~4,294,967,295 が保証されています[3]この範囲を表現するためには 32ビットが必要であることから、long/unsigned long型の大きさは最低でも 32ビットです。long型が int型よりも小さいということはありません。

現代では int型が 32ビットの処理系が多いため、long型が「int型と比較して大きい整数」ではない可能性があることには注意が必要です。int型では表現できない値を表現しようとして long型を選んでも、実は表現しきれない恐れがあります。

long long 🔗

long long型は long型よりもさらに大きいことを期待して使う型です。unsigned キーワードを付加できます。

// 以下の4つは同等
long long lli {};
long long int lli {};
signed long long lli {};
signed long long int lli {};

// 以下の2つは同等
unsigned long long ulli {};
unsigned long long int ulli {};

long long型で表現できる値として -9,223,372,036,854,775,807~9,223,372,036,854,775,807 が、unsigned long long型で表現できる値として 0~18,446,744,073,709,551,615 が保証されています[3]この範囲を表現するためには 64ビットが必要であることから、long long/unsigned long long型の大きさは最低でも 64ビットです。long long型が long型よりも小さいということはありません。

bool 🔗

bool型はすでに何度も登場しています(詳細は「論理値」のページを参照)。true か false を表すための型ですが、分類上は整数型に含まれています。signed や unsigned を付加することはできません。

bool型の大きさは処理系定義です[4]。2種類の値しか扱わないので小さそうに思えますが、最小でも 1バイトはありますし、もっと大きい可能性もあります。

char16_t、char32_t、wchar_t 🔗

char16_t型char32_t型wchar_t型は、それぞれ文字を扱うための型ですが、分類上は整数型に含まれています。signed や unsigned を付加することはできません。

文字を扱うための型なので、整数を扱うつもりで使うべきでないことは char型と同様です。

これらの型で文字を扱う方法は、char型とは違いがあります。このページは整数型がテーマなので、ここではこれらの型については深入りしないことにします。

整数リテラルの型 🔗

整数リテラルの型のルールを取り上げておきます[5] [6]

ここで示すルールは、基数を指定するプリフィックス(まだ解説していません)を付けていない場合の話です。

まず、100 とか -100 のように、サフィックスがない場合は、int -> long int -> long long int -> 拡張符号付き整数型の順番に、その値を表現できる型を探して、最初にみつけた型が選択されます。いずれでも表現できない場合はコンパイルエラーです。

100L-100L のように、Lサフィックスが付加されている場合は、long int -> long long int -> 拡張符号付き整数型の順番に、その値を表現できる型を探して、最初にみつけた型が選択されます。いずれでも表現できない場合はコンパイルエラーです。Lサフィックスは、小文字と大文字のいずれを使っても構いませんが、l1 と見間違いやすいため、L を勧めます。

auto li = 100000L;   // long
auto li = 100000l;   // long (1000001 のようにみえるので勧めない)

100LL-100LL のように、LLサフィックスが付加されている場合は、long long int -> 拡張符号付き整数型の順番に、その値を表現できる型を探して、最初にみつけた型が選択されます。いずれでも表現できない場合はコンパイルエラーです。LLサフィックスは、小文字と大文字のいずれを使っても構いませんが、ll111l と見間違いやすいため、LL を勧めます。

auto lli = 10'000'000'000LL;   // long long
auto lli = 10'000'000'000ll;   // long long (10'000'000'011 や 10'000'000'001L と間違えやすいので勧めない)

いずれの場合でも、Uサフィックスが付加されている場合は、対応する符号無し整数型の中から選択されます。

auto ui = 1000U;               // unsigned int
auto uli = 100000UL;           // unsigned long
auto uli = 100000LU;           // unsigned long
auto ulli = 10'000'000'000ULL; // unsigned long long
auto ulli = 10'000'000'000LLU; // unsigned long long

signed char や short に対応するサフィックスがないことに気付かれたかもしれませんが、そのようなサフィックスは存在しません。これがないと困るという場面はありません。たとえば、次の変数宣言はコンパイルできます。

signed char n {100};  // OK. int から縮小変換しているはずだが

100 は int なので、int から signed char への縮小変換になっており、リスト初期化の構文は縮小変換を許さないはずです。しかし、整数リテラルを初期化子にしているだけでエラー扱いされるのは非常に面倒ですから、例外的に許可されています[7]

強いていえば、signed char や short には型推論させられないことが気になるかもしれません。auto n = 100; は当然 int になります。

sizeof 🔗

sizeof演算子 (sizeof operator) を使うと、型の大きさをバイト数で調べられます。

sizeof演算子の使い方には以下のパターンがあります。

sizeof(型名)
sizeof()
sizeof

【上級】もう1つ、可変引数テンプレートで、パラメータパックに含まれる要素数を調べるパターンが存在します。[8]

「型名」を指定した場合、その型の大きさが得られます。

「式」を指定した場合、その式の結果の型の大きさが得られます。このとき、その式は実際には評価されません。たとえば、int の戻り値を返す関数f があるとき、sizeof(f()) は、戻り値の型 int の大きさを得られますが、関数f が呼び出されるわけではありません。同じ理由で、sizeof(a++) では a の値は増えません。

sizeof演算子によって得られる値の型は std::size_tです。std::size_t は標準ライブラリで定義された符号無し整数型に対する別名です。これ自体の大きさは処理系定義です。std::size_t を使うためには #include <cstddef> が必要ですが、わざわざ記述せずとも使えることが多いです。


sizeof演算子を使って、各種整数型の大きさを調べてみます。

#include <iostream>

int main()
{
    std::cout << "sizeof(char) == " << sizeof(char) << "\n"
              << "sizeof(signed char) == " << sizeof(signed char) << "\n"
              << "sizeof(unsigned char) == " << sizeof(unsigned char) << "\n"
              << "sizeof(short) == " << sizeof(short) << "\n"
              << "sizeof(unsigned short) == " << sizeof(unsigned short) << "\n"
              << "sizeof(int) == " << sizeof(int) << "\n"
              << "sizeof(unsigned int) == " << sizeof(unsigned int) << "\n"
              << "sizeof(long) == " << sizeof(long) << "\n"
              << "sizeof(unsigned long) == " << sizeof(unsigned long) << "\n"
              << "sizeof(long long) == " << sizeof(long long) << "\n"
              << "sizeof(unsigned long long) == " << sizeof(unsigned long long) << "\n"
              << "sizeof(bool) == " << sizeof(bool) << "\n";
}

実行結果:

sizeof(char) == 1
sizeof(signed char) == 1
sizeof(unsigned char) == 1
sizeof(short) == 2
sizeof(unsigned short) == 2
sizeof(int) == 4
sizeof(unsigned int) == 4
sizeof(long) == 4
sizeof(unsigned long) == 4
sizeof(long long) == 8
sizeof(unsigned long long) == 8
sizeof(bool) == 1

char、signed char、unsigned char の大きさは必ず 1 です。ほかの型の大きさはすべて処理系定義であり、この実行結果とは異なるかもしれません。

構造体型の大きさを調べることも可能ですし、構造体型に含まれている特定のデータメンバの大きさだけを調べることもできます。後者は、スコープ解決演算子を用います。

#include <iostream>

struct S {
    int v1;
    short v2;
};

int main()
{
    std::cout << sizeof(S) << "\n"
              << sizeof(S::v1) << "\n"
              << sizeof(S::v2) << "\n";
}

実行結果:

8
4
2

int が 4バイト、short が 2バイトなのに、構造体全体で 8 であることに疑問を持たれたかもしれません。構造体全体の大きさが、すべてのデータメンバの大きさの合計よりも少し大きくなるのは、構造体のデータメンバの隙間や末尾に、パディング(詰め物)と呼ばれる余分な領域が作られることがあるためです。

【上級】構造体型がデータメンバを1つも持たない場合でも、全体の大きさは 1 になります。型として使える以上、メモリアドレスが割り当てられなければならないためです。

std::vector<> や std::string の大きさを調べることもできますが、要素数が増減しても、sizeof演算子で得られる結果は変わりません。要素数に応じて、要素の大きさの合計を知りたいのであれば、要素の型の大きさに要素数を掛け合わせてください。要素の型は、std::vector<>::value_typestd::string::value_type です。

#include <iostream>
#include <string>
#include <vector>

int main()
{
    std::cout << "vector\n";
    std::vector<int> v(100);
    std::cout << sizeof(v) << "\n"
              << sizeof(std::vector<int>::value_type) * v.size() << "\n";
    v.insert(std::begin(v), {0, 1, 2, 3, 4});
    std::cout << sizeof(v) << "\n"
              << sizeof(std::vector<int>::value_type) * v.size() << "\n";

    std::cout << "string\n";
    std::string s(100, 'x');
    std::cout << sizeof(s) << "\n"
              << sizeof(std::string::value_type) * s.length() << "\n";
    s += "abcde";
    std::cout << sizeof(s) << "\n"
              << sizeof(std::string::value_type) * s.length() << "\n";
}

実行結果:

vector
16
400
16
420
string
28
100
28
105

列挙型の大きさは、その基底型の大きさです。

参照型の大きさは、参照元の型の大きさです。たとえば、sizeof(long&)sizeof(long) と同じ結果になります。

標準ライブラリの整数型 🔗

int などの標準整数型の大きさは処理系定義なので、確実にある大きさでなければならないときに困ります。そこで、以下の例のように、型の別名を定義しておき、処理系に応じて元になる型を修正する手段を採ることがあります。

// 確実に 32ビットな整数型が欲しい

// int が 32ビットの処理系では以下のように定義
using s32 = int;
using u32 = unsigned int;

// int は 16ビット、long が 32ビットの処理系なら以下のように修正する
using s32 = long;
using u32 = unsigned long;

これと同じ考え方をして、各種の整数型の別名が標準ライブラリに追加されています。#include <cstdint> によって、以下の型が使用できるようになります。

型名 意味 備考
std::intN_t Nビットの符号付き整数型 実装されていない可能性がある
std::uintN_t Nビットの符号無し整数型 実装されていない可能性がある
std::int_leastN_t 少なくとも Nビット以上である最小の符号付き整数型
std::uint_leastN_t 少なくとも Nビット以上である最小の符号無し整数型
std::int_fastN_t 少なくとも Nビット以上であり、もっとも高速に演算が行える符号付き整数型
std::uint_fastN_t 少なくとも Nビット以上であり、もっとも高速に演算が行える符号無し整数型
std::intmax_t 処理系が扱える最大の符号付き整数を表現できる型
std::uintmax_t 処理系が扱える最大の符号無し整数を表現できる型

それぞれが、標準整数型や拡張整数型の別名として定義されています。型名の「N」の部分には、8、16、32、64 を指定できます。つまり、std::intN_t という型があるのではなく、std::int8_t とか std::int16_t といった型があります。

肝心の std::intN_t と std::uintN_t に関しては、処理系によっては実装されていない可能性があります。実際のところ、主要な処理系でこれらの型が使えないことはまずありませんが、一応注意しておかなければなりません。

【上級】定義されていないことがあるのは、1バイトが 8ビットでない処理系がごくわずかに存在するからです。そのような処理系では、32ビットや 64ビットといった固定された大きさの型を定義することができない、あるいは非常に非効率になってしまいます。このような事情に配慮しているのであって、実装が可能なのであれば必ず実装しなければならないことになっています。[9]

新C++編では、どうしても必要な場面でないかぎり、<cstdint> で定義されている型は使用せず、int などの標準整数型を使うことにします。

整数拡張 🔗

int よりもランクの低い整数型に対して演算を行うときには、演算に先立って、int以上のランクの型に暗黙的に変換されるルールがあります。このルールを、整数拡張(整数のプロモーション) (integral promotion) と呼びます。

ランクとは型の順位を定義したものでした(「符号無し整数」のページを参照)。以下の表で上にあるものほどランクが高くなります[10]

  1. long long int、unsigned long long int
  2. long int、unsigned long int
  3. int、unsigned int
  4. short int、unsigned short int
  5. char、signed char、unsigned char
  6. bool

拡張整数型は、同じ大きさの標準整数型よりも低いランクになります。同じ大きさの拡張整数型が複数定義されている場合の扱いは、処理系定義です。

【上級】char16_t、char32_t、wchar_t は、それらの基底型に応じたランクになります。

整数拡張の対象になるのは、int よりもランクが低い型なので、この表で 4~6 にあたる型です。

演算対象の型が bool の場合は必ず int に変換され、true が 1 に、false が 0 になります[11]

演算対象の型が char、signed char、unsigned char、short int、unsigned short int の場合は、対象の型で表現できる値のすべてを int でも表現できるのなら int型に、表現できない値があるのなら unsigned int型に変換されます[12]たとえば、対象の型が 16ビットの unsigned short で、int型が 32ビットだとすると、unsigned short で表現できる値(0~65535) はすべて、int でも表現できるので int に変換されます。unsigned short も int も 16ビットの処理系では、int の表現範囲は -32768~32767 でしょうから、32768~65535 の範囲が表現できないため、unsigned int に変換されます。

【上級】演算対象の型が unscoped enum の場合、unscoped enum 自体は整数型ではありませんが、整数拡張の対象になります。基底型が指定されていなければ、int、unsigned int、long、unsigned long、long long、unsigned long long、拡張整数型の順番で、すべての列挙子の値が収まる型への整数拡張が起こります[13]。基底型が指定されている場合はまずその型に変換されますが、その型が int よりも低いランクの場合はさらに整数拡張が適用されます。[14]

整数拡張には、演算途中で型の限界値を超えてしまい、正しい結果が得られないことを防ぐ意味があります。たとえば、signed char 同士の乗算の結果は signed char では表現できない可能性が高いですが、計算前に int に拡張されるため問題なく計算できます。

異なる整数型の混在

オペランドが2つある演算子で、それぞれの型が異なる整数型の場合には、両者の型を揃える型変換を行ってから、演算が行われます。この型変換は、通常の算術変換 (usual arithmetic conversions) と呼ばれる以下の流れにしたがって行われます[15]

  1. 一方のオペランドが浮動小数点型なら、他方をその型に変換する
  2. 両方のオペランドに、整数拡張をおこなう
  3. この時点で両方の型が同じなら終了。異なっていたら、以下を順番に判定し、該当したところで終了する
  4. 両方とも signed、あるいは両方とも unsigned である場合、ランクが低い側が、ランクが高い側の型に変換される
    • 例)long int + int -> long int + long int
    • 例)unsigned int + unsigned long int -> unsigned long int + unsigned long int
  5. unsigned の側のランクが signed の側のランク以上の場合、signed の側が unsigned の側の型に変換される
    • 例)int + unsigned int -> unsigned int + unsigned int
    • 例)int + unsigned long int -> unsigned long int + unsigned long int
  6. signed の側の型が、unsigned の側の型で表現できるすべての値を表現できるのなら、unsigned の側が signed の型に変換される
    • 例)long long int (64bit) + unsigned int (32bit) -> long long int + long long int
  7. 両方とも、signed の側の型に対応する unsigned な型に変換される
    • 例)long int (32bit) + unsigned int (32bit) -> unsigned long int + unsigned long int

実際のコード例を確認してみましょう。

#include <iostream>

int main()
{
    int a {100};
    unsigned char b {50};
    long int c {100000};

    auto a_b = a * b;  // int * int
    std::cout << a_b << "\n";

    auto a_c = a * c;  // long int * long int
    std::cout << a_c << "\n";

    auto b_c = b * c;  // long int * long int
    std::cout << b_c << "\n";
}

実行結果:

5000
10000000
5000000

a * b は、int と unsigned char の乗算です。unsigned char は int よりランクが低い型なので、整数拡張が行われて、int に変換されます。int 同士での計算になるので、それ以上の型変換は起こりません。

a * c は、int と long int の乗算です。整数拡張は不要ですが、型は異なっているので、型を揃える変換が起こります。上述のリストにあった「両方とも signed、あるいは両方とも unsigned である場合、ランクが低い側が、ランクが高い側の型に変換される」が適用されて、両方とも long int になります。

b * c は、unsigned char と long int の乗算です。b は整数拡張されて int になります。そのため int と long int の乗算になり、さきほどと同様に、両方とも long int になります。

表現できない値を与えたとき 🔗

符号無し整数」のページでの説明と同じことではありますが、初期化や代入などで与えようとした値が、相手側の整数型で表現できない場合の結果についてあらためて取り上げます。この場合の結果は、相手側の型の符号の有無によって異なります。

相手側が符号無し整数型の場合は、表現できない上位部分のビットが捨てられます[16]たとえば、16ビットの符号無し整数型に、32ビットの整数を渡すと、上位の 16ビットが捨てられます。情報の一部を失っているわけなので、当然危険性はありますが、動作は一貫しています。

一方、相手側が符号付き整数型の場合は、処理系定義です[17]処理系によって動作が異なるので、移植性を考えると、表現できない値を渡さないほうがいいということになります。

まとめ 🔗


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク 🔗


練習問題 🔗

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

各種の標準整数型で扱える値の範囲を、std::numeric_limits<> を使って確認してみてください。

解答・解説

問題2 (確認★)

<cstdint> で定義されている型について、sizeof演算子がどのような大きさを返すか調べてみてください。

解答・解説

問題3 (確認★)

次の変数 n1~n8 の型はそれぞれ何になりますか?

signed char sc {-10};
unsigned short us {10};

auto n1 = 10L;
auto n2 = 10LL;
auto n3 = 10LU;
auto n4 = 10 + n1;
auto n5 = n2 + n3;
auto n6 = sc * 2;
auto n7 = sc * us;
auto n8 = sc + 100LL / 2U;

解答・解説

問題4 (基本★★)

トランプのマークをあらわす列挙型は、4通りの値が表現できれば十分なので、基底型を short や signed/unsigned char にできます。int の場合と比べて、メモリの使用量にどれだけ違いが出るでしょうか?

解答・解説


解答・解説ページの先頭



更新履歴 🔗




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る