Q & A:教科書との違い

授業で使用する教科書はかなり古いものです。
教科書で使用している当時の PC と演習で使用する現在の学情の PC とでは構成や環境が異なりますので、同じようにプログラムのコンパイル・実行をしても結果が異なるケースがあります。
ここでは教科書通りの結果が得られない例題を中心に解説します。

int 型の大きさ

教科書の PC と演習用の PC では int 型の大きさ(ビット長)が異なりますので、取り扱える整数の範囲が大きく変わります。

種類int 型の大きさshort 型との比較整数の範囲
教科書 PC2 Byte (16 bit)int = short-32768 ~ +32767
演習用 PC4 Byte (32 bit)int > short-2147483648 ~ +2147483647

教科書では short 型の変数を int 型と同じように扱っていますが、演習では short 型と int 型を明示的に使い分ける必要があります。
特に、

等に影響が出ますので、それらの解説も精読のうえ十分に注意してください。

演算の型

c = a + b という代入文について説明します。
C 言語では、『 a に b を加えた和を c に代入する 』という意味になります。
まず演算(この場合和の計算)が行われますが、a と b をいろいろな型で定義した場合を考えます。

a の型b の型演算の型
intintint
doubledoubledouble
intdoubledouble

このように、異なる型が混在する場合には大きい方の型に合わせて演算が行われます。
そしてその演算結果が c に代入されることになります。

ではここで EX0202 (p.17-19) を見てみます。
short 型同士の積ですので short 型で演算が行われているように思えますが、実は違います。
演習用 PC の場合、整数同士の演算はいずれも int 型で行われています。(実数演算は double 型)
教科書の int 型は 2 バイト (= short 型) なので演算でオーバーフローしていますが、演習では 4 バイトになるのでオーバーフローしません。
演習でオーバーフローを確認したい場合は、代入先である変数 c を short 型で定義します。
すると、int 型で演算された結果が short 型に代入されますのでオーバーフローが確認できます。

入出力関数 (printf, scanf 等) における書式変換指定子

入出力の変換指定については、p.310-312 に解説がありますので併せて見てください。
int 型の大きさの項でも書きましたが、演習では short 型と int 型を明示的に使い分ける必要がありますので気をつけてください。これは short 型の変数を使っている多くの例題で当てはまります。
出力の printf 関数はよく使いますのですぐに慣れるかと思いますが、入力の scanf 関数でも変換指定子の使い分けは必須です。
よく使う変換指定子の例は次のとおりです。

- 整数 -
short 型int 型long 型
%hd%d%ld
%hx%x%lx
- 実数 -
float 型double 型
%f%lf

short 型ではh、long 型ではlを必ず付け加えるようにしてください。
ちなみに double 型は float 型の long だと覚えておくといいでしょう。

アンダーフロー

正規化されている場合の float 型が扱える範囲は、10進数で近似して表すと次のとおりです。

(最大) ± 3.4028235 × 10+38 ~ (最小) ± 1.17549435 × 10-38

EX0203 (p.19-23) では、a (= 3.333…) を 1038 あるいは 1039 で割ることによりこの範囲の最小値を下回ってアンダーフローすることを確認しています。
しかし、演習用 PC ではこのプログラムでアンダーフローを確認できません。
その理由を順を追って説明します。

float 型 (4 Byte) で実数を表現する場合の各ビット内部表現は、次のようになります。

float の内部表現
有効桁数: 24 桁(2 進数) = 約 7 桁(10 進数)

これら符号・指数・仮数で表された実数値のことを浮動小数点数と呼び、通常次のような形になります。

(-1) 符号部 × 2 指数部-127 × (1.仮数部)
・・・正規化の場合

指数部が 1 以上のときは必ず正規化 (normalized) されて表され、第 3 項にあるように仮数部に 1. が付け加えられます。このとき扱える値は上記の範囲内になります。

それでは、正規化されて表されている浮動小数点数を小さい順に並べて見てみます。

2 進数(仮数部バイナリ表示)10 進数刻み幅
正の数において扱える範囲での最小値は、
(1.00000000000000000000000) bin × 2-1261.17549435 × 10-38
になります。この値から最小の刻み幅①で値が増加し、
(1.00000000000000000000001) bin × 2-1261.17549449 × 10-38
その次の値は仮数部で左隣のビットに 1 が立ちます。
(1.00000000000000000000010) bin × 2-1261.17549463 × 10-38
同様に増加していくと、






やがて仮数部の全てのビットに 1 が立ちます。
(1.11111111111111111111111) bin × 2-1262.35098856 × 10-38
この値にもう一刻み分加えると (10.000…) bin × 2-126 になるので正規化すると、
(1.00000000000000000000000) bin × 2-1252.35098870 × 10-38
となります。ここからの増加については刻み幅もスケールアップ②します。
(1.00000000000000000000001) bin × 2-1252.35098898 × 10-38
以後、同様。
----------
※ 刻み幅はそれぞれ次のとおり。
(0.00000000000000000000001) bin × 2-1261.40129846 × 10-45
(0.00000000000000000000001) bin × 2-1252.80259692 × 10-45

ここである問題点が浮かびあがってきます。
上で見てもらったように値が小さいほうがより精度良く表現できるわけですが、最小約 10-45 間隔で値を刻めるのに対して、float 型が実際に表現できる値は約 10-38 までになります。これより小さい値は表現できませんので一気に 0 に丸められてしまいます。
このような小さな刻み幅での扱いを求められる非常に小さな数を演算する過程を考えたときに、表現できる範囲を超えると突然ゼロ値に向かって桁違いに巨大な刻み幅で丸めが発生する問題を「ゼロへのフラッシュ」と呼びます。
もう一つの問題として、異なる小さな値同士の差を計算しているにもかかわらず結果が約 10-38 を下回った場合には 0 に丸められてしまうことが考えられます。

このゼロへのフラッシュに対処する方法として、IEEE754 規格では「緩やかなアンダーフロー」 (gradual underflow) と呼ばれる手法を定めています。
緩やかなアンダーフローでは、ゼロ値と最小の正規数の間に「非正規数」 (denormalized number) と呼ばれる数値群を等間隔に配置することによって、ゼロ値へ向けた段階的なアンダーフローを実現します。
正規数の場合と異なり、非正規数の最小値とその近辺の刻み幅は一致するよう設計されています。
例えば、ゼロ値非正規数の最小値その次に小さい非正規数との間では、どれも等しい間隔であることが保証されます。この間隔は全ての隣り合う非正規数同士の差分と一致し、さらに非正規数の最大値正規数の最小値その次に小さい正規数との間の間隔とも一致します。このことにより、最小値近辺でのスムースなアンダーフローが期待できます。
また、非正規数を採用することで異なる値同士の引き算が決してゼロ値にならないことが保証されます。
IEEE754 標準をサポートしているハードウェア上では、非正規化数値を用いた緩やかなアンダーフローが有効となります。

先ほどの正規化する場合(指数部が 1 以上のとき)に対して、指数部が 0 のときには正規化しない (denormalized) 形での表現が可能になり、浮動小数点数は次のような形になります。

(-1) 符号部 × 2 1-127 × (0.仮数部)
・・・非正規化の場合

正規数に非正規数とゼロ値を加えて表すと次のようになります。

2 進数(仮数部バイナリ表示)10 進数刻み幅
指数部・仮数部の全ビットが 0 の場合は、ゼロ値になります。
ゼロ値(0.00000000000000000000000) bin × 2-1260.00000000
非正規数の最小値は次の値になります。
非正規数(0.00000000000000000000001) bin × 2-1261.40129846 × 10-45
非正規数の最小値と同じ値の刻み幅①で増加していき、
非正規数(0.00000000000000000000010) bin × 2-1262.80259692 × 10-45
同様に増加していくと、






やがて仮数部の先頭ビット以外の全てのビットに 1 が立ちます。(非正規数の最大値)
非正規数(0.11111111111111111111111) bin × 2-1261.17549421 × 10-38
この値にもう一刻み分加えると、正規数の最小値になります。
正規数(1.00000000000000000000000) bin × 2-1261.17549435 × 10-38
コレより先は上の表のように増加していきます。
正規数(1.00000000000000000000001) bin × 2-1261.17549449 × 10-38
以後、省略。

長くなりましたが話をまとめると、演習用 PC が約 10-39 の値でアンダーフローしない理由は非正規数を表現できる(IEEE754 標準をサポートしている)ためです。
演習用 PC で float 型のアンダーフローを確認するには、非正規数の最小値 ± 1.40129846 × 10-45 よりも小さい値を扱うようにすればよいということになります。

(補足)
double 型の考え方も同様です。
double 型 (8 Byte) で実数を表現する場合の各ビット内部表現は、次のようになります。

double の内部表現
有効桁数: 53 桁(2 進数) = 約 15 桁(10 進数)

浮動小数点数の形はそれぞれ次のようになります。

(-1) 符号部 × 2 指数部-1023 × (1.仮数部)
・・・正規化の場合
(-1) 符号部 × 2 1-1023 × (0.仮数部)
・・・非正規化の場合

正規化されている場合の double 型が扱える範囲は、10進数で近似して表すと次のとおりです。

(最大) ± 1.7976931348623157 × 10+308 ~ (最小) ± 2.2250738585072014 × 10-308

また、非正規数の最小値は ± 4.9406564584124654 × 10-324 になります。

メモリアドレス

EX0601 (p.93-96) のようなアドレス(番地)を表示するプログラムでは、実行結果で表示されるアドレス値が教科書と異なります。
これは例えば int 型の変数を宣言したときに、コンパイラがメモリ空き部分のどこから 4 バイト分を確保するかでアドレス値が変化します。
コンパイラだけでなく PC の構成や OS の種類等で変化するものだと思っておいてください。

バッファオーバーラン

バッファオーバーラン (buffer overrun) とは、プログラムのバグによってバッファ領域の内外をデータで上書きすることにより誤動作を引き起こしてしまうことです。
これはコンピュータの動作を乗っ取ってしまうことが可能なためセキュリティ上とても脅威になります。
標準入出力関数である gets や scanf はバッファ長のチェックを行わずに標準入力 (stdin) をバッファに書き込むことが可能なため、これらの関数を使うプログラムにはバッファオーバーランによる不正動作の危険性が存在することになります。
そのため、関数 gets や scanf を動作テスト目的以外で使用することは推奨されません。

EX0803A (p.127-129) では 7 行目でバッファ長 10 バイト分を確保していますが、このままでは標準入力に改行文字か EOF が現れるまで確保分を超えても読み込み続けますのでバッファオーバーランの危険性があります。
演習用 PC においては、特に関数 gets に書き換えた場合にはコンパイル時に
gets' function is dangerous and should not be used.
と警告されます。
このプログラムを実行するときには、バッファ長 10 バイトを超えて入力しないよう気をつけてください。

バッファオーバーランに対処する方法として、読み込み文字数を指定することができる関数 fgets を gets や scanf の代替として用いることが推奨されています。
関数 fgets の書式は次のようになります。
char fgets(char *s, int n, FILE *stream)
引数の第 1 項は格納先配列名、第 2 項は最大文字列数、第 3 項は読み込み元になります。
また、ここでの char, int, FILE はそれぞれ指定された型を引数にしなさいという定義の意味ですので、実際にプログラムで使用する場合は書きません。
EX0803A を例にすると、
fgets(a, 10, stdin);
のようになります。
ただし、fgets を標準入力関数の代替として用いる場合は下記の点に注意して使用してください。

関数文字幅指定改行文字の扱い最大文字数を超えた入力に対する対処
fgets必須1 文字として数える超えた文字はストリーム上にそのまま残る
(後から別の入力を行うときに誤動作の危険)
getsできないヌル文字に置き換える何も残らない
scanf省略可能char 入力の場合は 1 文字
それ以外の場合は無視される
文字幅指定を省略した場合は何も残らない
指定した場合はストリーム上にそのまま残るが
代入抑止を使用すればクリアできる

文字列定数で初期化されたポインタ変数

EX0902 (p.143-147) では配列 s とポインタ変数 *s_pnt を宣言し、配列の先頭アドレスをポインタ変数に代入することでポインタ変数による配列の操作を確認しています。
この中でポインタ変数を文字列定数で初期化する方法 (p.145-) が記載されておりますが、このプログラムには注意が必要です。
この場合は配列を宣言しておりませんので、ポインタ変数には文字列定数の先頭アドレスが入ります。
ここで、
*(s_pnt+5) = 'D';
のような代入文を用いて定数に値を代入することは本来できません。
教科書 PC では実行可能なようですが、演習用 PC では実行段階でセグメントエラーとしてプログラムを終了します。

関数 rand

rand は擬似乱数を作る関数で 0 ~ RAND_MAX (最大値)までの値のうちひとつを返します。
この関数を使うときはヘッダーファイル stdlib.h をインクルードしてください。
演習問題として 11.3A (p.191) がありますので見てください。
教科書では RAND_MAX = 32767 となっていますが、演習では int 型の大きさが違いますのでこの値も変わります。
RAND_MAX = 2147483647
となりますので注意してください。

[範囲乱数]
最小値 + (int) ( rand() × (最大値 - 最小値 +1.0) / (1.0 + RAND_MAX) )

プログラムの実行時間

EX1201 (p.195-197) ではプログラムの実行時間を計測して表示しています。
この実行時間はコンピュータの性能によりますので、このままのプログラムでは演習用 PC だと time = 0 秒になります。
演習用 PC でこのプログラムを検証するには、15 行目と 26 行目にある for ループの継続条件(第 2 項)を増加して処理にかかる時間を増やしてください。

分割コンパイル

目的とするプログラムが非常に大きくなる場合には、そのプログラムを幾つかの部分に分けて作り、それぞれ別々にコンパイルして目的プログラム(オブジェクトプログラム)にした後で最後にこれらをリンクしてひとつの実行ファイルを作成することができます。
このように複数のソースファイルを個々にコンパイルする作業のことを分割コンパイルといいます。
EX1203B (p.204-208) では、EX1203B1 と EX1203B2 という 2 つのソースプログラムを合わせて EX1203B という名前の実行ファイルを作成しています。
教科書では
tcc -eEX1203B EX1203B1.C EX1203B2.C
となっておりますが、
演習では
cc -o EX1203B EX1203B1.c EX1203B2.c
または
cc EX1203B1.c EX1203B2.c -o EX1203B
とコマンド入力することで分割コンパイルできます。

MS-DOS と Linux

教科書は MS-DOS 上で動作する「Turbo C」というコンパイラソフトを用いて解説しています。
一方、演習用の学情 PC では Linux 上でコンパイルします。
EX1306C (p.258-263) は、MS-DOS で用意されているデバイス名や FILE 構造体へのポインタを使用しているため Linux 環境ではコンパイルできません。
EX1401B (p.283-285) は、dos.h をインクルードして使用しており Linux にはこのヘッダーファイルが存在しないためコンパイルできません。