はじめに
今回はISO/IEC9899というC言語の規格を記している資料から型の正しい定義について学んでみる
なぜ今さら型について学ぶのか
移植性のあるコードを書くためです。
「long型が実行環境によって32bitなのか64bitなのか変わる」という話は聞いたことがあると思います。ただ僕はこの話を聞いてから「それぞれの型の"正しい定義"を知らないと移植性のあるコードは書けない」と、前々から感じていました。
なぜなら環境によってその"正しい定義"は守られているはずだからです。
逆にいえばそれ以外はその環境が自由に決めていると考えた方が良いのかもしれません。
型
型は以下のように定義されています
(原文)
Types are defined in the following categories:
— integer types having certain exact widths;
— integer types having at least certain specified widths;
— fastest integer types having at least certain specified widths;
— integer types wide enough to hold pointers to objects;
— integer types having greatest width.
(和訳)
型は以下のカテゴリで定義されます。
ー ある確かな幅を持つ整数型
ー 少なくとも確かな特定の幅を持つ整数型
ー 少なくとも確かな特定の幅を持つ最速の整数型
ー オブジェクトへのポインタを保持するのに十分な幅を持つ整数型
ー 最大の幅を持つ整数型
なんとなくC言語を学んできた人であれば納得できると思います。
ここから実際に僕らがよく使う整数型の定義を見ていきます。
整数型
bool型
数値型と言って最初にbool型を紹介するのは違和感があるかもしれません。ですが、bool型はfalse(0)とtrue(0以外)を扱う型なので整数型に属します。
そしてbool型は
「0と1を格納するのに十分な大きさを持つ整数型」
と定義されています。
なので環境によっては0か1かしか表さないことがあるということです。
char型
char型は文字型だ、という意見があると思います。ですがchar型は数値を格納してそれに対応する文字を出力することもできるので文字型でありながら数値型に属しています。
char型は
「基本実行文字セットのメンバーを格納するのに十分な大きさを持つ整数型」
と定義されています。
ここでいう基本文字セットはASCIIからきています。
int型
int型は
「ヘッダで定義されているINT_MINからINT_MAXを格納するのに十分な、その実行環境で自然とされている大きさを持つ整数型」
と定義されています。
なので規格上ではint型はbit数が決まっていません。
しかし、limit.hについて
INT_MIN <= -(2^15 - 1) <= (2^15 - 1) <= INT_MAX
と定義されているのでint型は少なくとも16bit以上であることが保証されています。
long型、long long型
long = long int
long long = long long int
上記のように省略した形です。
これらについて定義はint型と同様です。
そしてlimits.hについて
LONG_MIN <= -(2^31 - 1) <= (2^31 - 1) <= LONG_MAX
LLONG_MIN <= -(2^63 - 1) <= (2^63 - 1) <= LLONG_MAX
と定義されているので、long型は32bit以上、long long型は64bit以上であることが保証されています。
signed、unsignedについて
それぞれ符号あり(signed)、符号なし(unsigned)の整数型を宣言するための修飾子です。
signedは付けなくてももともと整数型は符号ありなので変わりませんが、unsignedを付けると符号分の1bitを多く数値を表すのに使えるので表せる数値の絶対値の大きさが1bit分増えます。
INT_MAX -> 2^15 - 1
UINT_MAX -> 2^16 - 1
char型のみ特殊で
「char型はsigned char型またはunsigned char型のいずれかと同じ範囲で定義しなければなりません」
と書いてあります。つまりchar型だけはsigned char型と定義してあげないと符号なしの可能性があります。
また、型の最大値を超える計算について、signedであればオーバーフローが発生し未定義の動作(処理系に依存する)になりますが、unsignedだとその型の最大値+1を法として剰余演算が行われるのでオーバーフローが発生しません。
UINT_MAX -> 2^16 - 1 = 65535
65535 + 10 = (65535) ≡ 9(mod65536)
定義を知った上で...
型について知った上で実際にコードを書く時に意識すべきことは
文字型であるchar型を除いて
整数型はintN_tやuintN_tで表記すべき
ということだと思います。
また、char型はsigned char型で定義すべきです。
規格でも定められていますが、Nbitの整数型をintN_tと表記することができます。
なので32bitの整数型が欲しいときはint32_tと宣言してあげるのがより良いコードであると言えます。
また、それに伴うINTN_MAXも定義されています。
終わりに
小数型についても調べてみると面白そうです。
参考文献:https://www.open-std.org/jtc1/sc22/wg14/www/standards
Comments
intN_t/uintN_tはオプションなので、(規格的には)移植性があるとは言えないけどね( *´艸`)
そうですね!移植性というのは語弊があったかもしれないです。
自分の意図としては「intN_t型であればその処理系に存在するのならbit数がN、ないのならコンパイルエラーになるはずなので意図しない挙動が起こらない」ということです、ご指摘ありがとうございます!
intN_t
で整数型を表すようにしても気づくのが難しい移植性の問題が発生する事例として以下のようなものを見たことが有ります。バイト列を結合してひとつの整数を作るというもので、この場合は
0x89aa
を作ることが意図されているのですがint
が 16bit の環境では意図通りになりません。 プログラムの字面の上ではint
が現れないので気づき難いようです。もちろん基本的には整数型を
intN_t
で表すのは良い習慣だとは思いますが、 それだけではなくなるべく言語仕様を広く把握して慎重にやるしか仕方がないですね。確かに気をつけるべきは型名だけではないですね…
実際に規格を読んで開発している人はあまり見ないですが、自分は今回の記事が規格を読むきっかけになったので型以外の部分も読んでみようと思います!
コメントありがとうございます!
@Canard_engineer_c_cppさん、
ISO/IEC9899と言ってもANSI C - Wikipediaによると
C90 = ISO/IEC 9899:1990
C95 = ISO/IEC 9899/AMD1:1995
C99 = ISO/IEC 9899:1999
C11 = ISO/IEC 9899:2011
C17 = ISO/IEC 9899:2018
ということみたいですが、この記事が何について言及されてるかは明確にされると良いと思います。
まだ規格として決まっていないC23ではbool型が使えるようになりそうという話は聞いた気がするのですが、それ以前の規格ではboolは型ではなくマクロだったと思います。bool型ではなくC99で決めらた_Bool型のことを言われてるのではないでしょうか。
環境によっては標準規格で決められた_Bool型で0か1以外の値を表せられるということでしょうか?
ASCII以外の文字セットは規格外ということですか?
なんでですか?
今回参考にした規格はC99です、明記すべきでしたね
boolは_Boolのエイリアス(別名)なので混乱を避けるために触れませんでした(こちらも明記すべき)
また、環境によっては0か1以外の値を格納できます。
ASCII以外の文字セットは規格では明記されていませんが、各環境によって拡張文字セットとして採用されていることがあります。
記事本文にも書きましたが、char型だけはsignedと明記しないと符号付きであることが保証されないからです。
@Canard_engineer_c_cppさん、
投稿後も記事は編集できるので、後から気付いた点があれば記事は修正されると良いでしょう。
ISO/IEC 9899:1999を基礎としているX 3010:2003 (ISO/IEC 9899:1999)には
とあるので_Bool型のオブジェクトへは0か1以外格納できないと思います。
あるいは、こんな例を想定されているでしょうか?
これはX 3010:2003 (ISO/IEC 9899:1999)の6.2.6.1に
に該当し未定義動作となるので_Bool型のオブジェクトへはやはり0か1以外格納できないと思います。
規格の話をされてるところで機種に依存した話を持ち出される意味はないと思います。
X 3010:2003 (ISO/IEC 9899:1999)では5.2.1に
と説明があり、数字は文字コードが連続して並んでいることを規定していますがラテンアルファベットの大文字と小文字については文字コードが連続して並んでいることを規定してしていません。
規格がASCIIを想定していればラテンアルファベットも数字同様に文字コードが連続して並んでいることを規定できる筈ですがそうなってはいないということです。つまりはC言語の標準規格ではASCIIを規定しておらず、規格に準拠したC言語のプログラムであればASCIIを想定するのは間違いとなります。
C言語では
int
とsigned int
は同じ型ですが、char
とsigned char
とunsigned char
はそれぞれ別の型です。標準ライブラリの引数の型と合わせるためにchar
を使用する機会は普通にあるでしょう。符号付き、あるいは符号なしの小さい整数を使用したい場合にsigned char
やunsigned char
を使う場合もあると思います。これらは必要に応じて使い分けるべきものであり、char
は無条件でsigned char
へ置き換えるようなことではないと思います。_Bool型に1以外の値を代入して変換されることと、値の格納が“可能“かどうかというのは別の話ですね。
ASCIIは規定されているというより対応しているということですね。規定外の話をしても意味が無いというのは違います。今回は規定外の挙動が存在することを意識することでより規定に添うことが趣旨にあるので。
もちろん無条件でsigned charにすべきとは書いていません。確かに、“基本的に“なども書いていませんが。その辺りはこの記事を読んで頂いた方がどう考えて応用するかの部分です。
Let's comment your feelings that are more than good