本連載は,「C言語」というプログラミング言語の“定番”を通して,あらゆるプログラミングに共通する基礎となる部分を,じっくり解説していきます。初心者の方はもちろん,プログラミング経験のある方も自分の知識を再確認するのにきっと役立つと思います。

この連載に目を止め,「読んでみようか」と思ってくれた皆さんは,これまで,どんなプログラム言語の経験があるでしょうか? 「JavaScriptなどのWeb系のスクリプト言語はよく使う」,「Visual BasicやJavaでアプリケーションを作っているのだけど,基本を確認したくて…」,あるいは「連載1回目だし,これからプログラミングを学びたい」など,いろいろな方がいらっしゃると思います。

☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆
☆      カタログ      ☆
☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆

第1回 もう一度,C言語から始めよう

コンピュータはマシン語しか理解しない

 皆さんは,そもそもコンピュータは何語で動いているのかわかりますか? 日本人にとっての母国語が日本語で,英国人にとってのそれが英語であるように,コンピュータにとっての母国語は機械語(マシン語)です*1。人間は,必要に迫られれば学習し,カタコトでも他国語を話せるようになりますが,コンピュータはちっとも自発的に学習してくれません。何年たっても,マイクロプロセサが直接に解釈・実行できる言語はマシン語だけです(多くの日本人が,中学からずーっと英語を勉強しているわりに,英語を話せないという事実はまあ,それはおいといて…)。

 マシン語というと,何か意味のある言葉のようなイメージがしますが,デジタル機器であるコンピュータの中では,プログラムも含め,あらゆる情報が2進数で表現されています。ですから,マシン語は人間の目には単なる数字の羅列にしか映りません。例えばこんな感じです。

 4D 5A 44 01 12 00 12 00 20 00 FF …

マシン語プログラムを16進数*2で表示させると,上記のように数字が並んでいるだけなのです。

 かつて,「コンパイラ」や「インタプリタ」という名前の便利な道具が存在しない時代がありました。そのころは,人間がこのマシン語でプログラムを記述していたのです。「4D5Aと書くと×××が○○○になるから,次は4401で△△△して…」と人間にとっては意味を見出せない数字の羅列でプログラムを作っていた――今,考えるとぞっとする作業ですね。

 そこで,人間にわかりやすい文字で書いたものをマシン語に変換(翻訳)するプログラムが開発されました。それが「アセンブリ言語」です。アセンブリ言語では「ニーモニック」という英単語を略式化したような記号でプログラムを記述します。例えば以下のような感じです。

push ebp

mov ebp,esp

xor edx,edx

mov eax,1
…

このニーモニック・コードで記述されたプログラムを,「アセンブラ」という別のプログラムがマシン語に翻訳するのです。アセンブラは,ニーモニック・コードをマシン語に1対1で翻訳するプログラムです。ADD(加算),CMP(比較),DIV(除算),INC(+1する),DEC(-1する),JMP(無条件ジャンプ),MOV(データの転送)――といった命令が用意されています。

 アセンブリ言語を使えば,数字の羅列に比べてだいぶプログラムを読み書きやすくはなりました。しかし,しょせんアセンブリ言語も,基本的にはマシン語と1対1で対応しているに過ぎません。複雑なロジックを書くには面倒だということは想像がつくでしょう。わかりやすくなったけど,プログラミングはやっぱり大変な作業であるという状態ですね。

C言語はプログラマの共通語

 やがて,大規模なプログラムを,もっと効率よく,もっと簡単に作成できないかと,より自然言語*3に近い言葉でプログラムを記述するためのプログラミング言語(高級言語)の開発が始まりました。科学技術計算に向くFORTRAN,プログラミング初心者に適したBASIC,小数の計算が正確で事務処理に向くCOBOL,明確な文法を持つPascal――など1950年代ごろから,様々な高級言語が生まれました。

 そして1970年ごろ,これから本連載で皆さんが学ぶC言語が登場したのです。C言語は当初,UNIXというOSを開発するために作られました。しかしC言語が備える機能が強力だったため,UNIX上で動作するプログラムの開発や,Windowsにも使われていくようになりました。現在,Windows用のソフトウエア開発で,C言語が一番使われているというわけではありません。それでも,現在利用されている多くのプログラミング言語は,C言語の影響を少なからず受けています。

 C言語をオブジェクト指向化したC++や .NET対応のC#は当然C言語の流れをくんでいます。Javaもそうです。Webアプリケーションで使われているPerl,PHP(PHP:Hypertext Preprocessor),JavaScriptなどもC言語の影響を強く受けています。

 つまりC言語をマスターすれば,プログラミングに必要な基礎が身に付くとともに,現在注目を浴びている言語を学ぶ足がかりができるのです。今のC言語は,プログラミングを深く理解するうえでの共通言語に成っているといえなくもないでしょう。

C言語はコンパイラを使う

 さて,高級言語には,書いたプログラムをマシン語に翻訳するのに大きく2種類の方法,インタプリタとコンパイラがあります(図1)。図1上のインタプリタでは,プログラム実行時に1行ずつ翻訳を行います。翻訳-実行を1ステップ(1行)ごとに繰り返すわけです。実行するたびに翻訳する手間がありますから,処理速度はあまり速くはありません。でも,プログラムのバグ*4を見つけて修正したら,すぐに再実行できるという利点があります。短いプログラムを作成する場合や,プログラミングの学習に適しています。

図1●インタプリタとコンパイラ

一方,図1下のコンパイラは,マシン語にプログラムを翻訳して,実行可能なファイルを作成します。プログラムに文法上の間違いがあった場合は,コンパイラがコンパイル・エラーとして出力するので,エラー・メッセージの内容を分析してプログラム上のミスを修正し,再度コンパイルしてプログラムを作っていきます。実行速度は翻訳の手間が必要ないぶんだけ,一般にインタプリタに比べて高速です。C言語は,こちらのコンパイラ方式を採用しています。

 ちなみにJavaや .NETに対応したプログラミング言語(C#,Visual Basic .NETなど)も,コンパイラを利用します。ただし,これらのコンパイラは特定のOSに依存するマシン語を作成するのではなく,代わりに「中間コード」というものを出力します。出力された中間コードは,Javaなら,Java VM*5というJavaプログラム専用の実行環境上で,.NETなら .NET Framework上*6でマシン語に翻訳され実行されます。実行環境がOSの違いを吸収してくれるので,同じプログラムが別の種類のコンピュータ上でも動作できる仕組みなわけです。

なにはともあれ

コンパイラをインストールする

 プログラミング言語の学習で一番大事なことは,実際に“自分の手を使って”プログラムを作成し,コンパイル・実行してみることです。読むだけではだめです。本連載でも,皆さんが実際に手を動かしながら学んでいけるようにしていきますので,どうぞ試してみてください。

 本連載で利用するC言語のコンパイラは「Borland C++ Compiler 5.5」です。ボーランドがWebサイトで無償公開しています。このコンパイラを使えば,Windows 95/98/MeのMS-DOSプロンプトや,Windows XPなどのコマンドプロンプト上で,C言語あるいはC++言語で記述したソース・プログラムをコンパイルできます。

まずはインストールしましょう。今回はWindows XPにインストールする手順を説明しますが,Windows 2000や98/Meでも利用できます*7。インストール後に必要な設定がありますので,読み飛ばさずにゆっくりお付き合いください。

 ダウンロードした「freecommandlinetools2.exe」をダブルクリックすると,インストーラが起動します。使用許諾契約のダイアログが表示されますので,同意して,インストールするフォルダを選択してください。デフォルトはc:\borland\bcc55です。特に理由がない限り,そのままがよいでしょう。インストールが終わると,c:\borland\bcc55にいろいろなファイルが作成されます。「readme.txt」というファイルに必要な設定が書いてありますので,メモ帳などのエディタで開いて読んでください。

 readme.txtの「2.のa.」という欄に「既存のパスに “c:\Borland\Bcc55\bin” を追加します」とあります。パス(path)という言葉がはじめてだと,目的が理解しにくいと思いますので,簡単に解説しておきましょう。

 コマンドプロンプトを起動して,カーソルが点滅している部分に次のコードを入力してEnterキーを押してください。

 cd \borland\bcc55\bin

cdとは,ユーザーが作業を行うフォルダ(カレント・ディレクトリ)を移動する,MS-DOSのcd(change directory)コマンドのことです。コマンドプロンプトには「C:\cd \borland\bcc55\Bin>」という文字が出て,カーソルが点滅していると思います。つまり,カレントディレクトリが,c:\borland\bcc55\binに移動したわけです。

 実はBorland C++ Compiler 5.5の本体であるコンパイラ・プログラムは,このフォルダの中にある「bcc32.exe」ファイルです。ですからここで,

 bcc32

と入力すると,コンパイラ・プログラムを起動できます。でも,コンパイラ・プログラムを起動するために,いちいちc:\borland\bcc55\binにカレント・ディレクトリを移動するのは面倒ですよね。フォルダを新たに作って,そこにプログラム・ファイルを保存したいと思うでしょう。

 そこでパスの設定が必要になります。というのも,c:\borland\bcc55\bin以外のほかのフォルダがカレント・ディレクトリになっている場合は,bcc32.exeがどこにあるか,Windowsが理解できなくなってしまうからです。Windowsには,「c:\Borland\Bcc55\binにbcc32.exeがあるよ」と教えてやらなければなりません。そうしないと「bcc32.exeが見つからないよ」というエラーになります。

 パスを設定するには,Windows 2000/XPの場合,マイコンピュータを右クリックし,プロパティを選びます。詳細設定(Windows 2000の場合,詳細)タブを選び,環境変数ボタンを押します。続いてシステム環境変数のPathを選び,編集ボタンを押します。変数値(Windows 2000の場合,値)の末尾に,すでに入力されている文字列に続けて

 ;C:\borland\bcc55\Bin

と入力してください。これでOKボタンを押して登録すれば,どこのフォルダがカレントになっていても,bcc32.exeを実行できるようになります。なお,この変更は次にコマンドプロンプトを起動したときに有効になります。

 もう一つやっておくことがあります。readme.txtの「2のbとc」に,「bcc32.cfgとilink32.cfgというファイルを作るように」と書いてあります。bcc32.cfgファイルの目的は,コンパイラに「インクルード・ファイル」と「ライブラリ・ファイル」というファイル群の存在する場所を指定することです。

 bcc32.cfgを作らないと,コンパイルできないというわけではないのですが,作らない場合はコンパイルを行うたびに,インクルード・ファイルとライブラリ・ファイルの場所を指定しなければなりません。bcc32.cfgという設定ファイルを作っておけば,コンパイルが容易になるのです。

 readme.txtの

 -I”c:\Borland\Bcc55\include”

 -L”c:\Borland\Bcc55\lib” という2行をコピーして,テキスト・エディタの別のウィンドウにペーストします。そしてファイル名を「bcc32.cfg」として,c:\borland\bcc55\Binフォルダに保存します。

 同様に

 -L”c:\Borland\Bcc55\lib”

の1行も,コピーアンドペーストして,ilink32.cfgというファイル名で同じフォルダに作成してください。

 それから,c:\borlandの中に,ソース・プログラムを入れておく場所として,srcなどの名前でサブ・フォルダを作っておくとよいでしょう*8。これで準備はOKです。

最初のプログラムを作って実行する

 さて,いよいよC言語のプログラムを書いてみましょう。

 ソース・プログラムはエディタと呼ばれるソフトウエアを使って書きます。いつもお使いのエディタがあれば,それをお使いください。何もなければ,メモ帳でも構わないのですが,インターネットで「エディタ フリー」などのキーワードで検索してC言語のソース・プログラム編集に適したものをダウンロードされるとよいでしょう。

 それでは最初のプログラムを書いて,コンパイル/実行してみましょう(リスト1)。画面にHelloと表示するだけのプログラムです。c:\borland\srcにエディタで作成して「hello.c」というファイル名で保存します。C言語のソース・プログラムだから拡張子はcになります。

リスト1● 最初のC言語のプログラム。画面に 「Hello」と表示する

ここでコマンドプロンプトを起動し,カレント・ディレクトリをc:\borland\srcに移動して(前述のcdコマンドを使って),

 bcc32 hello.c

と入力してコンパイルします(図2)。コンパイル・エラーがなければ,hello.exeという実行ファイルが作成されます。

図2●リスト1のプログラム(hello.c)をコンパイルする様子

 作成したプログラムを実行するには,

 hello

と入力します(図3)。画面にHelloと表示されました。

図3●コンパイルしたhello.cを実行した

 ここで一つ,覚えておいていただきたいことがあります。実はコンパイル処理だけで実行ファイルができるのではありません。自分の書いたソース・プログラムをコンパイラがオブジェクト・ファイル(マシン語に翻訳されている)として出力します。そのオブジェクト・ファイルを,リンカーというプログラムが,標準ライブラリとリンク(結合)して,実行ファイル(この場合ならhello.exe)を作成します(図4)。Borland C++ Compilerでは,オプションで「コンパイルのみ」を指定しない限り,エラーがなければ,自動的にリンク処理が呼び出されます。bcc32.cfg ファイルに

-L”c:\Borland\Bcc55\lib”

と記述したのは,標準ライブラリの場所を示すためだったのです。

図4●オブジェクト・ファイルと標準ライブラリ・ファイルをリンカーがリンクして実行ファイルを生成する

プログラムを読んでみる

 連載1回目ですから,あまりややこしい話はしたくないのですが,ソース・プログラムの説明をしないわけにはいかないので,少し我慢してお付き合いください。

 C言語は「構造型言語」と呼ばれます。アセンブリ言語のように,コードをズラズラと続くような書き方はしません。プログラムは一つ以上の複数の関数で構成されます。リスト1をもう一度見てください。

 まず,mainという名前の関数(2)が必要です。関数の始めと終わりは(3)と(6)のように「{」と「}」で示します。プログラムはmain関数の1行目から実行されます。このサンプルでは(4)のprintf関数から実行されます。printf関数は「標準ライブラリ」に登録されている関数です。標準ライブラリは,画面への表示のように,プログラマの誰もがよく使う処理を集めた道具箱のようなものです。

 printf関数は,ダブルクォーテーション(”)に囲まれた文字列を出力します。リンカーが標準ライブラリの一部を,自分の作ったプログラムにくっつけてくれるので,printf関数を使って画面に文字列を出力できるのです。

 標準ライブラリとして,多くの関数が用意されていることがアセンブリ言語に比べて,プログラミングの効率が良い理由の一つです。C言語では,用意されている(あるいは自分で作った)関数を組み合わせて,プログラムを構成していくわけです。

 ソースコードの1行はセミコロン(;)で終わります。関数は,中学校の数学の授業で習った関数と同じで,値を返します。(5)のreturn文が0という値を返しています。

 (1)の「#」ではじまる命令はプリプロセッサ命令と呼ばれ,Cコンパイラがソース・プログラムを解釈する前に実行されます。#includeは<と>の中に書かれている拡張子.hのヘッダー・ファイルをインクルード(取り込み)します。stdio.hに書いてある内容が,そのまま自分のプログラムの中に展開されるのです。bcc32.cfgに記述した

 -I”c\Borland

 \Bcc55\include”

は,このヘッダー・ファイルのありかを示していたのです。  これで,プログラムを作成し,コンパイル,実行するまでの手順がおわかりいただけたと思います。次に,ちょっと面白いことをしてみましょう。

アセンブリ・コードと比べてみる

 Cコンパイラの多くは,アセンブリ・リストを出力する機能を備えています。C言語で書いたソースコードをアセンブリ言語のコードとして出力することができるのです。例えばBorland C++ Compilerでは,次のようにオプションに「-S」を指定すると,アセンブリ・コードを出力してくれます。

 bcc32 -S hello.c

なお,オプションの指定では,大文字と小文字が区別されますので注意してください。

 実際に実行してみると,この例では,hello.asmというファイルが作成されます。一部を抜粋してみました(リスト2)。構成がわかりにくいかもしれませんが,セミコロンに続くコードが元のCのコードで,それ以外の部分がアセンブリ言語のコードです。

リスト2●hello.cから生成したアセンブリ・コード

なにやらズラズラと書いてありますが,コードの内容がわからなくても大丈夫です。冒頭で説明したように,ニーモニック・コードとマシン語は1対1で対応します。しかし,Cのコードとアセンブリ言語のコードは1対1にはなっていませんね。C言語のコード1行から,複数行のアセンブリ・コードが出力されています。

 ソースコード1行から,複数行のアセンブリ・コード,つまりマシン語が出力されるのですから,C言語はアセンブリ言語よりもプログラムの開発効率が高いということができます。「C言語って,マシン語やアセンブラでプログラムを作るよりも便利なんだ」――そう感じてもらえれば,連載第1回の目標は達成です。

第2回 変数の性質を理解しよう

皆さん,こんにちは。この連載「よくわかるC言語」は,今回が2回目です。前回はC言語のソース・プログラムをアセンブラのコードとして出力して,Cのソースコード1行が複数のアセンブラのコードに対応していることを確認しました。なるほどC言語は“高級”な言語なのだと,感じていただけたのではないでしょうか。

 さて,皆さんがプログラミングをしていて,“こう書くとこうなるけど,その理由はわからない”という方はいらっしゃいませんか。でもあまり心配する必要はありません。なぜそうなるのかの理由がわからないのは,たぶんプログラミングがわからないのではなくて,コンピュータの動作原理などの基礎知識が不足しているだけです。今回のテーマは「変数」ですが,この“コンピュータの仕組み”に重点を置いて説明していきます。ぜひ最後まで読んでみてください。ぼんやりしていた部分がはっきりすることでしょう。

変数はメモリーの一部に名前を付けたもの

 では早速始めます。変数そのものを解説する前に,プログラムが扱う様々なデータを,コンピュータがどのように“記憶”するかを確認するところからお話ししましょう。

 コンピュータがデータを記憶するには,メモリーかハードディスクを利用します。メモリーは,CPUが直接アクセスできる記憶装置です。半導体素子を利用して,データを電気的に記録します。動作は高速ですが,電源を切ると内容が失われてしまいます。情報処理の用語では「主記憶装置」と呼ばれています。

 電源が切れたら消えてしまうと困るようなデータを記憶させるときには,皆さんご存じのハードディスクを利用します。フロッピ・ディスクやCD-Rなどを利用することもありますね。これらは「補助記憶装置」と呼ばれています。

 ちょっと当たり前すぎて簡単に感じられたかもしれません。ここからが大事なところです。補助記憶装置に記憶されたデータをコンピュータが利用するときには,必要なものだけを主記憶装置(メモリー)に読み込んで利用します。やみくもに読み込むだけでは,何がどのデータだかわかりにくくなってしまいます。なのでプログラミング言語からは,メモリーの一部を,自分が付けた名前で扱えるようになっています。これが変数です。

char型の範囲が-128~127って?

 となると,変数について正しく理解しないと,目的に合った正しいプログラムを作るのは難しいですよね。変数とはどのようなものかをもう少し詳しく見ていきましょう。

 変数には「型」というものがあります。型は,その変数が(1)どのような形でデータを格納するかと,(2)一つの変数がメモリーをどれくらい必要とするかの二つを定めたものと考えてください。C言語の代表的な型と表現できる値の範囲は,表1のようになります。10年いや20年ぐらい前のことですが,筆者が初めてこのような表を目にしたとき,char型は文字を表すのに,どうして-128~127って書いてあるの? と思いました。誰でも最初は素人なのです。

表1●C言語の主な変数の型と,表現できる値の範囲

前回,説明しましたように,コンピュータではプログラムもデータも,すべてオンとオフの2値の情報として,2進数のイメージで情報を扱います。メモリーの中に0と1がびっしり埋まっている様子を想像してください。2進数はご存じですよね。2で桁上がりする数のことです。例えば日頃使っている10進数の0,1,2,3,4を2進数で表すと0,1,10,11,100となります。この0と1の並びをどんどん,ながーく伸ばしていけば大きな値も扱えることに疑問の余地はありません*1

コンピュータは,数値に限らず,文字も,音声も,画像も,2進数の0と1の並びで情報を扱います。コンピュータが生まれた国のアルファベットはもちろん,ひらがなも,漢字も,あらゆる文字も0と1の組み合わせで表現します。

 複数のコンピュータで互いに情報をやりとりするためには,どの文字を,どの0と1の組み合わせで表現するかをあらかじめ決めておかなければいけません。この決まりを「文字コード」といいます。

 もっとも代表的な文字コードは,ANSI(米国規格協会)が1962年に制定したASCII(アスキー)コードです*2。ASCIIコードでは,例えば1000001という7ビットの並び(16進数では41*3)がAを表し,1100001(16進数では61)はaを意味します。「1」という“文字”は0110001(16進数では31)です。表2のように,A,Bなどの可読文字だけではなく,NULやSOHなどの制御文字*4を含め128のコードが制定されています。

表2●ASCIIコード表。例えば「A」は上位3ビットが4,下位4ビットが1なので,コードは16進数で41とわかる

128種のコードなら7ビット,つまり1バイトで1ビットの余裕を持って表現できますよね。なので,C言語では文字は1バイトのchar型で表します。でも,漢字を含む日本語の文字は種類が多いので1バイトでは表現できません。WindowsではシフトJISコード,UNIX系OSではEUCコードなどの2バイトのコードを使って漢字やかなを表現します。また,近年では,1バイトで表現可能な文字も,漢字と同様に2バイトを使って表すUnicode(ユニコード)も広く使用されています。

 説明ばかりで少し退屈してきましたね。プログラムを作って,C言語で文字を変数に代入したとき,どのように扱われているかを見てみましょう。リスト1は,変数に文字や値を代入し,様々な形で標準出力(ディスプレイ)に出力してみるプログラムです。

リスト1●変数に文字や値を代入し,様々な形で標準出力(ディスプレイ)に出力してみる

実行結果は図1です。この実行結果とリスト1のコードを照らし合わせて,char型変数の中身をイメージしていきましょう。まず,リスト1の(1)でchar型の変数を四つ宣言しています。c1,c2,c3,c4という名前(識別子)で指し示すことができる1バイトの箱を,メモリーに四つ作成したと考えてください。なお,c1,c2という変数名には特に意味はありません。C言語では英字・数字・アンダースコア(_)の組み合わせを識別子として使用できます。変数に名前を付けるときは,「数字で始めることはできない」「大文字・小文字は区別される」「C言語の予約語(例えば,intやreturn)を使うことはできない」――などの規則があることを覚えておいてください。

図1●リスト1の実行結果

リスト1の(2)でc1に「A」という文字を代入しています。C言語では文字はシングルクォーテーション(’)で区切ります。このc1の値を画面に表示するコードは,すぐ下にある(3)のprintf関数です。printf関数は書式付きで文字列を出力します。同じ値を文字として出力したり,10進数,16進数で表示することができます。

 printf関数の第1引数には文字列を指定します。文字列は,複数の文字の並びのことで,文字とは違い,ダブルクォーテーション(”)で区切ります。文字列中の%c,%x,%dなどを変換仕様といい,第2,3,4引数がこの変換仕様の部分に展開されていきます。文字として出力するには%cを,16進数として出力したい場合は%xを,10進数の整数として出力する場合は%dを指定します。第2,3,4引数はどれもc1です。図1の実行結果を見ると,

文字:A 16進数:41 10進数:65

と表示されていますね。文字としては「A」なのですが,メモリー内部では16進数で41,つまり2進数で01000001というビットの並びで表現されていることがわかります。

 さて,printf文の第1引数で指定した文字列の中に,画面に表示されていない文字があります。「\n」です。\nは改行を意味する「エスケープ・シーケンス」です。エスケープ・シーケンスは,画面に表示できる文字だけで,制御文字を入力するための仕組みです。\nのように,「\」と「n」を続けて入力すると改行の制御文字を表すことができます。なのでリスト1の(3)の実行を終えたところで,1行改行しているわけです。なお,「\」という文字そのものを表したい場合は,「\」と\を二つ続けて入力します。

 (4)の\x61も,2桁の16進数で61を表すエスケープ・シーケンスです。(5)で文字として表示させると,aと表示されます。(6)と(7)は文字としての「1」の表示です。文字の1は,16進数で31,2進数で00110001というビットの並びです。

 (8)と(9)は変数c4に代入した\nを文字として出力しています。図1を見ると,「\n」と表示される代わりに,行が変わっていますね。改行を意味する制御文字を表示しようとすると,画面では改行として扱われます。\nは16進数ではa,10進数では10と表示されています。2進数では00001010です。

足し算で小文字を大文字に変える

 一つの英数字が,メモリー上では2進数8桁で表されているというイメージをつかむことができたでしょうか。次に,キーボードから入力した英字の大文字を小文字に変換するプログラムを考えてみましょう。

 英字の大文字はA=41(16進)からZ=5A(16進)まで,順序よく並んでいます。小文字もa=61(16進)から,お行儀よく整列しています。char型の変数といっても中味は数値ですから,文字と文字を足し算したり,引き算したりと,お互いに計算させることができます。Aとaの数値としての差と,Zとzのそれに違いはないので,入力された大文字に’a’-’A’の値を足してやれば,小文字になるはずです。

 この仕組みをプログラムとして書いたのがリスト2です。(1)で宣言したchar型変数cに,(3)のscanf関数で文字を入力しています。

 scanf(“%c”,&c);

の「%c」はprintf関数に指定したものと同じ変換仕様です。標準入力(キーボード)から文字を1文字読み込みます。

リスト2●大文字を小文字に変換する

第2引数にある「&c」の「&」はアドレス演算子といい,指定した変数のメモリー上の場所(アドレス)を取得します。変数のアドレスについては,今後の連載でじっくりと説明しますが,scanf関数は変数という一つの箱に値を入れるときに,変数の名前ではなく,“場所を教えてくれ”と要求する関数なのだと理解してください。ともかくこのように書くと,char型の変数cに文字を読み込むことができます。

 さて,aとAの差は,リスト2の(2)でint型(整数型)の変数diffに入れています。int型は4バイトで整数を記憶するデータ型です。整数値として差異を求めることが目的なので,char型ではなくint型を使用しています。(2)のように変数は宣言と同時に値を代入し,初期化することができます。(4)では,入力された文字にdiffを足して小文字に変換しています。

 実行結果は図2です。「J」が「j」に変換されていますね。なお,このサンプル・プログラムでは,入力された値のチェックを行っていません。実用的なプログラムにするには,入力値が英字大文字の範囲内にあるかどうかをチェックする必要があります。

図2●リスト2の実行結果

コンピュータは小数点以下の計算が苦手?

 ここまでは,char型(文字型)とint型(整数型)の変数を使ってきました。それぞれunsignedと付いている型は符号なしで正の値のみを扱うための型です。各変数のサイズはコンパイラやOSの種類によって異なる場合がありますが,Borland C++コンパイラで作成したプログラムは,int型は4バイト,char型は1バイトのメモリー領域を使用します。

 整数型には収まらないような大きな値や,小数点以下の値を含む実数を扱うときに,C言語ではfloatやdoubleといった浮動小数点数型を使います。科学技術計算などでよく用いられ,実際とても便利なのですが,計算すると誤差が出る場合があり注意が必要です。

 サンプル・プログラムで見てみましょう(リスト3)。0.2をfloat型の変数に111回足して,その値が22.2と等しいかどうかを調べ,等しい場合は「0.2を111回足すと,22.2になります。」と表示するものです。

リスト3●浮動小数点数型の変数に0.2を111回足して,結果が22.2になるかを試す

リスト3を読むためには,「++」など,C言語特有の演算子と制御文を理解する必要があります。リスト3の内容に入る前に簡単に解説しておきましょう。リスト3の(2)で登場する「++」は,インクリメント演算子と呼びます。「i++」と書くと,iに1を加算します。Visual Basicなどでは単純にカウンタの値を1アップさせるときでも,i = i + 1と書かなくてはならないので,「C言語みたいにi++と書ければ良いのに」と筆者は面倒に感じます。減算はデクリメント演算子を使ってi–と書きます。

 なお,インクリメント演算子とデクリメント演算子は,++iのように変数の前に記述することもできます。変数を単純にインクリメント,デクリメントするだけなら,演算子を前に置く(前置)場合と後ろに置く場合(後置)に違いはありません。でも,式の中で使う場合は注意が必要です。

 例えば,aが0のとき,後置インクリメントで

 b = a++;

を行うと,aの値は1になりますがbの値は0です。

 b = ++a;

と前置にするとaとbの両方が1になります。後置では,bにaを代入してからaをインクリメントし,前置ではaをインクリメントしてからbに代入するという違いがあるのです。

 ほかに,C言語で利用する主な演算子を表3にまとめておいたので参照してください。制御文の書き方は,コードを追いながら解説しましょう。

表3●C言語の主な演算子

というところで,リスト3の解説に入ります。(1)でfloat型の変数fを宣言すると同時に,0を代入して初期化を行っています。(2)のfor文が0.2を111回繰り返し加算する制御文です。forのカッコの中にある「i=0;i<111;i++」は,iが0から111より小さい間,一つずつiの値を増やしながら繰り返しをする,という意味です。なので,iが0から110まで順番に,(3)の文を繰り返し実行します。

 (3)のf+=0.2もC言語の便利な書き方で,意味は上の行にコメント*5として記述してあるように,f = f + 0.2です。もちろんf = f + 0.2と書くこともできます。減算や掛算,割り算も同様に,-=,*=,/=と記述することができます。くだくだ書かなくてもよいところが,C言語がプログラマに好まれる理由の一つかもしれません。

0.2を111回足した値が22.2と等しいかどうかを判断しているのが(4)のif文です。if文は条件分岐を行う制御文です。この例ではelseとともに記述していますので,fが22.2と等しいとき(5)の文を実行,else(それ以外)のとき(6)を実行します。==は等しいかどうか比較する演算子です。比較演算子はほかにも表3のように用意されています。

 さて,このプログラムの結果はどうなるでしょうか? 意外かもしれませんが,「0.2を111回足しても,22.2になりません。」と表示されるのです(図3)。何度実行しても22.2にはなりません。「0.2を111回足せば,22.2に決まっているじゃないか!」と普通,思いますよね。でも浮動小数点数型の計算では,小さな違いが出てしまうのです。

図3●リスト3の実行結果

皆さんは,1÷3が割り切れず,0.3333…という「循環小数」になることはご存じですよね。0.3333…を3回足しても1にはなりませんね。一方,1÷2は0.5ぴったりの「有限小数」です。

 今回リスト3で利用した0.2は,私たちになじみの深い10進数では有限小数ですが,実はコンピュータにとっては循環小数なのです。コンピュータの中ではあらゆる情報が2進数で扱われています。10進数ではきりのよい数値が,2進数では扱いにくい数になることがあるのです。2進数になじみがないとイメージしにくいかもしれません。あわてず,ゆっくり考えていきましょう。

 10進数の9999は,10の0乗×9 + 10の1乗×9 + 10の2乗×9 + 10の3乗×9と表すことができます。同様に,2進数の1011は2の0乗×1 + 2の1乗×1 + 2の2乗×0 + 2の3乗×1と表すことができます。

 10進数では桁上がりすると,10倍,100倍…となっていきます。一方,2進数の場合は2倍,4倍…です。では,小数点以下はどうなるのかと言うと,10進数では,1/10,1/100…ですから,2進数では,1/2,1/4…と半分,半分になっていくのです。ここで,10進数の0.2を2進数で表すために2進数の各桁の値を足していくと,0.001100110011…と,0011を繰り返す循環小数になってしまいます。

 このように,10進数では有限小数だった数が,2進数では循環小数になってしまうことがあるのです。というわけで浮動小数点数型を使うときは,乱暴な言い方に聞こえるかもしれませんが,「近似値を求めるのだ」と最初から割り切っておくことが必要でしょう。金属の金は,フォーナイン(999.9)で純金ですし,工業や科学技術計算では許容できる誤差があります。例えば,このサンプル・プログラムの場合,完全に一致していなくても,22.2との差が0.0001未満なら一致していると判断してもいい場合があるのです。しかし,どうしても困る分野もあります。銀行の金利計算や為替の計算では,近似値というわけにはいきません。コンピュータの都合で,誰かが損をすると困ってしまいます。

 そのために,例えばVisual Basicには通貨型(Currency)が用意されていたり,データベースにもMoney型などの正確に金額を記憶するための型があるのです。また,事務処理分野で古くから利用されてきたCOBOL言語には,BCD(Binary Coded Decimal:2進化10進数)という形式があります。BCDでは10進数の0から9までを,4桁の2進数で表現します。185は,1と8と5をそれぞれ,0001,1000,0101と表すことができます。これに小数点が何桁目の位置にあるかという情報があれば,人間と同じように10進数で計算することができるのです。

 では,そういうデータ型が用意されていないC言語ではどうすればよいのかと言うと,計算は整数で行い,計算結果を小数点以下の値を含む値に戻すという方法が考えられます。0.2を111回足した結果を表示するのなら,2を111回足して,最後に10で割って答えを出すようにすればいいのです。

第3回 制御構文がわかればプログラムの「流れ」がわかる

「よくわかるC言語」,今回は連載3回目です。1回目はBorland C++コンパイラ*1のオプションを指定して,C言語のコードの1行から複数のアセンブリ・コード,つまりマシン語が出力されることを確認しました。2回目は浮動小数点数の計算に誤差が出る理由などを説明し,変数の性質を学びました。さて今回は,プログラムの構造を説明し,「C言語のプログラムはどのように書けばよいのか」を大まかに理解してもらうことを目標にします。カメラのズームをいったんぐっーと引くように,ワイドな視界でC言語のプログラミングを眺めてみましょう。

基本は構造化プログラミングにあり

 皆さんは「構造化プログラミング」という言葉をご存知ですか? 1960年代後半,エドガー・ダイクストラ*2が中心となって提唱したプログラミングの考え方です。現在,主流となりつつあるオブジェクト指向プログラミングも,実は構造化プログラミングをベースとしています。

 構造化プログラミングの目的は,大規模なプログラムを効率よく作成し,プログラムのミス(バグ)を少なくすることです。そのための具体的な方法としてダイクストラは,プログラムに必要な手続きをいくつかの単位に分け,メインの処理に大まかな処理を,サブルーチンに細部の処理を記述せよと主張しました。また,プログラムは「順次」「反復」「分岐」の三つの基本構造で記述できるという構造化定理を証明し,結果として「goto文は不要である」と主張しました。

 「goto文は不要」と言われても,goto文自体を見たことがない方もいるでしょう。goto文というのは,プログラムの途中で,指定した行番号やラベルにジャンプできる制御文のことです。例えばgoto文を実装している初期のBASIC言語で書かれたプログラムは,goto文でプログラム中の行番号やラベルに飛んで,そこに記述してある処理を数行実行したあと,またgoto文でどこかへ飛んでいく――という構造になっていることが多かったのです(図1)。これでは,どこから来てどこへ行くのかわからないスーパーマンのようなもので,いつどのコードが実行されるのか,ロジックを追うことが難しくなります。「実行直前に他の場所に飛んでしまうので,永遠に実行されないコードがあった」なんてことも古いプログラムではよくあったのです。

図1●BASICにおけるgoto文のイメージ

そういった複雑さを読み解くことに,迷路やクイズのような楽しさを感じる方がいらっしゃるかもしれませんが,一般的に「わかりにくい」プログラムほど「迷惑」な話はありません。そこで以降は,ダイクストラの教えに従って,C言語で,

●プログラムを複数の単位に分けて作成する ●順次・反復・分岐の論理構造を作る

方法を解説していきましょう。

関数という単位で複数の単位に分ける

 C言語は,printf関数*3を始めとする汎用性の高い関数をライブラリ関数として標準で装備しています。しかし,装備している関数を使うだけで,すべてのプログラムを作成できるわけではありません。

 リスト1のCのプログラムを見てください。文字型の変数cに「文字」として0から9までの数字を代入し,printf関数に%c ,%xと変換仕様を指定して,文字と16進数を画面表示させるプログラムです(図2)。文法上は何の問題もありません。でも,何か冗長な感じがしませんか?先の構造化定理の中で,使っているのは「順次」,すなわちmain関数の上から下に向かって処理が実行されている部分だけですね。

リスト1●0から9までの「文字」と,その16進数を表示するプログラム。文字が増えればコードは長くなり,冗長である

図2●リスト1を実行した結果

これをリスト2のように書き直してみるとどうでしょう? outxという名前の自作の関数を作り(1),main関数から呼び出すようにしてみました(2)。main関数の内部では,繰り返しを行う制御文for(後述)を使って,変数cの値が文字としての‘0’から‘9’以下の間,処理を継続させています。

リスト2●リスト1を関数という単位で作り直したCのプログラム

リスト2のポイントは,

●main関数が処理の制御を行っている ●outx関数が画面への値の出力を行っている

の2点です。main関数とoutx関数の二つだけですが,プログラムを複数の単位に分けたわけです。

 このような「関数化」のメリットは何でしょう。まず「コードの量が少なくなる」という点が挙げられます。コードの量が多ければ多いほど,タイプミスなどが原因でバグが出やすくなりますから,関数化すれば間違える確率が下がります。リスト1のように「printf(“%c:%x “,c,c);」を何度も書くより,「outx(c);」を1行だけ書く方が,間違えにくいですよね。

 また,処理を関数としてうまく分割してやることで「プログラムがわかりやすくなる」ことも挙げられます。「この関数ではXXXの処理を行う」とシンプルなコメントで説明できる関数を作れば,メンテナンスも容易になるでしょう。

 それでもまだメリットを実感できない方は,main関数に500~600行とコードがズラズラ記述してあるプログラムをイメージしてください。筆者ならとてもそんなコードを読みたくありません。小さなプログラムなら,1人暮らし用の小さなワンルーム・マンションのように,トイレと風呂で一つの部屋(関数),それ以外で一つの部屋,で済むこともあるでしょう。しかし家を建てる時に1階を全部ワンルームにして,キッチンも子供部屋も客間も寝室も,あろうことかトイレもバスも仕切りなしに,ぜーんぶ1部屋に収めたら,メチャクチャになりますよね。たくさんの家族が住む2世帯,3世帯住宅のような大きなプログラムだと,それに応じた部屋数(関数)が必要になることは,十分納得いただけると思います。

 とはいえ,どの部分を関数として作成するかの判断には,ある程度のプログラミング経験が必要です。初心者の方は,最初のうちは目的の処理をザッーと書いて,そのあとで同じような処理を書いてあるコードを見つけて関数にしていけばいいでしょう。それだけでも,ずっと「わかりやすい」良いプログラムになるはずです。

C言語の関数は値渡しがデフォルト

関数の話題が出てきたので,せっかくですからC言語の関数の仕組みをもう少し掘り下げておきましょう。

 関数には値を受け取って処理を行い,その結果の値を返す仕組みが用意されています。リスト2では,main関数の前に「int」と書いてありますね。これは「型宣言」で,main関数がint型の関数であること,つまりint型の値を返すことを表しています。型について忘れてしまった方は,前回の本連載を読み返してください。

 main関数の最後で「return 0」とあるのは,return文で0を返すという意味です。C言語では,プログラムが正常終了した場合に「0」を返し,そうでない場合は違う値を返すようにプログラミングすることが多いのです。

 outx関数の型はvoidとなっていますね。voidは,この関数が値を返さないことを表します。Visual Basicプログラマの方なら,Subプロシジャのようなものだと思えばいいでしょう。

 関数の間で値を受け渡しする仕組みで重要になるのが,引数(ひきすう)です。引数には仮引数(かりひきすう)と実引数(じつひきすう)があります。リスト2で言えば,outx関数を定義した「void outx(char c)」の「char c」が仮引数で,main関数の「outx(c);」の「c」が実引数となります。つまり,関数を定義するときに指定する値が仮引数,関数を実際に呼び出す際に指定する値が実引数というわけです。

 ここで大事なことが一つ。C言語では,引数は値渡し(pass by value)で渡されるということです。変数である実引数そのものが,関数に渡されるのではなく,引数に指定した変数の「値」が関数に渡っていくのです。変数そのものが渡されるのと,変数の値が渡されることにどんな違いがあるのか,具体的に説明しましょう。

 リスト3のプログラムを見てください。kake関数を独自に定義し,main関数からkake関数を呼び出すプログラムです。main関数では変数aに3を,bに5を代入してkake関数を呼び出し,kake関数はaとbの値を受け取って,掛け算した結果をint型で返します。ポイントは,kake関数の中でaを2倍しているところです(1)。main関数の「printf(“%d X %d = %d \n”,a,b,c);」では,最初にaの値を,次にbの値を,最後にaとbを掛けた値cを表示するわけですから,「6 × 5 = 15」と表示しそうなものですよね。しかし,実行結果は図3のように「3 × 5 = 15」となるのです。kake関数側で受け取ったaを2倍しても,main関数側のaには何も影響がありません。これはいったい,どう考えればいいでしょうか?

リスト3●C言語の値渡しを確認するためのプログラム

図3●リスト3を実行した結果

話は簡単です。要するに,kake関数に変数aとbのコピーが渡されたのだと思えばいいのです。コピーの値を変更しても,コピー元には何も影響はありませんよね。例えば,ワープロソフトで作成したドキュメントをネットワーク経由で他のパソコンにコピーして編集しても,コピー元のファイルには影響がないのと同じ理屈です。

 こんな風に書くと,これが当たり前のように感じられるかもしれませんが,ほかのプログラミング言語,例えばVisual Basic 6.0はC言語とまったく逆です。キーワードByValを記述して,明示的に値渡しであることを宣言しないと,main関数側のaの値も変わってしまうのです。これを参照渡し(pass by reference)と呼びます*4。この値渡しの仕組みによって,C言語では関数の独立性が高くなっています。これらの関係をまとめると図4のようになります。

図4●仮引数と実引数のイメージ

for,while,if,switchで反復と分岐を実現する

さて,ダイクストラの「処理を分割する」教えがある程度理解できたところで,今度は「順次・反復・分岐の論理構造を作る」方法について説明しましょう。

 C言語では,決まった回数,処理を反復させるために「for文」を使います。また,ある条件が成り立つ間,処理を反復させるためには「while文」を使います。それぞれのコードの例とプログラムの流れを図示すると図5のようになります。for文には,反復処理を行うための初期値である式1,反復すべきかどうかを評価する式2,反復回数をカウントする式3の三つのパラメータが必要ですが,while文では条件式が一つあればいいことがよくわかると思います。

図5●for文とwhile文を使ったC言語のプログラムと流れ

ちなみにC言語には,for文やwhile文のほか,do~while文という制御文もあります。while文はループを開始する前に条件をチェックするのに対し,do~while文はループ中の処理を一度実行してから,続けて処理を行うかどうか条件を判断します。

 では,実際にfor文を使ったサンプル・プログラムを見てみましょう。乱数の値によって「大当たり!」と表示するプログラムです(リスト4)。大きくmain関数とran10関数の二つの関数に分かれ,main関数の中でfor文を使って1から10までの値aをランダムに計算しています。条件式から10回反復することがわかると思います。

リスト4●for文とif文を使ったプログラム

(1)のrandは,乱数を返す関数です。乱数の範囲は0から定数RAND_MAX以下の値になります。RAND_MAXはプリプロセッサ命令#includeでインクルードしているstdlib.hの中に定義されています。

 ran10関数は乱数発生プログラムとでも言うべきもので,

 return rand()%10 + 1;

で剰余演算子%を使い,rand関数が返した乱数を10で割った余りを求め,1を足しています。こうすると,1から10の範囲でランダムな数を得ることができます。

 main関数の

 srand((unsigned) time(NULL));

は,乱数の種(seed)を与えるものです。time関数はグリニッチ標準時(GMT)の1970年1月1日00:00:00 から現在までの経過時間を秒単位で表した値を返します。「種って何?」と言われそうですが,要するに乱数を計算するための素です。rand関数を使う前にsrand関数を実行しないと,同じ数ばかりがrand関数から返されてしまうのです。理屈で考えるよりも,やってみれば明白なので,リスト4のsrandの行を//ではさんで,コメントにして実行してみてください。同じ数ばかりが表示されるはずです

C言語でもgoto文は使えるがエラー処理に限定すべき  ダイクストラによって「不要である」と言われてしまったgoto文ですが,実はC言語でも,goto文を使うことができます。ラベルとgoto文を使って反復するプログラムを見てみましょう。

 >リストAのコードを入力してファイルを作成し,実行してみてください。1から10までの数値が表示されます(>図A)。goto文を使ってloopとendというラベルにジャンプさせ,あたかもfor文のような反復処理を実現しているわけです。

 しかしこのコードを見て,特にメリットを感じるところがありますか? 筆者からすれば,for文を使ってループさせた方が,よっぽど簡潔に見えます。

 goto文とラベルは,特定の処理,例えば処理の途中でエラーが発生したときに,エラー処理に飛ばすといった使い方に限定した方が良いでしょう。

リストA●C言語でgoto文を使ったコード

図A●リストAを実行した結果

リスト4には,反復だけでなく,分岐の構文も登場しています。「if文」です。if文を使って,aの値が10と等しいかどうかを評価し「真」ならば「大当たり!」と表示させています(図6)。「これではあまりにシンプルすぎる。評価の結果で偽となったとき,別の処理を行わせたい」ということならば,「if~else文」を使えばいいでしょう。リスト4の(2)の部分をリスト5のように,if~else文に置き換えて実行した結果が図7です。

図6●リスト4を実行した結果。コンパイル時に警告が出るがプログラムは実行可能

リスト5●リスト4の(2)の部分をif~else文に変更したコード

図7●リスト5の変更を加えて実行した結果

分岐は,if文やif~else文だけではありません。一つの式が返す値によって何種類も分岐を作りたいときは「switch文」を使います。リスト6は,今日の運勢を占うプログラムです。リスト4のran10関数のロジックをそのまま使って乱数を発生させ,その値によって出力するメッセージを変えています(図8)。乱数aの値が1だと「最高!」,2だと「いいですよ!」と表示します。乱数が1,2,9,10のいずれでもないときは,default以下が実行され,「ぼちぼちかな」と表示します。このようにswitch文を使うと,複数の分岐をすっきりと表現することができます。

リスト6●switch文を使ったプログラム

図8●リスト6を実行した結果。コンパイル時に警告が出るがプログラムは実行可能

switch文で注意してほしいのは,break文の存在です。条件に一致して,該当のメッセージを出力した後のbreak文を書き忘れると,次のcase文に記述された処理まで実行されてしまうからです。他のプログラム言語では,break文が不要な場合があるので,筆者もついつい忘れてしまいがちです。皆さんも気をつけてください。

関数の順番を意識させないプロトタイプ宣言

 さて,main関数のほかに,もう一つの関数を持つプログラムをいくつか見てきました。これらのリストを見て,何か気が付いた点はありませんか? それは,もう一つの関数が,必ず“main関数の前に”書かれていた点です。プログラムを上から順番に読んでいくことを考えると,いつまでもmain関数が出てこないことにイライラした方もいるでしょう。さらに言えば,(これまであえて説明しませんでしたが)リスト4~6まで,コンパイルすると出力される警告メッセージ「W8065~プロトタイプ宣言のない関数~」が気になっていた方もいらっしゃるかもしれません。

 では,関数の順番を入れ替えれば警告が出なくなるのでしょうか。実際にリスト2のプログラムの関数の順序を入れ替えて,main関数の下にoutx関数を持っていきましょう。先ほど作った実行ファイル(拡張子exe)を削除してからコンパイルすると,「** 1 errors in Compile **」と今度はコンパイル・エラーが表示され,実行ファイルそのものが作成されなくなってしまったはずです。

 困りましたね。でも心配無用です。ここで,リスト7の(1)のように,main関数の前に

void outx(char c);

と記述して,もう一度コンパイルし直してください。するとエラーも警告も出ず,実行ファイルが作成されます。

リスト7●プロトタイプ宣言を実装したプログラム

この,魔法のような1行が,「プロトタイプ宣言」です。C言語のコンパイラはソースコードをファイルの先頭から順に解析していきます。outx関数についての情報がない状態でいきなり,main関数の中の「outx(c)」の行にコンパイラが出会ったら「引数にchar型の変数が指定されているけど,これでいいのか?」「outxは本当に値を返さないのか?」とコンパイラは判断に困ってしまいます。エラーが出たのはそういうわけだったのです。

 でもプロトタイプ宣言をしておけば,あらかじめこの関数は引数をいくつ取って,型は何とかで,戻り値の型は何であるかという原型(プロトタイプ)をコンパイラに示しておけます。これなら,コンパイラは判断に困りません。エラーも出ないで済むというわけです。

 今回は関数といっても二つだけですが,関数が数十個あって,ある関数が別の関数を呼び出し,その関数が…となるようなプログラムを作ることを想像してみてください。関数の順序などとても意識していられません。しかし,プログラムの先頭にプロトタイプ宣言をまとめて書いておけば,関数の順序を気にする必要はなくなるのです。

 ちなみに,プログラムで使うすべての関数のプロトタイプ宣言をコードの先頭に書かなくてはならないかというと,そんなことはありません。randやsrand関数を使うリスト4のプログラムでは,stdlib.hを,time関数を使うためにtime.hをインクルードしました。これらの関数を使うにあたってコンパイル・エラーが起こらなかったのは,実はそれぞれの関数のプロトタイプ宣言がstdlib.hやtime.hに記述されていたからなのです。

 つまり,標準ライブラリ関数と同様に,

void outx(char c);

という1行だけを記述したmyfunc.hというヘッダー・ファイルを作り,includeプリプロセッサ命令で取り込むようにすれば,たくさんのプロトタイプ宣言をプログラムの中に書く必要はなくなります*5

さて,まだまだ,おぼろげかもしれませんが,プログラムの流れをどう記述するかということがわかってきたでしょうか。もし「何となく自分でもプログラムを書いていけそうだ」という気分になってもらえたら,今回の筆者の目標は達成です。次回は変数のスコープとアドレスについて解説したいと思います。ぜひ,お読みください。

CPP32でプリプロセッサの結果を出力する  includeプリプロセッサ命令で,ヘッダーファイルを取り込むと説明しましたが,プリプロセッサは,構文解析をはじめとするコンパイル処理に先立ってソースコードに対してテキスト処理を行う仕組みです。

 Borland C++コンパイラ(BCC32)では,プリプロセッサと構文解析を同時に行うので,プリプロセッサの結果を意識することはできませんが,付属のツール「CPP32」を使えば,プリプロセッサの結果を出力させることができます。

 例えば,CPP32 ソースファイル名とコマンドプロンプトで実行すると,sample.iというファイルが作成されますので,テキスト・エディタで内容を確認してみてください。もし,stdio.hをインクルードしていれば,図Bのようにヘッダー・ファイルの内容が展開されているでしょう。-Pパラメータで,ヘッダー・ファイルなどが表示されなくなります。

図B●ヘッダー・ファイルstdio.hがソースコード中に展開されている様子

第4回 変数のスコープをアドレスを使って理解する

前回は,C言語がプログラムを複数の関数に分ける関数型の言語であることを説明し,制御文を使って順次/反復/分岐のロジックを作る方法を紹介しました。その前の第2回では,変数には“型”があり,浮動小数点数型では小数の計算に誤差が出ることを2進数で説明しました。そろそろ自分が作りたいプログラムを独力で作れるような気がしてきたでしょうか?

 さて,今回のテーマは変数のスコープ(scope)です。実際にプログラムを書くには,変数の型だけでなく,その働きや仕組みを理解しなくてはなりません。「変数をどこに宣言すればよいのか,なんとなくわかるようになる」――それが今回の目標です。

ローカル変数とグローバル変数の違いはスコープにあり

「変数を定義する」ということは,「メモリーの一部を自分が付けた変数名(識別子)で扱えるようにする」ことです。例えばリスト1のプログラムをコンパイルすると,変数aが二つ宣言されているというコンパイル・エラーが表示されます。コンパイラの身になってみればわかりますね。「a=1」と代入するときに,どっちのaなのか特定できないのでエラーになるわけです。つまり,一つの関数の中に,同じ名前の変数を複数個定義することはできないということがわかります。

リスト1●一つの関数内に,同じ名前の変数を複数定義することはできない

 リスト2はどうでしょう。main関数とoutx関数の中で,それぞれ変数aを定義しています。main関数ではaを1で初期化し,outx関数では10で初期化しています*1。このプログラムはコンパイル・エラーになりません。普通に実行でき,「10,1」と表示します(図1)。outx関数で「a=10」と値を代入してもmain関数側のaには影響がないので,二つの変数aは別物であると考えることができます。つまりmain関数で宣言した変数aはmain関数内で有効で,outx関数で宣言した変数aはoutx関数内で有効なわけです。

リスト2●二つの関数で,それぞれ同じ名前の変数を定義することは可能

図1●リスト2を実行したところ

関数内で宣言した変数のことを「ローカル変数」と呼びます。また,変数が有効であるプログラム中の範囲のことを「スコープ」と呼びます。リスト2の実行結果から言えることは,「関数内で宣言されたローカル変数のスコープはその関数の中にある」ということです。

これに対し,関数の外で宣言した変数のことを「グローバル変数」と呼びます。例えばリスト3では,関数の外でグローバル変数aを宣言しています。順番に処理を追ってみると,

(1)main関数でaに1を代入

(2)outx関数でaに10を代入

(3)outx関数でaを表示

(4)main関数でaを表示

</strong>

リスト3●関数の外で定義した変数はグローバル変数になる

となるので,リスト2のように「10,1」と表示するような気がしますね。でも,結果は図2のように「10,10」と表示します。

図2●リスト3を実行したところ

このことからグローバル変数のスコープは,関数の中だけという制約がなく,プログラム全体で有効,つまり「グローバル変数のスコープはプログラム全体である」ということが言えるわけです*2

変数名が同じ場合ローカル変数が優先する

 では,グローバル変数と同名のローカル変数があったらどうなるのでしょうか。リスト4を見てください。

リスト4●グローバル変数とローカル変数で同じ変数名を使ったコード

 「int a=0」と,宣言とともに初期値として0を与えた変数aはグローバル変数です。main関数の中で1を代入し,printf関数で画面に表示しています。ポイントは,outx関数の中でもローカル変数aを宣言している点です。このプログラムをコンパイルしてもエラーにはなりません。同じ名前のグローバル変数とローカル変数を宣言することをコンパイラは許可しているからです。

 となると疑問点はただ一つ。outx関数の中で「a=10」と10を代入しているのは,グローバル変数のaなのか,ローカル変数のaなのか,どちらなのでしょう? グローバル変数のスコープはプログラム全体で,ローカル変数のスコープは関数の中ですから,スコープがぶつかっているように思えますよね。

 その答えは,実行結果を見るとすぐわかります(図3)。「10,1」の順で表示されていますね。outx関数の中で10をaに代入しても,main関数側のprintf関数の出力結果は1なので,「a=10」で10を代入されたのはローカル変数のaということになります。つまり同名の変数が宣言されてスコープが重なった場合は,ローカル変数がグローバル変数を隠ぺいするのです。同名のローカル変数が有効な範囲では,グローバル変数が雲に隠れるようなイメージだと思ってください。

図3●リスト4を実行したところ

ところで,先ほどローカル変数のスコープは変数を宣言した関数の中だと説明しましたが,正確に言うと,これは間違いです。関数の始まりと終わりを示す中カッコ「{ }」にはさまれたブロックの中がローカル変数のスコープです。関数ブロックの中で宣言されたローカル変数は,閉じカッコ「}」が現れるまで有効なのです。実際に確かめてみましょう。極端な例ですが,図4のように関数ブロックの中に中カッコ({ })でブロックを作り,そのブロック内だけで有効なローカル変数を定義してみます。どうなると思いますか? 1番内側の最もローカルな変数aの値から順に出力していますので,実行すると「6,5,4」と表示されるのです。

図4●スコープを確認するコード。色が重なっている部分はグローバル変数a,関数ブロック内で宣言されたローカル変数aが隠ぺいされる

変数を生き物にたとえると,宣言された変数の型に応じて必要なメモリー領域が確保されることで始まり,値を代入したり出力に使ったあと,メモリーから解放されて一生を終えます。グローバル変数はプログラムの開始時にメモリーが確保され終了時に解放されるので,静的変数(恒久的変数)とも呼びます。それに対し,ローカル変数はブロックの始まりでメモリーに確保され,ブロックの終わりで解放されます。通常は,関数ブロック内で発生し,関数が終わるときに消えてしまいます。ですから,一時的変数(自動変数)などとも呼びます。恒久的だの一時的だの,変数にもいろいろな人生(変数生?)があるのだと思えば,変数を丁寧に扱うプログラムを書けるようになるかもしれませんね。

変数のアドレス

 このあたりで,少し視点を変えてみましょう。変数を定義することは,メモリーのどこかに自分の付けた変数名で扱える場所を作ることでしたね。場所を確保すれば,値を記憶させたり,読み出したりすることができます。

 どの場所に値を記憶させたかを判別するため,メモリーには「アドレス」という番号が振られています。現実世界でいうと,住所とか所番地のようなものです。メモリーのアドレスを意識させない,もしくは意識できないプログラム言語もありますが,C言語はアドレス演算子(&)を使えばアドレスを取得できるので,実際に変数がメモリーをどう確保するかを確認してみましょう。例えば図5のように,int型の変数a,bが連続してメモリー上に確保されたときのイメージを思い浮かべてください。このときのイメージをコードにしたのがリスト5です。

図5●int型の変数a,bが連続してメモリー上に確保されたときのイメージ

リスト5●変数のアドレスを確認するプログラム

グローバル変数a,bを確保し(1),main関数の中でaに16を代入して(2),bには「a×4」の結果である64を代入しています(3)。次にprintf関数でa,bの値と,変数a,bのアドレスを出力し*3,outx関数では,ローカル変数aに代入した16を左シフト演算子で,2ビットぶんシフトすることで4倍しています(4)(カコミ記事「シフト演算って何?」参照)。

 リスト5を実行すると図6のようになります。変数のアドレスに注目してください。int型の変数は4バイトぶんのメモリー領域を使って記憶されるので,グローバル変数のaとbは並んでメモリーに場所を確保しています(図5のイメージのままですね)。ローカル変数a,bも仲良く隣り合っています。でも,グローバル変数とローカル変数のアドレスは,遠くに離れていますね。これはどういうわけでしょう。

図6●リスト5の実行結果

実はメモリーの内部は,弁当箱のように区切られていると考えてください。ごはんの場所,おかずの場所のように,目的に応じて図7のように仕切られているのです。

図7●メモリーがエリア分けされているイメージ

 関数の引数や関数内部で宣言したローカル変数はスタックに配置されます。スタック領域は,関数が呼び出された時点で確保され,関数の処理が終わると自動的に解放されます。だから,ローカル変数の寿命は関数ブロック内に限定されるのです。

 一方,グローバル変数は,データ・セグメント(静的記憶領域)に配置されます。この領域に確保された変数の寿命はプログラム終了までです。ローカル変数とグローバル変数は管理の仕方が違うエリアに確保されるのです。

シフト演算って何?  シフト演算はビットをずらします。例えば,16を8ビット(1バイト)の2進数で表すと「00010000」となりますね。これを2ビット左にシフトすると「01000000」となり,10進数では64になります。逆に,右シフト演算子(>>)で2ビット右にシフトすると「00000100」となり,10進数では4になります。2進数ですから,1ビットずらすと2倍もしくは1/2倍になるわけです。つまり,10進数だと10倍あるいは1/10倍ですね。

 このほかにもビット演算子には,ビット単位の論理積をとるAND演算子や,論理和をとるOR演算子などが用意されています。16進数0x01と0x02の論理積は「0x01 & 0x02」の式で求まり,結果は0です。2進数で考えると000000001と00000010のビット単位の積ですから,00000000となるのがわかります。

 一方,論理和は「0x01|0x02」で求めることができ,値は00000011です。ビット単位の操作を行えば,char型やint型をTrue/False(1/0)の論理型の集合のように扱うこともできます。

スタックはPEZのディスペンサー

 せっかくですから,もう少しスタックについて,突っ込んでみましょう。スタックでは後入れ先出し (LIFO :Last in First Out)方式でメモリーの管理が行われます。キャンディーを上からぎゅうぎゅう押し込み,一番上のものから1個ずつ取り出し食べていくお菓子「ペッツ(PEZ)」のディスペンサーのようなものをイメージしてください。「ペッツなんて知らない」と言われたら,他に何にたとえればいいか困ってしまうのですが,上にしか口がない筒状の容器に,何かを押し込んでいくと,最初に取り出すことができるのは最後に入れたものになりますね。そういう容器を想像してください。

リスト6●スタックの様子を調べるプログラム

リスト6では,main関数でローカル変数a,bを宣言しています。main関数はoutij関数を呼び出し,outij関数はローカル変数i,jを宣言します。outij関数はoutnmとoutxyという二つの関数を呼び出します。実行結果は図8です。アドレスの下2桁だけを使って図解しましょう(図9)。a,b → i,j → n,mの順で各関数の開始時にローカル変数がスタックに押し込まれていきます。outnm関数が終了した時点で,n,mが取り出され,outxy関数で再びその領域が利用されます。

図8●リスト6を実行したところ

図9●リスト6におけるスタックの様子

終了した関数の使っていたメモリー領域から,解放されるので,関数が関数を呼び出し,その関数がまた関数を呼び出すようなプログラムの構造では,このスタックの後入れ先出し方式は理にかなっています。呼び出された関数の処理が終われば呼び出し元の関数に制御が戻ってくるからです。

 しかし,一般にスタックはデータ・セグメントやヒープに比べると小さい領域です。関数が何重にも関数を呼び出していくプログラムの中で,大きな変数をどんどん確保していくと,スタック・オーバーフローというエラーになることがあるので注意しなくてはいけません。大きなメモリー領域を確保したいときは,標準ライブラリ関数mallocを使って動的にメモリーを確保します。malloc関数を使った場合はヒープにメモリー領域が確保されます。ちなみに,コード・セグメントという場所はプログラム領域と呼ばれ,プログラムのコード,もろんマシン語に変換されたコードがロードされる領域です。

記憶クラス指定子staticを使ってみよう

リスト7●staticを使ったプログラム

最後に,staticという記憶クラス指定子の効果を見てみましょう。リスト7ではグローバル変数aとローカル変数b,そしてstaticというキーワードを付けたローカル変数cを宣言しています。そして,outabc関数内のprintfで,++a,++b,++cと各変数の値をインクリメントしています(図10*4。aはグローバル変数ですから,プログラム全体で値をキープできます。一方,bはローカル変数ですから,outabc関数を抜けると消滅してしまうので,インクリメントした結果は,いつも1です。しかし,staticをつけた変数cはグローバル変数と同じようにカウントアップされています。static記憶クラス指定子を付けると,静的寿命を持つローカル変数になるのです*5

図10●リスト7を実行したところ

でも,この説明では納得がいきませんよね。ローカル変数はスタックに確保されるから,「関数を抜ければ解放されるんでは?」と疑問を感じます。ふに落ちない場合は,アドレスを表示させて確認できるところがC言語のいいところです。

リスト8●リスト7のoutabc( )関数のprintfの後に追加するコード

リスト7のプログラムに,リスト8のような変数cの値が5のときにだけ各変数のアドレスを表示するコードを追加して,実行してみてください(図11)。ローカル変数bの横にいたはずのローカル変数cが,グローバル変数aの隣にいますね。staticという記憶クラス指定子を付けることにより,記憶場所がスタックからデータ・セグメントに変わったのです。

図11●リスト7にリスト8のコードを追加して実行したところ

 このようにアドレス演算子を使うと,簡単に変数のアドレスを取得できるので「なぜ,そうなるの?」を自分で解決できます。

☆     ☆     ☆

 さて,次回はmalloc関数とは違う方法で,メモリーの領域をまとめて確保する方法や配列について説明したいと思います。また,お付き合いください。

第5回 配列を理解してアルゴリズムを考える

本連載も今回で5回目,いよいよ中盤に差し掛かってきました。第1回から4回までは,C言語のプログラムとアセンブラのプログラムとの対比,浮動小数点数が計算を間違う理由,構造化プログラミングの必要性,変数のスコープとメモリー・セグメントの関係,スタックの動作など基本的な知識を中心に解説してきました。「こうなるのは,こういう理由か」と,目に見えぬ仕組みを理解していただけたでしょうか。

 今回からは,基本的な仕組みに加えて,プログラミングの面白さもお伝えしたいと思います。プログラミングの面白さは“考えること”にあります。どう処理すればよいかをイメージできない複雑なことを,創意工夫でスパッとコードで実現できたときの爽快感は,他の仕事では得がたいものかもしれません。例えば,大量のデータを少量のコードで扱い,処理の効率をアップさせるために知っておかなくてはならない基本的なデータ構造に「配列」があります。今回は,この「配列」にフォーカスし,配列とは何かから始め,配列を使うと何が便利なのかを知っていただきたいと思います。

同じデータが繰り返し発生するときに配列を使う

 例えば,陸上競技100メートル走の結果を記録し,誰が(何コースが)1位だったかを表示するプログラムを考えてみましょう。

 100メートル走のタイムは10.12などと小数点以下の値を持つので,float型で記録することにします。コースが8コースあるとすると,タイムを記録する変数も八つ必要になります。つまり,

float rtime1,rtime2,rtime3,rtime4,rtime5,rtime6,rtime7,rtime8;

とコースの数だけ別々の変数を定義し,

printf("1コースのタイムを入力してください:");
scanf("%f",&rtime1);

printf("8コースのタイムを入力してください:");
scanf("%f",&rtime8);

のようにscanf関数で各変数に,タイムを入力していくプログラムが思いつくでしょう*1。でもこれだと,変数の個数ぶん同じコードを繰り返し記述しなければならないので,冗長な感じがします。最高タイムを求めるコードも面倒なものになりそうな気がします。このように,同じ性質のデータが繰り返し発生するときに「配列」を使います。

リスト1が配列を使ったプログラムです。(1)の「float rtime[COURSE]」が配列の宣言です。COURSEは#defineプリプロセッサ命令でマクロとして宣言されています。#defineは

#define マクロ名 置換する文字列

の形式でマクロを定義します。ソース・プログラム中のマクロ名はプログラムがコンパイルされる前に,プリプロセッサにより,「置換する文字列」に変換されます。ですから,これでfloat型のrtime[0]~rtime[7]までの八つの要素を持つ配列がメモリー上に確保されるのです。各コースに8人の選手がキチンと並んだように,float型の値を入れるメモリー領域が8個ぶん連続して確保されるわけです(図1)。

リスト1●8コースぶんのタイムを記録し,一着のコースとタイムを表示するプログラム

図1●1次元配列のイメージ。float rtime[8]と書くと,float型の変数を入れる箱が八つ用意される

配列を扱う場合,マクロ定義はとても有効です。リスト1の最初のfor文のループでは,配列の添え字「i」の値を一つずつ変化させながら,scanf関数で配列の各要素に値を代入しています(2)。このforループで繰り返す回数をコントロールするためにも,「i < COURSE」とマクロCOURSEを使っていますね。このように書いておくと,コース数が変わったときに,#defineの「置換する文字列」だけを変更すればよいので便利です。

 配列について一つ注意してほしいのが,C言語では配列の添え字(インデックス)は常に0から始まるという点です。他の言語では1から始まるように指定できる場合もありますが,C言語では常に0からです。ですから,printf関数の「%dコース…」の変換仕様%dにインデックス「i+1」を指定し,コース番号としています。rtime[0]が1コースのタイムを入れる配列の要素になるのです。ですから,for文の繰り返し判定もi < COURSEと8より小さかったらとシンプルに記述することができます。

 最高タイムを求める処理では,まずrtime[0]を変数minに代入し,それより小さい値があったらminの値を入れ替え,インデックスの値をjに記憶しています(3)。最高タイムを表示するprintf文中の変換仕様「%.2f」は浮動小数点数を小数点以下2桁まで表示することを意味します。このプログラムを実行すると図2のようになります。最高タイムのコースとタイムを表示していますね。

図2●リスト1を実行したところ

多次元配列を使うと,より多くのデータを扱える

 陸上競技では,複数組の予選が行われ,勝者が決勝に残り,メダルを争います。リスト1のサンプル・プログラムは,決勝で誰が一番かを決めるようなイメージでしたが,今度は,予選の処理も考えてみましょう。

 100メートル走に48名のエントリがあったので,6組に分け予選を行い,各組の最高のタイムを表示させたいとします。「それなら,1次元配列を使ったリスト1のプログラムを6回実行すれば?」――確かにそうですが,ちょっと面倒です。こんなときに便利なのが「多次元配列」です。多次元配列を使えば,6組×8コースのタイムを一つの配列として扱うことができます。

 例えば,図3のような配列を宣言するには,「float rtime[6][8]」とインデックスを二つ書きます。こうすれば,8コースぶんの1次元配列が六つ格納できる2次元配列が確保できます。

図3●多次元配列(2次元)のイメージ。float rtime[6][8]と書くと,float型の変数を入れる箱が6×8=48個用意される

実際に2次元配列を実装したコードを見てみましょう(リスト2)。for文によるループを入れ子(ネスト)にして,各要素を扱っています。インデックス「i」が組に,「j」がコースに対応しています。この例では単純に各組のベストタイムを表示しているだけですが,2次元配列としてデータを表現しておけば,組(次元)ごとの処理だけではなく,配列全体を一つのデータの固まりとして扱う――例えば全体から上位3名のタイムを求めること――も可能です。

#include <stdio.h>
#define KUMI 6
#define COURSE 8
int main()
{
  float rtime[KUMI][COURSE];
  int i,j,k;
  float min;

  for (i = 0; i < KUMI; i++) {
    for (j = 0; j < COURSE; j++) {
      printf("%d組%dコースのタイムを入力してください:",
      i + 1,j + 1);
      scanf("%f",&rtime[i][j]);
    }
  }
  for (i = 0; i < KUMI; i++) {
    min = rtime[i][0];
    k = 0;
    for (j = 1; j < COURSE; j++) {
      if (rtime[i][j] < min) {
        min = rtime[i][j];
        k = j;
      }
    }
    printf("%d組の最高タイムは%dコースの%.2fです¥n",
    i+1,k+1,min);
  }
  return(0);
}

リスト2●2次元配列を使って実装したコード

 C言語では,2次元配列だけでなく,3次元,4次元と多次元の配列を扱うことができます。慣れないと扱いにくいかもしれませんが,要はインデックスが増え,for文のネストが一つ深くなるだけです。例えば,100メートル走のタイムを格納する「組×コース」の2次元配列を,100メートル走と200メートル走のタイムを格納できるように拡張しようと思ったら,

#define SYUMOKU 2

と種目の数をマクロ定義して,

float rtime[SYUMOKU][KUMI][COURSE];

で3次元配列を定義し,リスト3のようにforループの外側に,もう一つforループを追加してやればよいわけです。

for (i = 0; i < SYUMOKU; i++) {
  for (j = 0; j < KUMI; j++) {
    for (k = 0; k < COURSE; k++) {
      printf("第%d種目%d組%dコースのタイムを入力!:",
      i + 1,j + 1,k + 1);
      scanf("%f",&rtime[i][j][k]);
    }
  }
}

リスト3●2次元配列を,100メートル走と200メートル走のタイムを格納できるように拡張したコード(一部)

素数を求めるプログラムを配列を使って作る

 さて,ここまでで,変数をいくつもバラバラに宣言してプログラムを作成するより,配列を使った方が少しのコードでより大量のデータを扱えるということがわかっていただけたと思います。次に検証したいのは,配列というデータ構造を使うことで,処理そのものを効率化できるかどうかという点です。

 何か解決すべき問題があって,それを解く方法のことをアルゴリズムと呼ぶのは,皆さんならご存知ですね*2。当たり前の話ですが,コンピュータのプログラムに限らず,何かを解決する方法は複数存在します。しかし,すべての方法がベストな選択とは限りません。複数ある解法のうち,“手順の少ない方法”が効率の良いアルゴリズムと言えます。

 配列のデータを並べ替え(ソート)したり,ある値を探したり(サーチ)する処理では,いろいろなアルゴリズムが考えられてきました。クイック・ソートやバイナリ・サーチなどが有名ですね。

 今回のテーマは「配列を使うと処理の効率化が図れるか」ですから,配列を使わない場合と使う場合とでプログラムを比較してみます。サンプルとして,素数を求めるプログラムを考えてみましょう。素数を求める処理にかかる回数を比較してみたいと思います。

 ところで皆さんは,素数とは何かをきちっと言えますか? そう,1とその数自身以外では割り切れない整数です。間違えやすいのですが,1は特別な数で,素数ではありません。2以上の数は,素数と合成数*3で構成されています。例えば,20までの素数は図4のようになります。

図4●20までの素数と合成数

 リスト4のコードが,配列を使わずに素数を求めるサンプル・プログラムです。2からMAX(1000)までの範囲にある素数を求めています。ある数が素数であるか否かを調べるには,自身と同じ数でしか割り切れないことを調べればよいわけですから,変数「i」の値を変数「j」で割った余りが0のとき*4,iとjがイコールであれば,素数であると判断*5できます。プログラムでは,2から1000まで増加していくiの値をjで割っています。jは2から始まり,iまで増加していきます。「i==j」が成り立つ前に割り切れてしまった場合は,合成数だと判断できます。

#include <stdio.h>
#define MAX 1000

int main()
{
  int i, j;
  int counter1 = 0,counter2 = 0,counter3=0;
  for(i=2;i<=MAX;i++) {
    for(j=2;j<i;j++) {
      counter1++;
      if(i % j == 0) {
        break;
      }
    }
    if (i==j) {
      printf("%d¥n",i);
      counter3++;
    } else {
      counter2++;
    }
  }
  printf("計算した回数%d¥n",counter1);
  printf("合成数の数%d¥n",counter2);
  printf("素数の数%d¥n",counter3);

  return 0;
}

リスト4●配列を使わずに,素数を求めるプログラム

 counter1が剰余を求める計算をした回数を記憶する変数,counter2が合成数の数,counter3が素数の数です。実行結果は図5のようになります。素数の数が168で,それ以外の831個が合成数です。

図5●リスト4を実行したところ

 注目してほしいのは,計算した回数7万8022という数字です。iとjの剰余が0と等しいかどうかという評価処理を,7万8022回行っているわけです。かなりの数ですね。はたして,2から1000までの素数を求めるのに7万8022回も処理が必要になるのは仕方のないことなのでしょうか? もちろん,7万8022回といっても,コンピュータは一瞬で計算してしまいますから,特に不満は感じないかもしれません。しかし,大規模なプログラムになればなるほど,プログラムのあちこちで繰り返し計算を行うことになります。一つの計算に要する時間はわずかでも,それがいくつも積み重なると,プログラム全体の処理時間は長くなります。プログラマなら,一つひとつの処理に無駄がないかをキチンとチェックすべきなのは言うまでもありません。

エラトステネスのふるいを使って素数を求める

 では,もっと少ない手順で素数を求める方法はないのでしょうか? そこで,アルゴリズムの登場です。

 古くから知られている素数を調べるアルゴリズムに「エラトステネスのふるい」があります。合成数をふるい落としていく方法です。まず,1から10までの素数をエラトステネスのふるいで求める方法を考えてみましょう。まず,1は特別な数ですから,ふるい落とします。次に,2は素数ですからそのままにして,2の倍数をふるい落としていきます。

4,6,8,10

がふるい落とされました。次に,3の倍数,4の倍数と落としていきます。

6,9

8

 5の倍数「10」を落とした時点で素数が確定します。残ったのは,2,3,5,7です。このふるいを,配列を使って表現してみましょう。1000の要素を持つ配列を用意して,ふるいとして使います(リスト5)。

リスト5●アルゴリズム「エラトステネスのふるい」を使って素数を求めるプログラム

 (1)でfurui配列を初期化しています。(2)は「1は特別な数で素数ではない」を意味するコードです。C言語の配列はインデックス0から始まるのでfurui[0]は数字の1に対応しています。1000はfurui[999]です。furui[i]に1を代入することが,「ふるい落とすこと」を意味します。(3)のループの中で, furui[i * j -1]に1を代入していきます。iは2から始まり,MAX÷2までインクリメント(増加)します。jも2から始まり,「i*j」がMAXになるまで,インクリメントします。つまり,

2*2-1,2*3-1,2*4-1,2*5-1,2*6-1,…
3*2-1,3*3-1,3*4-1,3*5-1,3*6-1,…
4*2-1,4*3-1,4*4-1,4*5-1,4*6-1,…
…

と倍数がふるい落とされていくわけです。(4)のif文では,すでに1が代入されているかをチェックし,1が入っていないときに1を代入しています。counter2がふるい落とした数になります。素数かどうかを評価した回数はif文で「furui[i*j-1]==0」とたずねた回数とイコールですから,if文を抜けたあとでcounter1をインクリメントしています。(5)でふるい落とされずに残った数を配列のインデックス「i + 1」で表示しています。

 リスト5の実行結果は図6の通りです。素数の数は168で,リスト3の結果と同じですね。違いは「ふるいにかけた回数」と表示している評価を行った回数です。なんと5070回。7万8022回のリスト3に比べ激減しています。この結果を見ると,配列を使うことで処理の効率化は可能だと言えます。

図6●リスト5を実行したところ

 とはいえ,効率化できたからといって満足するだけではいけません。一般的に処理速度を上げようとすると,メモリーの使用量が増えるというトレードオフが発生します。今回は,リスト4の処理を,リスト5のように配列を使った処理に変更することで,int型の要素1000の配列ぶんのメモリー(4×1000バイト)が必要になりました。ここで,「この程度の処理速度なら,適切なメモリー使用量かな?」と,処理速度とメモリー使用量のバランスを考えるバランス感覚を持つことがプログラマには必要だと筆者は思います。

アルゴリズムと可読性のバランスを考えるようにしよう

 「それでは,お後がよろしいようで」と終わりにしたいところですが,もう一度リスト5を見直してみましょう。

 このプログラムでは,furui[i*j-1]が0のときだけ,つまり,すでに1(素数でない)というマークが付いてないときだけ1を代入していますが,そこに1が入っているということがあらかじめわかっている数を飛ばすことができれば,もっと無駄な処理を省けます。

 (3)のループで,最初に2以外の2の倍数にマークを付けるのですから,2以外の偶数はすべて素数ではないと1が代入されているはずです。ならば,4以上の2で割り切れる数は,スキップしてもいいんじゃないかというコードがリスト6の(1)です。iが3より大きくて,2で割った余りが0のときは,continue文でループの先頭に飛ばし,以降の処理が実行されないようにしています。このように変更すると,ふるいにかけた回数が2879回に減りました(図7)。

リスト6●リスト5の(3)のループを効率化したコード(一部)

図7●リスト5をリスト6のコードで修正して実行したところ

 この変更による効率化は,使用するメモリー資源を増やすものではありません。単に無駄な処理はしないという工夫です。何のトレードオフも発生していないように見えますね。

 しかし,実はそれでもトレードオフは発生しているのです。それは,自分以外の人間にプログラムを見せたときのわかりやすさです。リスト5のコードは,エラストテネスのふるいを知っていれば,何をしているのか一目瞭然です。しかしリスト6の修正の意味は,パッと見ただけでは,よくわかりません。

/* 2の倍数には最初に1を代入済み */

などとコメントを入れておけば,処理内容を説明する必要はないでしょう。しかし,コードを追加していけばいくほど,プログラムを読むことが難しくなってくることは避けられません。このことを「プログラムの可読性が下がる」と言います。

 プログラマは,処理効率化のためのプログラムの改変が,わかりやすさとのトレードオフになるということを意識しなければいけません。「ここを直したら,もう少し速くなるけど,わかりにくくなりそうだから,やめておくか」というバランス感覚もプログラマには必要だと筆者は考えます。

 次回は文字列を扱う方法を,ポインタの解説を交えてお送りしたいと思います。

第6回 ポインタを理解して文字列を扱う

 本連載も今回で折り返し地点の第6回となりました。前回は,エラトステネスのふるいを使って素数を求めるプログラムを作成し,ロジックを考えてプログラムを作ることの面白さをお伝えしました。今回は文字列の扱い方とポインタの基礎を解説して,暗号化プログラムを作成してみます。

 インターネットの普及とともに暗号化技術の研究も盛んになっていますね。とはいってもここでは最近用いられるような難解な暗号ではなく,古代ローマのジュリアス・シーザーが使ったといわれるシーザー暗号をプログラミングしてみましょう。*1

なぜ文字列とポインタの知識が必要なのか

 暗号化の話に入る前に,いま一度,C言語とはどんなコンピュータ言語なのかを振り返っておきたいと思います。1970年代に米AT&T社のベル研究所でD. M. Ritchie氏とB. W. Kernighan氏によって開発されたC言語は,C#やJavaを始めとする多くの言語に影響を与えただけでなく,オブジェクト指向化されたC++言語とともに,現在でも広く使われています。しかし普及したからといって,C言語を甘く見てはいけません。Cをプログラムするとき,プログラマは常にCの危険性を肝に銘じておく必要があります。

 一つ目は「ポインタ演算などの機能を持ち,アセンブラのようなハードウエアに密着した操作ができる」ことです。これはOSを書くためには必須のもので,もともとC言語はUNIXを書くために作られたのですから,当然といえば当然ですね。

 もう一つは「文字列はNULL文字でターミネートする配列であり,配列操作の実体はポインタ演算である。標準ライブラリに用意されている文字列を操作する関数はバッファ・オーバーフロー*2を考慮していない」という点です。

 文字列操作やポインタは,便利な半面,操作を誤ると思わぬバグを引き起こします。実際,ポインタ演算の機能は,バグの原因やコンピュータを暴走させる危険性があるという理由で,Javaなどでは省かれています。

 C言語にはそういった危険な要素があるんだぞということを理解しつつ,バグを作らないためにも,文字列とポインタの基礎をしっかり身に付ける必要があるのです。

文字列は文字 (char型)の配列で扱う

 Javaなどの言語では,複数の文字を並べた“文字列”を扱うためにString型という型が用意されています。しかし,C言語には文字列を扱う型がありません。そのためC言語では,文字(char型)の“配列”として文字列を扱います。

 例えば,「Jonny」という文字列を扱うときは,リスト1のように配列に代入する必要があります。(1)で1バイト文字が0から9の10文字ぶん格納できる配列を宣言し,(2)以降で一文字ずつ代入しています。(3)で代入している「\0」はNULL*3文字を表すエスケープ文字です。C言語の場合,文字列はNULLでターミネート(終了)します。難しく聞こえるかもしれませんが,NULLを文字列の終わりとして判断する記号に使っているわけです。土の中に,Jonnyという名前を一文字ずつ記入したカプセルを埋めたとイメージしてください(図1)。掘り出していくと,J,o,n…と順番に文字が出てきます。yまで掘っても,これで終わりだよというマークがなければ,「まだ,何かあるんじゃないか」と掘るのを止めることができません。この「これで終わりだよマーク」がC言語ではNULLなのです。ですから,文字列を格納する場合は文字数プラス(NULLのための)1バイトが必要なのです(図2)。

リスト1●Jonnyという文字列を配列に格納するプログラム

図1●埋まっている文字列の最後にあるのが「¥0」

図2●文字列を格納する場合は文字数プラス1バイトが必要

 文字(char)型で,「a」を記憶するには1バイトしか必要ありません。しかし,文字列として(char型の配列として),「a」1文字を記憶するには,2バイト必要になるわけですね*4

 しかし,文字列を扱うのにいちいちリスト1のように書いていたら大変です。文字列の配列の初期化は,リスト2の(1)のようにまとめて書くこともできますし,(2)のようにダブルクォートでくくって記述することもできます。この場合は末尾に自動的にNULLが付加されます。どう考えても(2)の方が簡単ですよね…。ただし,このとき文字列はダブルクォートでくくり,文字はシングルクォートでくくる点に注意してください。なお,コードの途中で

char str2[10]="Lucy";

str2[10]="Linda";

のようにして文字列を代入することはできません。コンパイル・エラーになります。文字列配列に他の配列を代入することはできないからです。

リスト2●文字列の配列の初期化は,ダブルクォートでくくって記述できる

配列名は配列の先頭アドレスを表す

 リスト3は,キーボードからの入力を文字列配列に格納して表示するプログラムです(図3)。ここで思い出してほしいことは,scanf関数は2番目の引数に変数の“アドレス”を要求する関数だということです。


#include <stdio.h>

int main()
{
  char str[256];
  printf("名前を入力してください:");
  scanf("%s",str);
  printf("こんにちは,%s\n",str);
  return(0);
}

リスト3●入力された文字列を表示するプログラム

図3●リスト3を実行したところ

 つまり,図3のように「Jinny」と入力してJinnyと無事表示されているということは,“配列名strが配列の先頭アドレスを表している”ということにほかなりません。ぜひ,ここで「配列名は配列の先頭アドレスを表す」と覚えておいてください。

 ところで先ほど,初期化以外に,ダブルクォートでくくった文字列を配列に代入することができないと説明しましたが,ある文字列配列の内容を変更したい場合に1文字ずつ代入するのは手間がかかります。そこで,C言語には標準ライブラリ関数として文字列を操作する関数がいくつか用意されています。

 リスト4を見てください。(2)のstrcpyが,文字列配列に他の配列をコピーする関数です。lastname配列をfullname配列にコピーしています。(3)のstrcatは文字列の連結を行う関数です。文字列を扱う標準ライブラリ関数を使うには,(1)のようにヘッダー・ファイルstring.hをインクルードする必要があります。

リスト4●文字列処理に標準ライブラリ関数を使ったプログラム

 実はこのプログラム,strcatを使って第1引数fullnameにfirstnameの内容を連結するときにバッファ・オーバーフローが発生します。fullnameのサイズがSuzukiとIchiroを連結して格納するには小さすぎるからです(図4)。もし,あふれた部分を他の変数や,他のプログラムが使っていたら,予期せぬ現象が起きる可能性があります。

図4●リスト4のプログラムではバッファ・オーバーフローが発生する

 コンパイラはあふれをチェックしてくれません。したがってCのプログラムを作るときは,プログラマは十分な大きさの配列を確保するように気を付けなくてはいけないのです*5。この場合,文字列配列のNULLを除く文字数を返すstrlen関数を使って,文字数をあらかじめ調べてから連結を行う方法も有効でしょう*6

文字をずらしてシーザー暗号をプログラムしてみよう

 さて,文字列の扱い方の解説が済んだところで,いよいよ冒頭で述べた「シーザー暗号プログラム」を考えてみましょう。

 ローマのスエトニウスが著した「カエサル伝」には「カエサル(シーザー)の書いた暗号文を読み取るには,各文字をアルファベットの順で三つ前の文字に置き換えなければならなかった」という記述が残っているそうです*7。ここでは,必ず3文字ずらすだけではなく,何文字ずらすかを指定できるようにしてみましょう。また,複雑になりすぎないように暗号化する文字は英大文字だけを対象にします。

 リスト5が,シーザー暗号プログラム,いわゆる換字式暗号化プログラムのサンプルです。冒頭で「#define LEN 255」となっていますが,#defineで配列の要素数をマクロ定義する方法はC言語の定石ですね。str配列は,暗号化前の元の文を入れる配列です。暗号化した文字列はcipher配列に格納します。ikeyは何文字ずらすかという“鍵”です。while文によるループの中で文字のずらし(シフト)を行っています。繰り返し条件が「str[i]!=’\0’」ですから,元の文のNULLに出会うまで繰り返し暗号化を行います。ループを抜けたら,「cipher[i]=’\0’」で暗号文にNULLを付加していますが,これを忘れると,どこまで文字が入っているのかわからなくなるので注意してください。

リスト5●換字式暗号化プログラムのサンプル

 このプログラムの中でわかりにくいのは(1)の部分でしょう。「str[i]-‘A’」は英大文字を表1のように,0から始まる連番に置き換えています。そして鍵を足して「ずらし」,26で割ったあまりを求めています。Zを1文字ずらすとAになるようにしているわけです。(2)でnにAの文字コードを足して,暗号化文字を求めています。実行結果を見ると,「ZOO」を2文字ずらしたので「BQQ」になっていますね(図5)。

表1●リスト5の暗号化処理で,英大文字は0から始まる連番に置き換わる

図5●リスト5を実行したところ

ポインタを使って文字列処理を実装する

 アップテンポで進めていきましょう。暗号プログラムをバージョンアップするために,ポインタについてちょっと詳しく解説します。

 プログラミングの勉強をしている多くの人がぶち当たる壁が二つあります。それは,BASICや他のプログラム言語の経験者がC言語のポインタに初めて出会ったときと,非オブジェクト指向言語の経験者がオブジェクト指向言語でプログラムを作らなければいけなくなったときです。筆者もオブジェクト指向言語の様々な機能を見ていると,なぜこんな機能が必要なのかと感じることがありますが,言語の開発者はあった方が便利だと考えたに違いありません。ポインタもしかりです。ポインタはC言語最大の難関などと言われていますが,「ポインタが使えると便利になる」程度に考えておけば,それほど難しい話ではありません。

 ポインタとは,変数の「値」を直接操作するのではなく,変数の「アドレス」を介して間接的に操作できるようにするための仕組みです。リスト6とその実行結果(図6)を見てください。リスト6では変数を二つ宣言していますね。「a」はint型の普通の変数です。「a=31」と整数値を代入していますので,「aの値は31」と表示します。アドレス演算子&を使って変数aのアドレスを取得すると0012FF88と表示されます。皆さんおわかりのように,0012FF88は16進数です。16進数は1桁で0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,Fまでの16通りの値を表すことができます。16進数1桁で2進数の4桁,0000から1111までを表現することができるので,アドレスは「4ビット× 8桁=32ビット」で表現されていることがわかります*8


#include <stdio.h>

int main()
{
int a;
int *p;

a = 31;
p = a;
printf("aの値は%d \n",a);
printf("aのアドレスは%p \n",a);
printf("ポインタpの値は%p \n",p);
printf("ポインタpのアドレスは%p \n",p);
printf("ポインタpの指す値は%d \n",*p);
return 0;
}

リスト6●ポインタを使ったプログラムのサンプル

図6●リスト6を実行したところ

 「int*p」がポインタの宣言です。この場合,ポインタ変数pのことを「int型へのポインタp」と呼びます。ポインタは他の変数のアドレスを記憶する変数なので,「p=&a」で変数aのアドレスを代入しています。ポインタを使う前には,必ず他の変数と関連付けなければならないことを覚えておいてください。他の変数のアドレスを代入するまでは,ポインタにはどんな値が入っているかわからないからです。

 図6を見ると,ポインタpの「値」は0012FF88となっています。またポインタの「アドレス」は0012FF84となっています。ポインタといっても変数の一つですから,他の変数と同様にメモリー上に存在します。そして,間接参照演算子(*)を付けることで,ポインタが示すアドレスの内容を間接的に取得できるのです(図7)。

図7●リスト6におけるポインタの値とアドレスのイメージ

 メモリーには1バイト単位でアドレスが振られており,そのアドレスをポインタ変数に記憶させることで,間接的に変数の値にアクセスできるわけです。変数aとポインタ変数pが4バイト離れているのは,本連載で使用しているBorland C++ Compiler 5.5が,int型を4バイトの領域を使って記憶するからです。またポインタ変数も,32ビットのアドレスを記憶するために,4バイトのメモリー領域を使います。しかし,アドレスを記憶するために,「int *p」とint型で4バイトのメモリー領域を確保するわけではないのです。では,ポインタを宣言するときの「型」とは何を意味しているのでしょうか?文字列配列をポインタで操作するサンプルを見ていきましょう。

ポインタに型が必要な理由

 リスト7は,英単語の入力を受け付け,一文字ずつ添え字を増加させて(インクリメント)出力する方法(1)と,ポインタをインクリメントして出力する方法(2)を比較したプログラムです。当然,同じ単語が2度出力されます。ポインタstr_ptrはchar型の配列を指すポインタなので,char型で宣言しています。

リスト7●文字列配列をポインタで操作するプログラム

 もう一つ,int型の配列をポインタで操作するプログラムを見てみましょう(リスト8)。int型のten配列を指すポインタ変数ten_ptrを「intten_ptr」と宣言しています。int型の配列はNINZU個の要素を持つ配列なので,forループで数字を一つずつ出力しています。ここでは,「(ten_ptr+i)」と,直接ポインタの値をインクリメントするのではなくiの値を0~4へと変化させることで,アドレスを変化させ,順に値を取り出しています。


#include <stdio.h>
#define NINZU 5

int main()
{
int ten[NINZU] = {92,70,94,85,100};
int *ten_ptr;
int i;

ten_ptr = ten;
for (i = 0; i < NINZU; i++) {
printf("%d \n",*(ten_ptr+i));
}
return 0;
}

リスト8●int型の配列をポインタで操作するプログラム

 リスト7とリスト8を比べると,ポインタの値を直接変化させるか間接的に変化させるかという違いはありますが,ポインタを一つずつインクリメントさせていることに違いはありません。片方はchar型(1バイトで記憶)の配列で,もう一方はint型(4バイト)の配列なのに,1を足すとどちらも次の要素を指すのは,ポインタに「型」を宣言しているからです。char型ならchar型のサイズぶん,int型ならin型のサイズぶん,アドレスを変化させてくれるのです。

 このようなことから,“配列操作はポインタ演算で実現されている”ということが実感できるのではないでしょうか。

シーザー暗号プログラムを改良する

 最後にリスト5を発展させて,もう少し解きにくい暗号プログラムを作ってみましょう。リスト5の暗号プログラムでは,英文字の出現頻度などから,暗号鍵を推測することは名探偵コナン君*9でなくても容易ですからね。

 一つの鍵で文字をずらすだけでは規則性に気づかれやすいので,複数の鍵,具体的には三つの数字を使うようにしてみます(リスト9)。図8のように,暗号化する文字列を入力したあとに鍵を三つ入力してもらいます。

図8●リスト9を実行したところ

リスト9●リスト5の暗号プログラムを改良したもの

 リスト9のプログラムで注目してほしいのは,暗号化を行うcs_cipher関数に実引数として配列名を与えているところと(1),それらをポインタとして受け取り処理しているcs_cipher関数(2)です。ポインタを使うと,複数の配列を簡潔なコードで扱えることがおわかりいただけるでしょうか。これもひとえに,「++str」などとポインタ演算ができるからです。(3)は1文字目には1番目の鍵を,2文字目には2番目の鍵,3文字目には3番目の鍵,4文字目にはまた1番目の鍵を…,とikey配列の要素を選択するための処理です。よくわからない方は,文字列とポインタの関係をもう一度復習してプログラムをながめてください*10

第7回 再帰処理と参照渡し(モドキ)のメリット・デメリット

☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆
☆     コメント      ☆
☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆

*1 母国語は機械語(マシン語) 英語と日本語の差ほどではないのですが,コンピュータやOSによって,マシン語にも違いがあります。例えば,Windows用のマシン語プログラムをUNIXマシンに持っていっても,そのまま実行させることはできません。

*2 マシン語プログラムを16進数で 2進数は,1桁で0と1の2値を表現できますが,マシン語などのダンプ表示に2進数を使うと,0と1がずらずらと並んでわかりにくくなります。そこで一般には2進数となじみのよい16進数で表記します。16進数1桁で2進数4桁を表現できます。

*3 自然言語 自然言語は,日本語や英語のように,人間が自然に使い出した言語のことです。

*4 バグ バグはコンピュータのプログラムに含まれる誤り,欠陥のことです。

*5 Java VM Java VMはJava Virtual Machine の略で,Javaの中間コードをマシン語に変換して実行するためのソフトウエアです。

*6 .NET Framework .NET Frameworkは,Microsoft .NET対応アプリケーションの動作環境です。.NET Framework用の中間コードにコンパイルされたコードを,.NETFrameworkがOSごとのマシン語に変換して動かします。JavaVMとの違いは特定の開発言語に依存しないことです。米Microsoftが提供する主要な開発言語であるVisual Basic/C++/C#などのほか,Borland Delphi 8でも.NETFramework対応のプログラムを作成できます。

*7 Windows 98におけるインストール Windows 98におけるインストールについては,日経ソフトウエアのWebサイトのWebスペシャルの情報などを参考にしてください。

*8 src srcはsource(ソース)の略で,一般にソース・プログラムを格納するフォルダ名として利用されます。