C言語の基礎知識


1. 型

規格には、型をいくつかまとめた呼び方がいろいろでてくるが、ここでまとめておく。

C言語の型は、次の3種類に分類される。

型をいくつかまとめた呼び方として、他によく使われるのは、次のようなものである。

(signed,unsignedはs_,u_と略した)
            符号付き 符号無し
             整数型   整数型  文字型 汎整数型 算術型 スカラ型
s_char          O                O       O       O       O
short           O                        O       O       O
int             O                        O       O       O
long            O                        O       O       O
u_char                  O        O       O       O       O
u_short                 O                O       O       O
u_int                   O                O       O       O
u_long                  O                O       O       O
float                                            O       O
double                                           O       O
long double                                      O       O
char                             O       O       O       O
列挙型                                   O       O       O
ポインタ型                                               O
(文字型は3種類あることに注意。これらは別々の型である。)

問: C99で追加された、_Bool、long long、unsigned long longはどれに含まれるか調べよ。

汎整数型は、単項演算子~、二項演算子<<,>>,&,^,|,%、ポインタに加減する値、switch文の制御式などに使われる。配列宣言の[]の中やの式や、case名札の式は、汎整数定数式である。

算術型は演算子+,-,*,/などに使われる。

スカラ型は、キャスト演算子、演算子++,--,!,&&,||、if文の制御式、while文の制御式などに使われる。

型については、非修飾型(これまでに述べた型)と修飾版(constとvolatileの一方または両方を付けた型)の区別もある。


2. 定数の型

整数定数の型は、次の並びのうちでその値を表現できる最初の型になる。

問: C99ではどのように変更されたか調べよ(long long、unsigned long longの追加など)。

小数点と指数部の少なくとも一つがあるなら、浮動小数点定数である(0で始まっても8進数として解釈されない)。

問: 列挙定数として宣言された識別子の型は何か。

問: 単純文字定数の型は何か。(C++では異なる)

問: 単純文字列リテラルの型は何か。


3. 処理系が決める型

sizoef演算子の結果は、size_tという符号無し整数型である。

二つのポインタの減算の結果は、ptr_diff_tという符号付き整数型である(結果がptr_diff_tに収まらなかった場合の動作は未定義)。

Coinでは今のところ扱わないが、ワイド文字定数の持つ型、ワイド文字列リテラルに対する配列の要素の型は、wchar_tという汎整数型である。


4. 型変換

暗黙の型変換(implicit conversion)と明示的な型変換(explicit conversion)の規則については、規格の「型変換」の節をよく参照する必要がある。ここでは、重要な概念を指摘するにとどめる。

4.1 汎整数拡張(integral promotion)

intより“小さい”型を暗黙に、intか、表現できなければunsigned intに変換することである。値は変わらない。

4.2 符号付き、符号無し整数型同士の変換

規格ではわかりにくい表現になっているが、2の補数表現では、 と考えてよい(規格では、後二者は、変換後の型が符号付きで、変換前の値(規格には、変換後の値と書いてある)を表現できないなら、値は処理系定義となるが、Coinsでは上のように動作する処理系に限定することにしたい)。

4.3 浮動小数点型から汎整数型

小数部は捨てる。変換後の型で表現できないなら、動作は未定義。

4.4 汎整数型から浮動小数点型

処理系定義の丸め。

4.5 浮動小数点型同士の変換

拡張では値は変化せず、縮小では処理系定義の丸め。

4.6 通常の算術型変換(usual arithmetic conversion)

算術型のオペランドを持つ多くの2項演算子が行う、オペランドの型変換であり、結果の型も同じである。汎整数拡張後、次の順で決まる大きい型にそろえる。
long double, double, float, unsigned long, long, unsigned int, int
ただし、一つ例外がある。longとunsigned intの組合せで、longがunsigned intのすべての値を表現できないときに限り、unsigned longにそろえる。

問: C99ではどうなったか。

4.7 左辺値(lvalue)

オブジェクトを指し示す式のことで、オブジェクト型か、void以外の不完全型を持つ。変更可能な左辺値(modifiable lvalue)は、配列型でも、不完全型でもなく、(構造体や共用体の場合どのメンバも)const修飾されていない左辺値のことである。

4.8 配列型の暗黙の変換

型“〜型の配列”の左辺値は、3つの例外を除いて、型が“〜型へのポインタ”で、配列の先頭要素を指す式に変換される。3つの例外は、 である。

Coinsでは、この変換を表すために、HIRにdecay演算子を導入して、変換を明示する。

4.9 関数型の暗黙の変換

関数型の式は、2つの例外を除いて、関数へのポインタに変換される。関数呼び出し()の左オペランドは、関数へのポインタ型が要求されることに注意せよ。Coinsでは、この変換を何で表すかきちんと議論しなかったが、addrでよいのではないか。

4.10 空ポインタ(null pointer)

以下の二つは、空ポインタ定数と呼ばれる。 空ポインタ定数は、ポインタ定数に代入する場合と、ポインタと等値比較する場合に、その型のポインタに変換される。変換されたポインタが空ポインタである(空ポインタのビットパターンがオール0とは限らない)。

5. 2の補数表現について

Coinsでは、符号つき数の表現としては2の補数表現のみを扱うことにしているため、2の補数表現であることを前提とした、設計の簡略化や積極的な最適化が可能となる。そこで、2の補数表現についての重要な性質をいくつか述べる。

5.1 nビット数同士の和・差・積の下位nビットは、符号つき数の場合も符号なし数の場合も同じ

オーバーフローを考えず、入出力のビット数が同じであるような、加算、減算、乗算演算子は、符号つき、符号なしの区別が不要である。加減算については明らかであるから、乗算についてだけ証明しておく。

まず、nビット符号なし数aの最上位ビットをsとするとき、同じビット並びの符号つき数の値は、

a-2ns
であることに注意しておく。すると、nビット符号なし数a,bがあるとき、それぞれの最上位ビットをs,tとすれば、符号なしの積は、
ab
符号つきの積は、
(a-2ns)(b-2nt)
となる。後者を変形すると、
ab-2n(bs+at)+22n
となり、ab以外の項は下位nビットに影響を与えない。つまり、下位nビットはabであり、符号なしの場合と同じであることが示された。

5.2 乗除算をシフトに置き換える場合の注意

2の冪による乗算は、単純にシフトに置き換えることができる(上で示したように、符号つきかどうかの区別は不要)。2の冪による除算では、符号なしの場合は論理右シフトを使えばよいが、符号つきの場合は注意が必要である。

多くの処理系では(C99では必ず)、商は0方向に切り捨てられる。これは、算術右シフトの動作と異なるため、除算をシフトで置き換えるには、次のような補正が必要となる。

a/2n → (a>=0 ? a : a+2n-1)>>n

なお、(a/b)*b+a%bはaと等しくなければならないので、余りも補正が必要である。

a%2n → a-((a>=0 ? a : a+2n-1)&-2n)

5.3 大小比較結果と差の符号は必ずしも一致しない

nビットの数同士の差が2n-1以上あるとき、比較結果と、nビットの差の符号は異なる。すなわち、a>bとa-b>0は異なる。ただし、a=bとa-b=0は同じである。

5.4 符号反転でオーバーフローする値が一つある。

nビット符号つき数で表した-2n-1は、符号反転するとオーバーフローする。オーバーフローを無視するなら、同じ値のままである。

5.5 除算でオーバーフローする場合が一つある。

nビット符号つき数同士の除算で、-2n-1を-1で割る場合だけは、結果をnビットで表すことができない。