04 月 30 日(木)1-2h

データの型と値と表現

ようやく,この科目の本題「C言語」に入ろう. まず,これまでに勉強してきた言語(Scheme,KTurtle)とは異なる部分について, 理解しよう.

Cのプログラムでは, さまざまな種類のデータを利用する. データは「」だけでなく「」を持ち, さらに,値は様々な形式で「表現」される. 今回は,これらの区別について理解しよう.

たとえば, 12 という「」について, 「120.12×102十二0x0CXIItwelve, ...」のような 書き分けが「表現」. 「整数実数複素数文字, ...」のような分類が「」だと思えばよい.

しばらく,C言語から離れていたので, 初回のページを軽く読み流してから, 今回の実習を始めよう. まず, 実習の準備をお忘れなく.


変数の宣言と初期化

初回の計算機プログラム add.c では, 整数データを変数に代入し,その値を画面に表示していた.

今回は,まず List 1 のように, 変数には何も代入せず,表示だけしてみよう.

List 1. 変数のテスト var.c
main()
{
	int  x, y, z;				// 変数を宣言

	printf("x=%d  y=%d  z=%d\n", x, y, z);	// 変数の値を表示(%d は整数値に置き換わる)
}	
ソースコード内の記号「//」から行末までは, コメント(説明文).入力不要.

ここで,int x の部分では, 「このプログラムでは int 型の変数 x を使う」 と宣言している. (または,「変数 x を用意しておけ」と指示している.) しかし,変数の値については,明示していない.

このソースコードを編集しコンパイルし実行すると,何が表示されるか? 次の Unix コマンドで確かめてみよう:

$ vim var.c			# エディタを起動
...				# List 1 を編集(コピペでOK)

$ cc var.c -o var		# コンパイル
...		# もし,エラーが表示されたら,編集へ戻れ

$ ./var
x=-1081959192  y=14110708  z=1456448	# 意味不明な数値(場合により異なる)
Unix コマンド内の記号「#」から行末までは,コメント(説明文). 入力不要.

このように,意味不明な数値が表示される. これらの数値は,メモリ内にたまたま残っていた無意味なデータであり, ゴミと呼ばれる.

変数値がゼロになっている場合もあるが,それは偶然にすぎない.

なお,たとえ話として「変数とは箱であり, 箱の中身が値である」と考えるとわかり易い場合もある. しかし,変数は現実の箱とは異なり, 空(カラ)の状態になることは無い. 変数には常に,何らかの値が入っている. また,ゼロとカラとは別物.

したがって大抵の場合,変数を使うときには, 最初に定数を代入しておく必要がある. この作業を初期化と言う. では,初期化のコードを var.c に追加してみよう:

...
int  x, y, z;	// 変数を宣言

x = 1;		// x を 1 に初期化
y = z = 2;	// y と z を 2 に初期化
...
printf( ) で変数を使っているので, その前に初期化.

あるいは,次のように,宣言と初期化をまとめて書いてもよい:

int  x = 1, y = 2, z = 2;	// 宣言と初期化

しかし,次の書き方は NG:

int  x = 1, y = z = 2;	// エラー

では再度,コンパイルし実行しよう:

$ cc ...
$ ./var
x=1  y=2  z=2		# 初期化した通りの値が表示された

というわけで,変数を使う前には,必ず初期化しよう.

プログラミング言語によっては, すべての変数を自動的に 0 に初期化してくれるものもある. しかし,Cでは,必要に応じて, プログラマが変数を初期化しなければならない.

すべての変数を初期化しなければならない,のではない. たとえば,プログラムのユーザが値をキーボード入力する場合等では, わざわざ初期化しなくてよい. 入力が初期化を兼ねることになる.


整数データ

intは, データ1個について 4 byte(4*8 = 32 bit)のメモリを使う. つまり,232 通り, −2,147,483,648 〜 +2,147,483,647 の範囲の整数を取り扱える.

多数桁の数値の表現について, ソースコードの記述や実行時の入力では, カンマ区切は使えない.

また,int 型のデータサイズは, 処理系(CPU や OS やコンパイラ)によっては, 4 byte ではない場合もある.

表現できる値の範囲の制限について確認するには, 初回の足し算プログラム add.c を改造し, 掛け算プログラム mul.c を作成すればよいだろう: (足し算 x + y を掛け算 x * y に変更するだけ. 以下,add.c の step 7 版を元にした実行例.)

$ ./mul 1000000 1000
1000000 * 1000 = 1000000000	# まぁ当然だ

$ ./mul 1000000 3000
1000000 * 3000 = -1294967296	# えっ?

ここで,2 番目の実行例では, 正しい計算結果 3,000,000,000 は int 型の範囲外(> 2,147,483,647)なので, オーバーフローが発生し,不正な結果となっている.

要するに,コンピュータは万能ではない. プログラミングするときには, コンピュータの能力の限界に注意すること.

ちなみに,ソースコードに整数を記述する場合, 10進数表現の他,16進数表現も利用できる. ソースコード内での記述例:

int a, b;
a = 1023;		// 10 進数として記述
b = 0x03FF;		// 16 進数として記述

重要な前提知識

なぜ 16進数なんて使う必要があるのか? コンピュータの内部の様子を知るために必要.

現代のディジタルコンピュータは,内部的には, すべての数値を2進数表現して取り扱っている. しかし,2進数では桁数が大きくなりがちなので, 2進数4桁を16進数1桁に換算して表現すると, 人間にも取り扱い易くなる.

たとえば,10進数の 47 は,2進数では 10 1111, 16進数では 2F となる.

参考:10 進数/16 進数/2 進数


実数データ

実数を取り扱うために, float(単精度浮動小数点)と double(倍精度浮動小数点) がある. 現代では普通,double 型の方を使う.

List 2 のプログラムを実行してみよう.

List 2. 実数データのテスト double.c
main()
{
        double x = 1.234e+12;   // 1.234×1012=1,234,000,000,000
        double y = 5.0e-6;      // 5.0×10-6=0.000 005
        double a;

        a = x + y;

        printf("%f + %f = %f\n", x, y, a);	// %f は実数値に置き換わる
}								

実行例:

$ ./double
1234000000000.000000 + 0.000005 = 1234000000000.000000	# えっ?

コンピュータによる実数計算は「完全に正確」ではない. 有効数字の制限から,必ず,誤差がつきまとう.

日本語に注意せよ. 「完全に不正確」とは言っていない. 「ある程度は正確」だ. ただし,誤差を忘れて不用意にdouble 型を使ってしまうと, 意図通りの結果を得られない場合がある,ということだ.

文字データ

半角英数 1 文字のデータを扱う場合には, charの変数を使う. List 3 のプログラムを実行してみよう.

漢字等の全角文字の場合,この型 char では不完全. 別の型 wchar_t が用意されている. (ただし,本科目の守備範囲外.)
List 3. 文字データのテスト char.c
main()
{
	char  x = 'a';
	char  y = '\n';
	char  z = '%';

	printf("x='%c'  y='%c'  z='%c'\n", x, y, z);
}								

実行結果:

x='a'  y='
'  z='%'

ソースコード中で文字定数は, 引用符「'」で囲んで記述される. たとえば,'a' は,1 文字 a を表わす文字定数.

しかし,改行「\n」のように 2 文字以上によって表現される特殊な文字 (エスケープ系列)もある. これらは文字列ではなく 1 文字として取り扱われる.

なお,記号「%」については, 特別な意味をもつ場合と特に意味をもたない場合があることにも注意しよう. 特別なのは,printfscanf と一緒に使う場合だけだ.

ちなみに,char 型は, データの記憶のために 1 バイト分のメモリを使うので, 0 〜 255 とか −128 〜 +127 の範囲の整数についても表現可能. しかし,これでは実用上,範囲が狭すぎるので, 整数には int 型を使うのが普通.

1 byte = 8 bit なので, 1 byte では 28 = 256 個までの情報しか表現できない. なお,1 bit は 2 進数 1 桁分(0 or 1)の情報量.

複数の文字をまとめて取り扱うための文字列データについては, 後日説明する予定. ちなみに,二重引用符「"」で囲まれているものが 文字列定数である. (たとえば,既に使っている "Hello, world.\n" とか "x=%d y=%d z=%d\n" は文字列定数.)

Cでは,文字(1個の文字)と文字列(複数個の文字の順列)とを区別する.

printf の書式

これまでに利用して来た printf は, 一般に,次のような形式で記述される:

printf(書式文字列, データ, データ, ...);

printf の末尾の f は, ここに出てきた書式(format)のことを意味している. このことから printf書式付き出力と呼ばれている.

Cには「書式なし出力」もあるが,「書式付き」の方だけを知っていれば充分.

書式文字列のほとんどの部分はそのまま表示されるが, 変換指定子(%d,%c,等の部分)は, データ(変数,定数,計算式,等の部分)の (計算結果)と置き換えられ, さらに,各指定子に固有の形式で表現されることになる. 変換指定子には次のようなものがある:

次のように,桁数の指定も可能:

printf("[%8.3f]\n", 1.0);	// [   1.000] と表示される(全8桁,小数3桁

変換指定子についての詳しい説明は,教科書 7.2 にある. 今後,printf( ) を自由自在に使いこなせるよう, 各自で研究しておくこと.


繰り返し

List 4 は,0 〜 9 まで数える(10 回繰り返す)プログラムのソースである.

List 4. 繰り返しのテスト for.c
main()
{
	int i;

	for (i = 0; i < 10; i++) {
		printf("%d\n", i);
	}
}								

ここで,for ( ) の後の中括弧 {} の間の処理が 10 回だけ繰り返される.

要するに,KTurtle の repeat 10 と同じ. なお,実は,KTurtle にも for 文がある.

今回は,美しいソースコードの書き方として, インデント(indent; 字下げ)の方法をおぼえよう:

基本は,これだけ.

なお,インデントが無茶苦茶なソースでも コンパイルや実行には問題ない. しかし,必ず適切なインデントを心掛けること! インデントが適切であれば, プログラムが大規模になってきても, デバッグ(debug;プログラムの修正)が楽になる.

転ばぬ先の杖. 正しいプログラムを効率よく書くために必要.

重要な注意

この授業では, インデントの乱れているプログラムは, たとえ,動作的に完全であったとしても, 完全であるとは認定されません. 「理解して作っていないので,動作したのは偶然にすぎない」 という考え方です. インデントの乱れは減点対象となります.

参考:インデントの作法


練習問題

  1. 文字データを整数として(%d 変換で)表示するとどうなるか?試せ. また,なぜその結果となるのか?考えよ.
  2. printf("%d\n", 'a');
    
    こんな問題の場合,定数を変えても実験すべき. 'a' だけでなく,'b''c',... と.
  3. 前問と同様,整数データを文字として(%c 変換で)表示するとどうなるか?
  4. printf("%c\n", 65);
    
    32 〜 126 の範囲内の数値を与えること. この範囲外だとおかしな現象が...
  5. 前問と同様,整数データを実数として表示するとどうなるか?
  6. 前問と同様,実数データを整数として表示するとどうなるか?
  7. この2問では,間違いを体験してみるだけ. 「正しい結果に直すには?」とか悩む必要はない.
  8. 文字 'A' と整数 x の足し算 'A' + x の結果を 文字として表示するプログラム ascii.c を作成せよ.
  9. ただし,for 文を使って, x を 0 〜 25 の間で変化させること.

  10. 直角三角形を描くプログラム triangle.c を作成せよ.
  11. 実行例:

    $ ./triangle
      0:
      1: *
      2: **
      3: ***
      4: ****
      5: *****
      .
      .
      .
     17: ***************** 
     18: ******************
     19: *******************
    

    なお,行番号の桁数を揃えること.

    ヒント: グラフを描くには,for を二重に使う. 文字 '*' の表示を繰り返し, 最後に改行 '\n' を表示すればよい. List 4 を元にして改造すれば簡単. 行番号の桁数を揃えるには,変換指定子を工夫しよう.

(c) 2015, yanagawa@kushiro-ct.ac.jp