Y - 3.01.数値型 Editorial /

Time Limit: 0 msec / Memory Limit: 0 KB

前のページ | 次のページ

キーポイント

  • 整数型には扱える値の範囲が決まっている
  • 計算の途中で扱える範囲を超えることをオーバーフローといい、正しく計算が行えなくなる
  • int型より大きい値を扱いたいときはint64_t型を使う
  • double型の値を出力する際に出力する小数点以下の桁数を指定するには以下のようにする
cout << fixed << setprecision(桁数);
  • (型)値で型変換(キャスト)を行うことができる
  • 暗黙的にキャストが起こる場合もある

数値型

今回は整数型について扱いますが、厳密な挙動は環境によって異なることがあります。 このページに書かれていることはAtCoder上で「C++ (GCC 9.2.1)」として実行することを想定した内容となっています。

int64_t型

今まで整数を扱う場合はint型を使ってきましたが、int型が表せる範囲は以下の通りに限られています。

  • 最小値:-2147483648
  • 最大値:2147483647

計算の途中でこの範囲を超えてしまうことをオーバーフローと言います。オーバーフローすると正しく計算が行えなくなります。

#include <bits/stdc++.h>
using namespace std;
int main() {
  int a = 2000000000;
  int b = a * 2;
  cout << b << endl;

  int c = (a * 10 + 100) / 100;
  cout << c << endl;
}
実行結果
-294967296
-14748363

2000000000 * 2の結果として4000000000が出力されてほしいところですが、-294967296という値が出力されてしまっています。
また、(2000000000 * 10 + 100) / 100の結果は正しく計算できていれば200000001となるので、オーバーフローせずに済むように思えますが、2000000000 * 10の時点で計算結果がint型の範囲を超えてしまうので、最終的な計算結果も正しくない-14748363という値が出力されています。

もっと広い範囲の値を扱いたい場合、int64_t型を使うのが一般的です。
int64_t型で扱える値の範囲は以下のとおりです。

  • 最小値:-9223372036854775808
  • 最大値:9223372036854775807
#include <bits/stdc++.h>
using namespace std;
int main() {
  int64_t a = 2000000000;
  int64_t b = a * 2;
  cout << b << endl;
}
実行結果
4000000000

int型は20億(2 * 10^9)くらいまでと覚えておき、それより大きな値が出てきそうであればint64_t型を使うようにしましょう。
int64_t型よりも大きな値を扱う方法もありますが、ここでは説明しません。気になる人は調べてみてください。

プログラム中に直接10のように値を書くと、それはint型の数値として扱われます。
int64_t型として扱ってほしい場合は10LLのように末尾にLLをつけましょう。

また、int型とint64_t型の計算結果はint64_t型になります。

#include <bits/stdc++.h>
using namespace std;
int main() {
  cout << 2000000000 * 2 << endl;     // int * int         -> int
  cout << 2000000000LL * 2LL << endl; // int64_t * int64_t -> int64_t
  cout << 2000000000LL * 2 << endl;   // int64_t * int     -> int64_t
}
実行結果
-294967296
4000000000
4000000000

double型の出力

double型を出力する場合、通常通りcoutで出力してしまうと適当な桁で四捨五入されて表示されてしまいます。
途中の桁まで四捨五入せずに確実に表示したい場合、以下のように書きます。

cout << fixed << setprecision(桁数);
#include <bits/stdc++.h>
using namespace std;

int main() {
  cout << 3.14159265358979 << endl;

  // 小数点以下10桁まで
  cout << fixed << setprecision(10);

  cout << 3.14159265358979 << endl;
}
実行結果
3.14159
3.1415926536

明示的な数値型同士の変換

int型とint64_t型の計算結果がint64_t型であったり、int型とdouble型の計算結果がdouble型であったりするように異なる型同士でも計算できることがありました。
計算せずに、ただ型の変換だけを行うこともできます。型変換のことをキャストと言います。

C++には様々なキャスト方法がありますが、ここでは最も原始的なものを紹介します。
キャストの記法は以下のとおりです。

(型)値

以下は数値型を変換する例です。

#include <bits/stdc++.h>
using namespace std;

int main() {
  // 小数点以下5桁まで
  cout << fixed << setprecision(5);

  int a = 5;
  cout << (double)a << endl; // int型の値をdouble型に変換

  double b = 3.141592;
  cout << (int)b << endl; // double型の値をint型に変換
}
実行結果
5.00000
3

1行目の出力では、int型の値をdouble型にキャストしたことで、出力時に小数点以下の数も表示されています。
2行目の出力では、double型の値をint型にキャストしたことで、小数点以下が切り捨てられて整数として表示されています。
double型をint型にキャストすると小数点以下切り捨てになることは重要なので覚えておきましょう。

int型とdouble型に限らず、様々な型同士がキャストできます。
ただし、int型からstring型への変換のように、数値型以外への変換は基本的にはできないということに注意してください。

int N = 12345;
string S = (string)N;  // コンパイルエラー

暗黙的な数値型同士の変換

計算の際に自動的にキャストが起こることもあります。

異なる数値型同士で計算を行う場合、以下の表のようにキャストされてから計算されます。

型の組合せ 暗黙的なキャスト 計算結果の型
int型とdouble型 int型→double型 double型
int型とint64_t型 int型→int64_t型 int64_t型
double型とint64_t型 int64_t型→double型 double型

また以下のような場合にも自動的にキャストが起こります。

  • 別の型の変数へ代入しようとした場合
  • 関数の引数に別の型の値を渡す場合

次のプログラムは暗黙的なキャストが生じる例です。

#include <bits/stdc++.h>
using namespace std;

void print_int(int x) {
  cout << "x = " << x << endl;
}

int main() {
  double a = 1.2345;
  int b = 1;

  // aがdouble型に変換されてからbとの足し算が行われる
  // cout << a + (double)b << endl; と同じ結果
  cout << a + b << endl;

  int c = 2000000000;
  int64_t d = 100;
  // 初めにcがint64_t型へ変換されてからdとの掛け算が行われる
  // cout << (int64_t)c * d << endl; と同じ結果
  cout << c * d << endl;

  double e = 3.141592;
  int f = e;  // ここでdouble型からint型への変換が起こる(小数点以下が切り捨てられて3になる)
  cout << f << endl;

  // int型を引数に取る関数にdouble型を渡す
  print_int(e);  // int型に変換されてから関数が実行される
}
実行結果
2.2345
200000000000
3
x = 3

なお、上の条件を満たす場合でも、キャストが行えない場合はコンパイルエラーとなります。

#include <bits/stdc++.h>
using namespace std;

void print_int(int x) {
  cout << "x = " << x << endl;
}

int main() {
  string s = "12345";

  int x = s;     // コンパイルエラー
  print_int(s);  // コンパイルエラー
}

関数の引数に別の型の値を渡す場合でも、STLのmin関数やmax関数のように暗黙的な型変換が行われない関数もあるという点に注意してください。 詳しくは4章の「テンプレート」で扱います。


細かい話

double型の精度と誤差

double型の内部について簡単に説明します。 double型では小数を扱うために「数字の並び」と「小数点の位置」を持っています。 そして、「数字の並び」で保持できる桁数はint型などと同じように上限があります。 double型では約16桁の並びを保持できます。逆に16桁を超える小数を正確に扱うことはできません。

表現できる桁数に限りがあるので、計算を行ったときに誤差が生じることがあります。

実際には精度を気にしなければならない場面は少ないかもしれませんが、ここでは情報落ちと桁落ちについて説明します。

情報落ち

差が極端に大きい2つの数を足し算することを考えます。 次のプログラムをみてください。

#include <bits/stdc++.h>
using namespace std;

int main() {
  double x = 1000000000;
  double y = 0.000000001;

  // 1000000000.000000001 を表現するには19桁分必要 → 扱えない
  double z = x + y;  // yの分が消えてしまう

  cout << fixed << setprecision(16);
  cout << "x: " << x << endl;
  cout << "y: " << y << endl;
  cout << "z: " << z << endl;
}
実行結果
x: 1000000000.0000000000000000
y: 0.0000000010000000
z: 1000000000.0000000000000000

足す数それぞれがdoubleの精度で扱うことができても、それを足した後の値が表現できないことがあり、 そのような場合には桁が消えてしまいます。 このような現象を情報落ちといいます。

なるべく小さい数から順に足し算を行うことで、情報落ちの影響を減らすことができます。

桁落ち

差が極端に小さい2つの数を引き算することを考えます。

x, yは本来無限に桁が続く小数とします。 これらをコンピュータで扱おうとしたときに6桁までしか表現できず、それ以降の桁は0になってしまっているとします。

double型を用いた場合は約16桁まで扱えますが、説明のために6桁までしか表現できないとしています。

実際の値 コンピュータ上での値
x 0.1234567890123... 0.123456
y 0.1233456789012... 0.123345

ここで、z = x - yを考えます。

実際の値 コンピュータ上での値
z = x - y 0.0001111111111... 0.000111

zの真の値は0.0001111111111...ですが、計算した結果は0.000111となっています。 5桁までの精度の数で計算を行ったにも関わらず、計算結果は3桁(3つの数字の並び)の精度になってしまっていることが分かります。

このように差が極体に小さい数同士の引き算を行うことによって値の精度が落ちてしまうことを桁落ちといいます。

桁落ちによって精度が落ちたまま計算を続けてしまうと最終的な計算結果の誤差が大きくなってしまうことがあります。

引き算を行う際にはなるべく差が大きくなるように計算順序を工夫したり、そもそも引き算をなるべく行わないように工夫する必要があります。

printfでの出力

printfという関数を呼ぶことでも出力することができます。

精度指定を行う場合やたくさんの値を出力する場合にはcoutよりもprintf関数を使う方が便利です。

#include <bits/stdc++.h>
using namespace std;

int main() {
  int x = 12345;
  double pi = 3.14159265358979;
  printf("x = %d, pi = %lf\n", x, pi);
}
実行結果
x = 12345, pi = 3.141593

printfの使い方は次の通りです。

printf(フォーマット文字列, 埋め込みたい値1, 埋め込みたい値2, ...)

「フォーマット文字列」の部分は後で説明するフォーマット指定子を含む文字列を自由に指定できます。 フォーマット指定子を書いた部分に「埋め込みたい値」が順番に埋め込まれます。

上のサンプルプログラムでは"x = %d, pi = %lf\n"と指定しています。 %d%lfの部分がフォーマット指定子です。 1つ目の%dの部分にはxの内容が埋め込まれ、2つ目の%lfの部分にはpiの内容が埋め込まれます。 なお、\nは改行を意味します。

フォーマット指定子

埋め込みたい値の型によって対応するフォーマット指定子を使う必要があります、代表的なものは次の表のようになっています。

フォーマット指定子
int %d
int64_t %ld%lld
double %lf
char %c

32ビットシステム上でint64_tprintfで出力する場合、フォーマット指定子は%lldとする必要があります。

なお、string型の文字列を出力する場合、次のように書く必要があります。

#include <bits/stdc++.h>
using namespace std;
int main() {
  string s = "hello";
  // フォーマット指定子 %s を用い s.c_str() を渡す
  printf("%s\n", s.c_str());
}
実行結果
hello

さらにフォーマット指定子の部分を変更することで出力する小数の桁数などの出力の仕方を変更することができます。

#include <bits/stdc++.h>
using namespace std;
int main() {
  double pi = 3.14159265358979;
  // %.桁数f とすると小数点以下「桁数」だけ出力される
  printf("%.10lf\n", pi);

  int x = 1;
  int y = 99;
  int z = 123;
  // %0桁数d とすると表示桁数が「桁数」に満たない場合に0埋めされる
  printf("%03d, %03d, %03d\n", x, y, z);
}
実行結果
3.1415926536
001, 099, 123

printfにはここで紹介していない機能がたくさんあります。詳しく知りたい人は調べてみてください。

scanfでの入力

scanfという関数で入力を受け取ることもできます。

#include <bits/stdc++.h>
using namespace std;

int main() {
  int x;
  double pi;
  // scanfによる入力
  scanf("x = %d, pi = %lf", &x, &pi);

  printf("x = %d, pi = %lf\n", x, pi);
}
入力
x = 12345, pi = 3.141593
実行結果
x = 12345, pi = 3.141593

scanfの使い方は次のとおりです。

scanf(フォーマット文字列, &変数1, &変数2, &変数3, ...);

引数に渡す変数の前に&が必要なことに注意してください。

この記号の意味については「4.05.ポインタ」で扱います。

フォーマット文字列は基本的にはprintfのものと同じです。

ただし、string型の文字列を直接入力することはできません。 この場合はcinを使ってください。

空白文字の扱い

%dなどのフォーマット指定子を指定した場合、直前に連続する空白文字を読み飛ばします。空白文字とはスペース()、改行文字(\n)、タブ文字(\t)などのことです。

以下のような呼び出しに対し、123 456という文字列を入力した場合、xは123となり、yは456となります。

scanf("%d%d", &x, &y);

文字列との変換

to_string関数

数値型から文字列に変換するには、to_string関数を用います。

to_string関数は、引数の数値をstring型に変換します。

#include <bits/stdc++.h>
using namespace std;

int main() {
  int number = 100;
  string s = to_string(number);
  cout << s + "yen" << endl;
}
実行結果
100yen

stoi関数

文字列からint型に変換するには、stoi関数を用います。

#include <bits/stdc++.h>
using namespace std;

int main() {
  string s = "100";
  int n = stoi(s);
  cout << n << endl;
}
実行結果
100

文字列から他の数値型への変換はそれぞれの型に対応する関数を用います。 対応表を次に示します。

文字列からの変換関数
int stoi
int64_t stoll
double stod

これらの関数の使い方はstoi関数と同じです。

その他の型専用の関数も用意されていますが、基本的にint64_t型かdouble型にしてからキャストすればよいです。

他の数値型

int型やdouble型以外にも様々な数値型が用意されています。 これらは扱える値の範囲や精度、使用するメモリの量が異なります。

整数型

主な符号付き整数型

扱える値の範囲 使用するメモリ量
signed char -2^{7} 〜 2^{7}-1 1 byte (8 bit)
short -2^{15} 〜 2^{15}-1 2 byte (16 bit)
int -2^{31} 〜 2^{31}-1 4 byte (32 bit)
long long -2^{63} 〜 2^{63}-1 8 byte (64 bit)
int8_t -2^{7} 〜 2^{7}-1 1 byte (8 bit)
int16_t -2^{15} 〜 2^{15}-1 2 byte (16 bit)
int32_t -2^{31} 〜 2^{31}-1 4 byte (32 bit)
int64_t -2^{63} 〜 2^{63}-1 8 byte (64 bit)

主な符号無し整数型

扱える値の範囲 使用するメモリ量
unsigned char 0 〜 2^{8}-1 1 byte (8 bit)
unsigned short 0 〜 2^{16}-1 2 byte (16 bit)
unsigned int 0 〜 2^{32}-1 4 byte (32 bit)
unsigned long long 0 〜 2^{64}-1 8 byte (64 bit)
uint8_t 0 〜 2^{8}-1 1 byte (8 bit)
uint16_t 0 〜 2^{16}-1 2 byte (16 bit)
uint32_t 0 〜 2^{32}-1 4 byte (32 bit)
uint64_t 0 〜 2^{64}-1 8 byte (64 bit)

int??_t系やuint??_t系以外については、環境によっては上の表と異なることがあります。

使用するメモリについては、たとえば「long long型は64bitなので、変数1つに対してint型変数2つ分のメモリが必要」のように考えてください。

たくさんあってどう使い分けるのか分からなくなるかもしれませんが、 int??_t系やuint??_t系を使うことをおすすめします。 極端な話ですが、使用するメモリ量をそこまで気にしないのであれば全てint64_t型かuint64_t型を使ってしまうという手もあります。

実数型

扱える値の範囲(絶対値) 扱える精度(10進数) 使用するメモリ量
float 1.17×10^{-38} 3.40×10^{38} 約8桁 4 byte (32 bit)
double 2.22×10^{-308} 1.79×10^{308} 約16桁 8 byte (64 bit)

環境依存の仕様

環境によって扱える範囲が変わる整数型としてlongがあります。 longの扱える範囲は次の表のとおりです。

Windows Linux mac OS X
32ビットシステム int32_tと同じ int32_tと同じ int32_tと同じ
64ビットシステム int32_tと同じ int64_tと同じ int64_tと同じ

AtCoder上で実行した場合は、longが扱える範囲はint64_tと同じになります。

プログラム中に直接書いた数値をlong型として扱うには、10Lのように末尾に1つのLをつけて書きます。

AtCoder上ではint64_tlong型として扱われるため、厳密にはプログラム中に直接書いた数値をint64_tとして扱いたい場合には10Lのようにlong型に合わせて書いた方が正確ですが、他のシステム上でも同様に動くようにするために、「int64_t型として扱うためにはLLと書く」と説明しています。

size_t

配列や文字列のsize()で返ってくる要素数の値はsize_tという符号なしの整数型です。 C++ (GCC 9.2.1)においてsize_tunsigned long(単なる別名)として定義されています。

実行環境によって、for文でfor (int i = 0; i < 配列.size(); i++)と書いたときに警告が出るのは、int型(符号付き)とsize_t型(符号無し)を比較しているためです。 しかし、暗黙的にキャストが行われるので動作には影響ありません。

符号なし整数のオーバーフロー

符号なし整数は負の数を扱えないため、例えばunsigned char型の0から1を引くと255となってしまいます。

size_t型は符号なしの整数型であるため、次のようなプログラムは意図した挙動になりません。

#include <bits/stdc++.h>
using namespace std;

int main() {
  vector<int> data(0);  // サイズ0
  cout << data.size() - 1 << endl;  // -1ではない

  // 配列のサイズ-1回だけループしたい
  for (int i = 0; i < data.size() - 1; i++) {
    cout << i << endl;
  }
}
実行結果の例
18446744073709551615
0
1
2
3
4
5
6
7
8
(省略)

この例では、ループ回数のdata.size() - 1がオーバーフローして18446744073709551615という値になってしまっています。

配列のサイズ-1回だけループしたい場合は、次のようにsize()の結果を符号付きにキャストする必要があります。

// 配列のサイズ-1回のループ(ただし、空の場合はループ内は実行されない)
for (int i = 0; i < (int)配列.size() - 1; i++) {
  // 適当な処理
}

演習問題

今回は演習問題はありません。

次のAtCoder Beginner Contestの問題を解いてみてください。

前のページ | 次のページ