情報基礎A 「Cプログラミング」(ステップ1・準備)

このステップの目標

0. 何故プログラミングを学ぶのか?

私がまだ若いころ、ソフトウェア関係のマニュアルの中に「適用業務」という単語が頻繁に登場していて、それが「アプリケーション」を意味することはすぐに判ったけれども、日本語としてはなんとも言えない違和感を覚えたことを思い出す。。

パソコンやスマフォにはOS(Operating System)というソフトウェアが入っていて、それ無しでは動かないし、しばしばそれをアップデートするよう「催促」されたりする。 さらに、便利に使うには、アプリ(application softwareの略)を追加でインストールしないといけなかったりする。 いわゆる情報機器以外でも、テレビなどの家電製品もその中身はコンピュータそのものであるし、 普段はあまり気にしないようなちょっとしたデバイスでさえ、不具合を解消するため、ときどきファームウェア(firmware)のアップデートが必要になったりもする。 今や、我々の暮らしのかなりの部分が、コンピュータープログラム(ソフトウェア)によって支えられているのは間違いない。

そういったソフトウェアは、もちろん、その道のプロ(あるいは、優秀なアマチュア)が開発しており、 どんなに自動車が好きでも、部品から車を自作することは(ほとんど)無いように、 ソフトやアプリは、誰かが作ったものをダウンロードしたり購入したりするのが当たり前である。 ところが、この「情報基礎」では、プログラミングの実習に一定の時間が割かれている。 このクラスの学生の多くは、将来その方面に進む可能性は少ないだろうに、全員がプログラミングを学習する意味(あるいは、教える側の意図)はどこにあるのだろう。

この授業の目的は「パソコンの使い方教室」ではないので、個別的な使い方ではなく、むしろプログラミングを通じて問題を抽象化して考えたり、論理的な思考を行う能力を養うことが大切。そして、その基本的な考え方は、WindowsでもMacでもスマートフォンでも、CでもJavaでも、皆同じだ。

加えて、是非ここで強調しておきたいのは、プログラミングは知的な「ゲーム」としても、とても面白く刺激的であるということだ。

1. コンピュータの基本的な動作

コンピュータは、記憶装置(メモリ)に書き込まれている数値(機械語命令)を順に読み込んで、それに対応した動作を延々と行うだけの、原理的には、いたって単純な機械だ。コンピュータが処理できるのは、おおざっぱに言って、以下のような単純な仕事だけである:

  1. データ(数値)を外部装置に出力する
  2. 外部装置からデータ(数値)を入力する
  3. メモリーのデータ同士を演算(加減乗除など)して、その結果をメモリーにセットする(式の計算と代入)
  4. 演算の結果によって(結果が0より大きいかどうか、など)、次に処理する命令を切り替える(条件分岐あるいは選択処理)
  5. 機械語命令の決められた範囲を反復する(反復処理)
  6. 機械語命令の決められた範囲に分岐して、そこが終わると、もとの場所から処理を再開する(サブルーチン処理)

これらの処理を何万ステップも組み合わせると、画面に文字や画像を表示したり、数学関数を計算したり、意味のある動作を行わせることができるわけだ。ただ、それを数値(機械語; machine code)で記述するのはとても面倒で複雑なので、人間が分かりやすい記号で書かれたプログラム(ソースプログラム(source program)とかソースコード(source code)と呼ぶ)を、機械語に翻訳するシステムが開発された。それをコンパイラー(compiler)と呼ぶ。

c-2-basic-workflow

この箇所、とても重要!

こうした手順に対応して、C言語でのプログラム開発は、以下の3つの基本ステップで構成される:

  1. 何々.c というファイルC言語のソースコードを作成する
  2. cc (あるいはgcc)というコマンドをつかって、ソースコードをコンパイルする
  3. a.outというファイルが新しく生成され、その中に機械語が格納されているので、それを(その中身の機械語コードを)実行する

ついでながら、実行する、は英語で execution である(死刑執行、という怖い意味もある)。Windowsのプログラム名の拡張子と使われている"exe"は、このことに由来する。

2. プログラム作成から実行までの流れ

では、ICL演習室の環境で上記の3ステップを行う具体的な方法を紹介しよう。

c-1-turtleedit-icon
TurtleEditのアイコン

準備:実習用のソフト(TurtleEdit)を開く

画面左上の「アプリケーション」メニューをクリックし、「アクセサリー」または「プログラミング」の項目の中にあるTurtleEditを選んで起動する。 TurtleEditは情報基礎の実習用に開発されたソフトで、ソースプログラムの作成、コンパイル、実行をこのソフトを使って行うことができる。

TurtleEditの標準設定

TurtleEditの「実行」メニューから「設定」を選ぶと、ソフトの振る舞いを変えることができる。 以下に、標準的な設定をまとめておく。色々といじっているうちに動作がおかしくなったときは、これらの項目を確認すること:

  1. 「実行」メニューから「設定」を開く。
  2. 「コンパイルコマンド」の確認: cc %f -lm
  3. 「実行コマンド」の欄を確認: /opt/cite/bin/run ./a.out
  4. 「亀場のパス」の欄を確認: /opt/cite/bin/tfield-linux
  5. 「文字コード」を確認: "UTF-8"
  6. 「フォント」を選択
  7.  OKボタンを押して設定完了。

ステップ1:ソースプログラムの作成

TurtleEditのウィンドウは上下の2つの区画に分かれている。その上側が、プログラムを記述するための区画で、 ワープロのようにCのプログラムをその中に記述する。

TurtleEditのウィンドウの様子。上側の区画で編集作業を行う。 下側の区画はプログラムの実行結果の表示等に使われる。

c-1-turtleedit-main

C言語のプログラムの記述法などはこれから勉強するとして、ここでは、TurtleEditの画面(上側の区画)に 以下のCプログラムをコピー&ペーストしてみよう:

曜日の計算するCプログラム(dow.c)

なぜこれで曜日を計算することができるのかについては、さらに先のページで説明する。

#include <stdio.h>

main()
{
  int year, month, day, year2, month2, dayofweek ;
  static char dow[][4] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"} ;

  printf("Year ?: ") ;
  scanf("%d",&year) ;
  printf("Month ?: ") ;
  scanf("%d",&month) ;
  printf("Day ?: ") ;
  scanf("%d",&day) ;

  if (month==1 || month==2) { year2 = year - 1 ; month2 = month + 12 ;  }
  else { year2 = year ; month2 = month ; }
  dayofweek = (year2 + year2/4 - year2/100 + year2/400 + (13*month2+8)/5 + day) % 7 ;

  printf("%d/%d/%d is %s.\n",year,month,day,dow[dayofweek]) ;
}

コピー&ペーストする際に、最初の # や、おしまいの } を選択し忘れないよう気をつけること。

プログラムを記述(コピー&ペースト)したら、必ずその内容をファイルに保存しておく。 「ファイル」メニューの中の「保存」を選んで、自分の決めたディレクトリ(フォルダ)内にファイルを保存する。 その際、 ファイル名の拡張子は必ず .c にすること (拡張子cはそれがC言語のソースプログラムであることを表す)。 ここで、ファイル名はdow.cとしておこう。

ステップ2:コンパイル

コンパイルボタンを押した際の具体的な動作は、TurtleEditの「実行」メニューの中の「設定」を選んだ際に表示される「コンパイルコマンド」の欄で設定されている。
特に変更しない限り,コンパイルボタンを押すと,cc というUnixコマンドが実行されるように設定されている。

保存したソースプログラムをコンパイルするには、TurtleEditのウィンドウ内のコンパイルボタンを押す。

もし、ソースプログラムに何かの不具合(エラー)があれば、TurtleEditの下側の区画にエラーメッセージが表示されるので、行番号を手がかりに、エラー箇所を見つけ、修正すること。

コンパイルが正常に終了すると、TurtleEditの下側の区画に「正常終了」と表示される。

コンパイルが成功した状態で、作業ディレクトリ確認ボタンを押すと、ソースプログラムの保存先の ディレクトリ名と、その中のファイルの一覧が表示される。現在作業中のファイル名(この場合はdow.cは赤字で 表示されている。 リストを注意深く眺めると、a.outというファイルも見つかるはずだ。 これがコンパイルによって生成された機械語のデータが格納されたファイルになる。

ステップ3:実行(および中断)

実行ボタンを押した際の具体的な動作は、TurtleEditの「実行」メニューの中の「設定」を選んだ際に表示される「実行コマンド」の欄で設定されている。
特に変更しない限り,実行ボタンを押すと,a.out というコマンドが実行されるように設定されている。

コンパイルされたプログラムを実行するには、TurtleEditの実行ボタンを押す。

このとき、もしも予期せぬ動作をした場合は中断ボタンを押せば、プログラムを強制終了することができる。

この例題プログラムは、西暦年、月、そして日をキーボードから入力するように求めてくる。 TurtleEditの下側の区画をマウスでクリックし、赤い四角いカーソルが出ている状態で、キーボードからそれぞれの項目を入力しEnterを押すと、 最後に、その日の曜日が出力されるはずだ。 例えば、あなたの誕生日は何曜日だろうか?。

icon-pc 練習:実習のための専用のディレクトリを作成

今後の実習のために、各自のホームディレクトリの中に、Cのプログラムを保存するための専用ディレクトリを作成しておくこと。 ディレクトリ名は自由だが、日本語と空白文字(スペース)は使わないこと。 無難な名前の例としては、"c", "cprog", "c_examples", 等々。

tfield-icon亀場で練習:グラフィックスを描いてみる

「亀場」を使うため準備のページを開き、説明を手がかりに、「亀場」の画面に四角形を描けることを確認しなさい。


3. アルゴリズムを考え、それをプログラムとして表現する

Cプログラミングと全く関係無さそうな話題かもしれないが、ここで『炊飯器でご飯を炊く手順』を考えてみよう。 授業担当者の場合、ざっと以下のような感じになる:

Input: 精米済みの米 $n$ 合
Output: 炊きあがったご飯 $n$ 合
1: 容器に米を入れる
2:   水を入れ、米を撹拌する
3:   米が沈むまで待ち、上澄みの水を捨てる
4: 上澄みの水が大きく濁っている間は2:から繰り返す
5: 洗った米を炊飯器の容器に移す
6: 炊飯に容器内側の目盛り $n$ に達するまで水を入れる
7: 炊飯器をコンセントに接続する
8: 「炊飯」ボタンを押す
9: 「炊飯」ランプが消え、「保温」が点灯するまで待つ
10: 終了する

ご飯を炊いたことの無い人に「ご飯、3合炊いといてね」といったアバウトな指示は全く意味が無いように、 コンピュータへの指示(コンピュータ・プログラム)も具体的かつ詳細に記述する必要がある。 このように、問題解決に向けての一連の処理や作業の手順をステップに分解し、具体的に記述したものをアルゴリズムalgorithm)と呼ぶ。

炊飯器の中にもコンピュータが内蔵されており、エンジニアが炊飯のアルゴリズムを考え、それを(Cとは限らないが)プログラミングしているはずだ。

手順をどのくらい細かい手順にまで分解した上で、どのような語彙と文法を使ってアルゴリズムを記述する必要があるかは、もちろん、想定する機械(計算機)の機能によって変わる。 もし完全自動式の未来型の炊飯器があったら、その機械のための炊飯アルゴリズムは、『白米をn合を炊く』の1行で済んでしまうかもしれない。 一方、上に示したアルゴリズムは、現在普及している炊飯器を使い、人が作業することが暗に想定されている。

高度に抽象的なアルゴリズムを考えたとしても、それをCでコーディングする際には、より具体的で細かい手順に分解する必要が生じる。 反対に、コンピューターを極限まで数学的に単純化したモデルが チューリングマシン(Turing machine) で、TMの機能だけでアルゴリズムを記述するのは恐ろしく面倒な作業になる。

であるから、アルゴリズムを考える際には、我々がどれくらい賢い(あるいはおバカな)計算機システムを相手にしているのか を、あらかじめ想定しておく必要がある。 ここで我々が学ぼうとしているC言語は、プログラミング言語の中では「高水準言語」に分類されているものの、 コンピュータのハードウェアの設計と密接に関係した、かなり「低レベル」な操作を想定して設計されている。 そのため、Cでプログラミングする際に我々が考えなくてはならないアルゴリズムは、ハードウェア(CPU)の動作を直接・間接に反映したものとなる。

すると、きちんと手順(アルゴリズム)を記述するにはC言語の語彙や文法を把握しておかねばならない一方で、Cでプログラミングするためには、 まずアルゴリズムを考えなければならないことになって、これでは完全に循環論法である。 良い例えではないかもしれないが、作曲の際には楽器の特性を考慮しながら楽譜を書かなければならない一方で、楽器を演奏するには楽譜が必要である。 プログラミングの学習というのは、作曲法と演奏を同時に勉強するのとどこか似ているかもしれない。

その感じを掴んでおくために、具体的なCプログラミングに入る前に、C言語(を含む一般的なプログラム言語)を使って問題解決の自動化を行う際、 どれくらいの細かさでアルゴリズムを考える必要があるのか、トランプのカードを用いた簡単な例を使って、まずは頭の準備体操をしておきたい。

カードの並べ替え

以下では、データをトランプのカードに見立てて説明しているが、「コイン」や「麻雀牌の萬子や筒子」など、 数値の大小が明確ならば、より親しみのあるモノに対応させて考えても一向に構わない。

テーブルにカードが2枚あって、それぞれ、aとbと書かれた枠内に置かれているとしよう。 その他に、tと書かれた枠がもうひとつあるとする。 そして、以下のルールに従ってカードを操作できるとしよう。

c-1-cards

このとき、aとbのカードの場所を入れ替える手順を、「Cプログラミング流」に述べると、 以下のようになる(右側に、実際に動作するCコードの例も示した):

Algorithm:カードの交換
Cによるコーディング例(swap2.c)
Input: aとbに置かれたカード
Output: 置き場所が入れ替わったカード
1: aにカード(例えば1)を置く
2: bにカード(例えば2)を置く
3: aをtに移動する
4: bをaに移動する
5: tをbに移動する
6: aのカードの数値を確かめる
7: bのカードの数値を確かめる
8: 終了する
#include <stdio.h>
main()
{
  int a,b,t ;
  a = 1 ;
  b = 2 ;
  t = a ;
  a = b ;
  b = t ;
  printf("a=%d\n",a);
  printf("b=%d\n",b);
}

上記のアルゴリズムを『aとbのカードを交換する』と一言で表しても、論理的に間違っているわけではないが、 一般的なコンピュータのハードウェアには「二つのデータを1回の工程で交換する」という機能が備わっていないので、 そのことを反映して、Cプログラミングでも、データをひとつずつ移動するステップにまで分解して考えなければならないのだ。

icon-pc 練習:5枚のカードの並べ替え

(左から)一列に並んだ置き場所a,b,c,d,e上にそれぞれ置かれた5枚のカードの順序を反転させるアルゴリズムを、2枚のカードの交換の例に倣って、記述しなさい。 ただし、もう一箇所、仮置き場 t が用意されているとしなさい。


icon-teacher 解説:アルゴリズムの汎用化

あらゆる問題の規模に対応する

『カードの並べ替え』を考えるときに、2枚の場合、3枚の場合、... といった具合に、与えられたカードの枚数ごとに手順を考えるのは明らかにスマートではない。 $N$ 枚($N$ は任意の自然数)のカードが並んでいる場合の手順を一般的に記述しておいて、枚数に応じて $N$ の値を「後から」設定できれば、はるかに融通が効く。

置き場所を番号や変数で指定する

カードの並べ替えの場合、どんな規模($N$ の値)の問題にも対応できるようにアルゴリズムを記述するためには、カードの置き場所に a, b, c, ..という風に個別の名前を与えるやり方は破綻する(カードが1億枚あったら、1億通りの「地名」が必要になる)。 そのような場合は、「何番目の置き場所」という風に通し番号をつけて管理するほうが理にかなっている。例えば、カードの置き場所全体に $a$ という名前をつけて、1億番目の置き場所は $a_{100000000}$ とするのがわかりやすい。 さらに、置き場所の番号も、記号(変数)で指定できるようにして、$a_i$ といった風に添え字を使った記法を導入すると、記述の幅がうんと広がる(ここで $i$ は場所を表す自然数)。 例えば、$a_i$ の右隣は $a_{i+1}$ と指定できるし、左隣は$a_{i-1}$ 、一番右端の置き場所は $a_{N}$ である。こうすれば、どんな $N$ でも対応できるはずだ。

反復動作の指示

置き場所 a, b にそれぞれカードが置かれている場合、 すべてのカードを仮置き場 t に片付けるには、

1: a のカードを t に移動する
2: b のカードを t に移動する

と書けば事足りる。 一方、$N$ 枚のカードの場合、すべての置き場所についての移動を指示するには、上記の添字を使った記法を使って

$a_i$ $(i=1,2,\cdots,N)$ のカードを、順次 $t$ に移動する

などと書きたくなる。ところが、$\cdots$ のところは、人間が正しく察してくれることを想定した記法である。 つまり、添字の変化を表している $i=1,2,\cdots,$ の箇所は、意地悪に解釈すると、例えば、1, 2, 3, 5, 8, ... の場合なども含んでしまう。 こうした曖昧さを避けるには、起点、終点、そして「歩幅」まで、きちんと示しておく必要がある。 この場合、アルゴリズムの記述は以下のようにすれば良い:

1: $i$ を 1 から $N$ まで 1 ずつ増やしながら 3: までを繰り返す:
2:   $a_i$ のカードを $t$ に移動する
3: 繰り返しここまで

$a_1$から順に$a_2, a_3, \cdots$ と参照位置を変えながら、全てのカードを $t$ に移動させる

icon-pc 練習:N枚のカードの並べ替え

一列に並んだ$N$個の置き場所 $a_1, a_2, \cdots, a_N$にそれぞれ1枚ずつ計$N$枚のカードが置かれている。加えて、仮置き場 $t$ も使えるとする。 $N$枚のカードの順序を反転させるアルゴリズムを記述しなさい。

ここで、$i$番目の置き場所$a_i$とし、「反復過程」を含め

Input: $a_i$ $(i=1,2,\cdots,N)$ に置かれた $N$ 枚のカード
Output: 置き場所が入れ替わったカード
1: $i$ を ?? から ?? まで1ずつ増やしながら ?: までを繰り返す:
2:  ($i$の値に応じたカードの操作...)
3:  ...
4:  ...
?: 繰り返しここまで

の様式で記述すること。


「整列」については、教科書 p.30 例題2.25を参照

icon-pc 練習:3枚のカードの整列

一列に並んだ置き場所(左から)a,b,c上に数値が書かれたカードが置かれている。カードの数値は全て異なるとしよう。 最初のカードの配置にかかわらず、最終的にカードの数値が左から小さい順になるよう整列させるアルゴリズムを、「ヒント」の2枚のカードの交換の例に倣って、記述しなさい。

icon-hint ヒント

記述にあたり、二枚のカードの大きさを比較して、その大小に応じて、処理を切り替えることができるものとする。

例えば、カードが2枚の場合は

Input: aとbに置かれたカード
Output: 小さい順に並んだカード
1: a にカードを置く
2: b にカードを置く
3: a と b を較べ a >b ならば4:から実行
  そうでなければ7:に移る
4: a を t に移動する
5: b を a に移動する
6: t を b に移動する
7: 終了する

あるいは、(この課題では記述量を減らすために)「交換」をひとつの操作として定義してしまって、

Input: aとbに置かれたカード
Output: 小さい順に並んだカード
1: a にカードを置く
2: b にカードを置く
3: a と b を較べ a > b ならば a と b を交換する
4: 終了する

と手短に書いてしまってもよいことにしよう。

発展問題として、教科書の例題2.25を参考に、$N$枚のカードを昇順に整列するアルゴリズムを記述してみなさい。

icon-pc 発展練習:カードの「山」の移動

ここまでは、「カードの上にカードは置けない(重ねたら、一番上のカードにまとまってしまう)」という状況設定を考えていた。 この練習課題では、このルールを変更して

としてみよう。

三つの置き場所 a, b, cがあり、aに $N$ 枚のカードが積まれている。上記のルールに従い、カードに書かれた数値は、上から下に大きくなっている。 カードを1枚ずつ移動させて、このカードの山を aからcに移動する手順(アルゴリズム)を見つけなさい。

カードの枚数が $N=2$ のときをまず考え、$N=3, 4, 5$ と増やしてみなさい。

c-1-card-stack
icon-hint ヒント

これは「ハノイの塔」という名前で良く知られている問題である。 カードが$N$枚のとき、「山」を移動するには$N^2-1$回の手数が必要なことがわかっている(2枚の場合は3回、3枚で7回, 4枚で15回)。

記述の際には、aからbへの移動を $ a \rightarrow b$ と矢印で表すことにしよう (あるいは、$ b \leftarrow a$。C言語の代入操作(=) との対応を考えると、こちらのほうが良いかもしれないが、 いずれにしても、矢印の向きで移動方向が分かれば問題はない)。

icon-teacher 解説: 「関数」を使ったアルゴリズムの記述

$x,y,z$という3つの置き場所があったときに、$x$に積まれた$n$枚のカードを$y$に移動する「行為」を $\textrm{move}(n,x,y,z)$ と記述してみよう。 ここで、$x,y,z$は変数で、$x$の内容は置き場所aであったり、置き場所bであったり・・、と変わり得る、と考える。 具体的には、例えば$n=2$のときは、

$\textrm{move}(2,x,y,z)$の内容:
  $x \to z$
  $x \to y$
  $z \to y$

となるだろう。

$x$に$n$枚の山があったときは、上側の$n-1$枚をまず$z$に移動し、一番下のカードを$y$に移動後、$z$に積まれていた$n-1$枚を$y$に移せば良い。 その手順は

$\textrm{move}(n,x,y,z)$の内容:
  $\textrm{move}(n-1,x,z,y)$
  $x \to y$
  $\textrm{move}(n-1,z,y,x)$

と記述することができる(moveの中の$x,y,z$の順序に注意)。

c-1-hanoi

この、moveという「関数」を使うと、アルゴリズムは

Input: aに積まれた$n$枚のカード
Output: cに積まれた$n$枚のカード
1: 関数 $\textrm{move}(n,x,y,z)$ を定義する:
2:   もし $n=2$ ならば
3:     $x \to z$
4:     $x \to y$
5:     $z \to y$
6:   それ以外($n>2$)なら
7:     $\textrm{move}(n-1,x,z,y)$
8:     $x \to y$
9:     $\textrm{move}(n-1,z,y,x)$
10: 関数定義終わり
11:
12: 関数 $\textrm{move}(n,a,c,b)$ を評価する

のように表現することができる。このアルゴリズムをC言語に直訳したのが以下のコードである。

hanoi.c

#include <stdio.h>

void move(int n, char x, char y, char z) {
    if (n==2) {
        printf("%c --> %c\n",x,z) ;
        printf("%c --> %c\n",x,y) ;
        printf("%c --> %c\n",z,y) ;
    } else {
        move(n-1,x,z,y) ;
        printf("%c --> %c\n",x,y) ;
        move(n-1,z,y,x) ;
    }
}

main()
{
    char a='a' ;
    char b='b' ;
    char c='c' ;

    move(5,a,c,b) ;
}

10枚のカードについて解く様子(YouTube動画)

コンパイルして実行すると、5枚の場合の手順が画面に出力されるはずだ。プログラムの最後の辺りの"5"の値を変えれば、異なる枚数についても計算可能である。

この例では、「カードを移動する作業は・・・・をすることである」という風に、新しい語彙(ある種の動詞句)を定義することを通じてアルゴリズムが記述されている。 このように、手順の記述方法には、いくつかの流儀があって、そうした流儀に適したプログラミング言語が(C言語以外にも)沢山開発されている。