7回目の授業


5回目の課題について

 前回あたりから,プログラミング言語をマスターするための,一つ目の山場にさしかかってきていると思います。

演習課題については,毎回,必ず解答例を見て,
“なるほど。。。 こう書けばこのような計算が実行されるのね。。。”
と,必ず“納得する”ようにしてください。

演習課題は,それをただ提出するよりも,その内容を理解することが重要です!



第6章

関数とは



 これまで学んできたように,C言語のプログラムの基本は,自分のしたい仕事に合わせて,main() 関数のなかに命令文を組み立てていくということです。

以前に説明したように,この命令文には以下の四つの種類があります。 前回までの授業で,これらの命令文の基本については,皆さんはほぼ学び終えたことになります。

従って,前回までの知識だけでも,皆さんがこれから遭遇する殆どの計算作業を,C言語ですることができるはずです。

 さて,上の四つの命令文のうち,関数呼び出しのための文(3回目の授業のwebページ,関数呼び出しのための文を参照)において,これまで皆さんが使用してきた関数としては,printf(), scanf(), puts(), putchar() などがありましたね。

これらの関数は,まとめて,“ライブラリ関数”と呼びます。

ライブラリ関数は,簡単に言うと,C言語に始めから組み込み済みの関数のことをいいます。すなわち,ライブラリ関数は,コンパイラ開発者によって作成され,コンパイラと一緒にユーザーに提供されるということになります。


“マイ”関数の定義をしよう

 さて,自分のしたい仕事をプログラムにする際に,ライブラリ関数だけで事が済めばそれでもちろん良いわけですが,ちょっと変わった仕事をさせたい場面になると,ライブラリ関数だけでは不十分な面がでてきます。

その場合には,自分のオリジナルの,いわば“マイ”関数を新たに定義することができます。

C言語において,関数定義は,以下のような形式で書きます。

    関数の返却値の型 関数名(引数1の型宣言, 引数2の型宣言, ...)
    {
        この関数の中だけで使用する変数の型宣言 ;
        この関数で行う計算手続きの命令文 ;
            .
            .
            .
        return(この関数の返却値) ;
    }

 例えば,二つのdouble型の変数を引数として受け取って,その平均値を返却値として返す関数を,averageという名前で定義したいときは,以下のコードを書けばよいでしょう。

    /* double型の引数x, yの平均値をdouble型として返却する関数の定義 */
    double average(double x, double y)
    {
        double answer ;         /* この関数の中だけで使用する変数としてanswerを型宣言 */
    
        answer = (x + y) / 2. ; /* xとyの平均値を計算してanswerに代入 */
    
        return(answer) ;        /* 関数の返却値としてanswerを返却 */
    }

 関数定義において,関数の返却値の型を何にするのか,引数の型を何にするのか,引数を幾つ受け取るのかは,もちろん関数の設計者にまかされています(型を何にしても,引数を幾つ受け取ってもかまいません)。

 引数を全く受け取らない関数も定義することができます。その場合には,以下の形式で定義します。(Cf. テキスト154ページList6-9)

    /* 引数を全く受け取らない関数の定義の形式,voidは空(から)の意味 */
    関数の返却値の型 関数名(void)
    {
        この関数の中だけで使用する変数の型宣言 ;
        この関数で行う計算手続きの命令文;
            .
            .
            .
        return(この関数の返却値) ;
    }

ところで,上記のような形式の関数の定義を,今までどこかで見たことがありませんか?

今まで,C言語のプログラムの書き出しの決まり文句として理解していた,int main(void) から始まる以下の文です。

これは,実は,main() 関数の定義文だったということです。

    /* 引数を全く受け取らない関数,main()関数の定義 */
    int main(void)
    {
        main()関数の中だけで使用する変数の型宣言;
        
        計算手続きの命令文;
           .
           .
           .
        return(0) ; /* 関数の返却値として0を返却(この値はOSに返される) */
    }

 あるいは,返却値を持たない関数も定義することができます。その場合には,以下の形式で定義します。(Cf. テキスト152ページList6-7)

    /* 返却値を持たない関数の定義の形式 */
    void 関数名(引数1の型宣言, 引数2の型宣言, ...)
    {
        この関数の中だけで使用する変数の型宣言 ;
        この関数で行う計算手続きの命令文;
            .
            .
            .
        /* 返却値を持たないので,return() は書かなくてよい */
    }

引数の関数への受け渡され方

 自分で新たに定義した,“マイ”関数は,関数呼び出しのための文の中で,ライブラリ関数と同じような感覚で使用することができます。

例えば,上で定義した average() 関数を,main() 関数の中で,以下のような形式で呼び出すことができます。

    int main(void)
    {
        double a, b, c;
            .
            .
            .
        c = average(a, b); /* aとbの平均値を計算して,cに代入 */
            .
            .
            .
    }

 さて,プログラム中である関数を呼び出したときにどのようなことが起こるのかを,上のaverage() 関数を呼び出したときを例にとってまとめるとすれば,以下のようになります。
  1. 引数の型宣言により,その引数を使用するための準備がなされる。
    具体的には,double型の変数 x, y 用のメモリ領域が確保される。

  2. 呼び出し側の変数の値が,関数の引数へ渡される。
    具体的には,以下のような代入文が実行されて,関数の引数の値が設定される。
        x = a ; /* 呼び出し側(main() 関数のこと)の変数aが,average()関数の引数xに代入される */
        y = b ; /* 呼び出し側(main() 関数のこと)の変数bが,average()関数の引数yに代入される */
    
    このように,呼び出し側の変数が,呼び出された関数の引数に代入(コピー)されることによって,その関数に受け渡されることを,引数の“値渡し”と言います(Cf. テキスト145ページFig.6-4)。

    average() 関数の中では,代入されたほうの引数 x, y が使用されますから,average() 関数の中で x, y の値を変えても,呼び出し側の変数である a, b の値は変化しません。

    (注意) 引数が配列の場合には,“値渡し”ではなくて,“参照渡し”と呼ばれる,また別のメカニズムで引数へ値が受け渡されます(Cf. テキスト161ページFig.6-12)。参照渡しの場合には,関数の中で引数の値を変えると,呼び出し側の変数の値も変化してしまうという違いがあります。この辺りの理屈は,テキスト第10章に詳しく書いてありますので, 興味のある方は是非読んで確認されることをお勧めします。

  3. 関数の中だけで使用する変数の型宣言により,その変数を使用するための準備がなされる。
    具体的には,double型の変数 answer 用のメモリ領域が確保される。

  4. 関数の中で行う計算手続きが実行される。
    具体的には,以下のコードが実行される。
        answer = (x + y) / 2. ; 
    

  5. 関数の返却値が呼び出し側に返される。
    具体的には,以下のコードが実行される。
        c = answer ; /* average()関数の返却値answerが,呼び出し側(main() 関数のこと)の変数cに代入される */
    

  6. 関数の中だけで使用する変数が,消去される。
    具体的には,関数の引数である x, y 用に確保されていたメモリ領域と,関数の中で型宣言した answer 用に確保されていたメモリ領域が解放される。
    以降,x, y, answer という変数は使用できない。
関数の呼び出しは,プログラム中で何回でもできますから,
例えばもう一度,別の値の平均値を求めたくなったら,もう一度, average() 関数を呼び出せば,よいわけです。

そのときには,上の1.〜6.の手続きが,また繰り返される,ということになります。

同じ計算作業(上の例では平均値を計算する作業)を,パラメータを変えて(上の例では平均を取りたいxとyの値を変えて),何百万回もしなければならない(何百万組みにもなんなんとする異なるペアの平均値を求めなければならない)ときに,便利だということが,分かるでしょう!


識別子の有効範囲



 以前,プログラムの流れの分岐・繰り返しのための制御文(if...else...文や,for...文などのこと)を学んだときに,実行すべき命令文が複数あってそれを一かたまりとして扱いたい場合には,それらの命令文を,{ } でくくって複合文とする,ということを学びましたね。

関数の定義文においても,{ }を使って幾つかの命令文を一かたまりとしていますから,これも複合文であると言うことができます。

 さて,C言語には,変数の名前などの識別子は,それが型宣言された { } の内側だけで有効であり,その外側からは見ることができない,という規則があります。

例えば,上の average() 関数の場合には,その引数である x, y と,answer という名前の変数は,average() 関数を定義している { } の内側だけで使用することができ,その外側(例えば main() 関数の中)では使用することができません。

 複合文 { } の中で型宣言された変数は,その { } 内でしか見えませんから,例えば別々の関数の中で,同じ名前の変数を型宣言すると,それらの変数は,別々の変数として取り扱われる,ということになります。

例えば,上の average() 関数を,以下のような main() 関数の中で呼び出すとします。

    int main(void)
    {
        double x, y, c;
            .
            .
            .
        c = average(x, y); /* xとyの平均値を計算して,cに代入 */
            .
            .
            .
    }

この場合,main() 関数の中で型宣言されている変数 x, y と,average() 関数の中で型宣言されている変数 x, y は,名前は全く同じなのですが,プログラム全体の中では別物として扱われる,ということになります。

このような,識別子の有効範囲のことを,“ブロック有効範囲”といい,ブロック有効範囲を持つ変数のことを,“局所変数”といいます。(これに対して,“大域変数”という変数がありますが,これについてはおそらく次回に説明します。)


局所変数の利点

 この局所変数という仕組みがあるおかげで,プログラマが何かある仕事をする関数をオリジナルに作成した場合に,その関数をブラックボックスとしてユーザーに提供することができる,という利点があります。

ユーザーのほうは,プログラマが作成した関数の中でどのような名前の変数が使われているのかを一切気にせずに,自分で自由に main() 関数の中で使用する変数名を付けることができるというわけです。

ライブラリ関数は,このような仕組みでもって,ユーザーである我々に対しては,関数の使用方法(引数の型や与え方,返却値の型)以外は全くのブラックボックスとして提供されています。(我々は printf() 関数が,中でどんな変数を使っているのか,どんな計算手続きをしているのかは分かりませんね。)

 また,もっと単純な利点としては,プログラムを書く際に,たくさんの変数名をあれこれ考えなくともよい,ということもあります。

ある複合文の中で既に使われている変数名と全く同じ名前を,別の複合文の中の変数に付けたとしても,それらは別物として扱われますので,プログラム全体で使われる名前の数は少なくすることができます。

巨大なプログラムを一人で作成するときなど,変数名で悩むということは結構ありますから,これはなかなかありがたいものです。

あるいは逆に,大人数で一つのプログラムを作成するときも,この局所変数の仕組みは便利です。大人数で一つのプログラムを作成するときは,個人ごとに一つの関数を作成し,それら複数の関数を集めて一つの巨大なプログラムにすることがあります。そのようなときに,局所変数のみを使っていれば,自分の使用している変数名が,他人に使われてしまっているのではないか(他人の作成した関数によって,自分の使用している変数の値が意図せず書き換えられてしまうのではないか),といったことを心配する必要が,ないわけです。(自分の関数で使われている局所変数と,他人の関数で使われている局所変数は,たとえ変数名が全く同じであっても,別物として扱われる。)


UNIX作業環境のカスタマイズ



 第4回の授業のページで,UNIXがコマンドを探す仕組みについて説明しました。

UNIXシステムは,ユーザーがあるコマンドを入力すると,PATHという名前のシェル変数に記憶されているディレクトリの中でそのコマンドを探す,というものでした。

例えば,現在の作業ディレクトリをPATHの値に含めるために,以下のようなコマンドを実行しました。

% PATH=$PATH:.(enter)

上のコマンド文中,$PATH の部分は現在のPATHの値に置き換わります。それに,: で区切りを入れて,新たに . を追加することを,上記のコマンドは表しています。
( . は,作業ディレクトリを表す記号です。)

このようにして,シェル変数PATHの値を設定しておけば,いま自分のいるディレクトリ(作業ディレクトリ)の中についてもシステムはコマンドを探す対象としてくれます。

このようにしておけば,作業ディレクトリに納めた自分の作成したプログラムを実行させたい場合でも,わざわざパス名を含めてプログラム名を入力する必要はなく,単にプログラム名を入力するだけで済むことになります。
例えば,

% ./list1-3(enter)

などと入力しなくても,

% list1-3(enter)

と入力すれば,list1-3を実行してくれるようになるわけです。

 ところが,上記のコマンドを実行して,PATHの値を設定し直しても,次の授業のときに改めてログインして作業ディレクトリにある自分の作成したプログラムを実行してみようとすると,また,以前と同じように

プログラム名: command not found

と表示されてしまい,コマンドが実行できなくなってしまいます。

基本的に,シェル変数の値はシェルが起動される度にその値を設定しなければなりません。このような理由から,システムがコマンドを探すディレクトリの中に現在の作業ディレクトリを常に含めておきたい場合には,ログインする度に,PATHの値に作業ディレクトリを追加するコマンドを実行しなければならないということになります。

ただ,それではちょっと面倒ですね。

 そこで,いつログインしても自分の好みの同じ環境で仕事ができるように,シェルに対する命令を記述するファイルが存在します。このファイルはシェルの設定ファイルと呼ばれるもので,いわは初期設定ファイルのようなものです。ここでは, シェルの設定ファイルの変更の仕方を説明します。


シェルの種類を確認しよう

シェルの種類によって, シェルの環境設定ファイルは異なりますので, まず最初に, 現在使用しているシェルの種類を確認しましょう。
まずは, コマンドプロンプトで、下記のコマンドを入力してみてください。

% echo $SHELL

すると, おそらく, 下記のどちらかが表示されるのではないかと思います。

/bin/zsh

/bin/bash

上側が表示された方は, zshというシェルを使用していて, 下側が表示された方は, bashというシェルを使用しています.

シェルの環境設定ファイルは, どちらのシェルでも各々のユーザーのホームディレクトリ上に置かれます.

環境設定ファイルは, zshの場合, .zshrcというファイルがそれにあたり, bashでは,.bash_profileという名前のファイルがそれにあたります。このファイルには,シェル変数の設定のためのコマンドなど,シェルが起動する(例えばログインする)たびに最初に実行しておきたいコマンドを自由に書き並べておくことができます。

 さて,本当にそのような名前のファイルがあるのか見てみましょう。ファイルの一覧を表示するためのコマンドは... lsコマンドでしたね。今回は,lsの -a オプションの使い方を覚えて下さい。以下のように入力してみて下さい。

% ls -a(enter)

.zshrcまたは, .bash_profileという名前のファイルを見つけられましたか?

実は, .zshrcは初期設定では各々のユーザー用には作成されていないため, これまでに自分で作成していなければ, 上記のコマンドを入力しても見つけられないと思います。

.zshrcや.bash_profile以外にも,ファイル名が . (ドット)で始まるファイルが幾つか表示されたと思います。それらのようなファイル名が . で始まるファイルは,隠しファイルといって,通常はユーザーの目には見えないようになっていますが,lsの -a オプションにより,これらの隠しファイルも含めて全てのファイルの一覧を表示することができます。(-aオプションなしでlsコマンドを入力してみて下さい。隠しファイルは表示されないはずです。)

ここからは, シェルの設定ファイルを作成, または改造する方法を説明しますので, 自分の使用しているシェルの設定ファイルのところの手続きに従って, パスの設定をしてみてください。

シェルの設定ファイルを作成しよう(zshの方)

 .zshrc という設定ファイルは,単なるテキストファイルですので,テキストエディタを用いて簡単に作成, 変更することができます。


それでは早速,設定ファイル .zshrcを作成してみましょう...

テキストエディタで[ファイル]-[新規作成]を選び, 下記のように入力します.

export PATH=$PATH:.

(注意)PATHと=の間, =と$PATH:.の間にスペースを入れてはいけません.

そして, [ファイル]-[保存]で, ファイル名を.zshrcに, 保存フォルダをホームディレクトリにします.
すると["."(ドット)で始まる名前はシステム用に予約されています。]と警告が出ます。今回は,.zshrcファイルを作成したいので, ["."を使用]を選択します。

これで設定は終わりです。

なお, 既に.zshrcがあるホームディレクトリにある場合には, 対応が変わってきます.

.zshrc のように,ファイル名が . (ドット)で始まるファイルは,隠しファイルといって,通常はユーザーの目には見えないようになっているため,このままではテキストエディタで開くことができません。

そこで,一時的に,ファイル名を( . で始まらないようなファイル名に)変更して,テキストエディタで開けるようにしておきましょう。

そのために,前回学んだ,ファイル操作のためのUNIXコマンドを使います。ファイル名を変更するには,以下のコマンドを使用すればよいでしょう。

mv ファイル(や ディレクトリ)を移動する
% mv 旧ファイル名 新ファイル名

と入力すると,旧ファイルの名前が,新ファイル名に変更されます。

% mv 移動元ファイル名 移動先ディレクトリ名

と入力すると,移動元ファイルが,移動先ディレクトリの中に移動します。


  1. まず,mvコマンドで,.zshrcを,例えばtmpというファイル名に変更します。

    % mv .zshrc tmp(enter)

  2. ファイル名を変更したら,そのファイルを,自分の好きなエディタ(例えば,テキストエディット)で開きます。

  3. 以下の行を新しく付け加えます。

    export PATH=$PATH:.

  4. 他にも自分なりに最初に実行しておきたいコマンドがあれば,書き込んでおくとよいでしょう。
    ただし,このファイルに書き込んだコマンドは,シェルが起動される(新しいターミナルウィンドウを開くなど)度に実行されてしまうことに気をつけて下さいね。(あまりたくさんコマンドを実行させると逆にうざいかもしれません。)

  5. 入力し終わったら,同じ名前(tmp)で保存(save)します。

  6. もとのファイル名に戻しておきます。

    % mv tmp .zshrc(enter)


シェルの設定ファイルを改造しよう(bashの方)

 .bash_profile という設定ファイルは,単なるテキストファイルですので,その中身はテキストエディタを用いて簡単に変更することができます。

それでは早速,設定ファイル .bash_profileを自分用に改造してみましょう...

とその前に,さきほども言ったように,.bash_profile のように,ファイル名が . (ドット)で始まるファイルは,隠しファイルといって,通常はユーザーの目には見えないようになっているため,このままではテキストエディタで開くことができません。

そこで,一時的に,ファイル名を( . で始まらないようなファイル名に)変更して,テキストエディタで開けるようにしておきましょう。

そのために,前回学んだ,ファイル操作のためのUNIXコマンドを使います。ファイル名を変更するには,以下のコマンドを使用すればよいでしょう。

mv ファイル(や ディレクトリ)を移動する
$ mv 旧ファイル名 新ファイル名

と入力すると,旧ファイルの名前が,新ファイル名に変更されます。

$ mv 移動元ファイル名 移動先ディレクトリ名

と入力すると,移動元ファイルが,移動先ディレクトリの中に移動します。


  1. まず,mvコマンドで,.bash_profileを,例えばtmpというファイル名に変更します。

    $ mv .bash_profile tmp(enter)

  2. ファイル名を変更したら,そのファイルを,自分の好きなエディタ(例えば,テキストエディット)で開きます。

  3. 以下の行を新しく付け加えます。

    PATH=$PATH:.

  4. 他にも自分なりに最初に実行しておきたいコマンドがあれば,書き込んでおくとよいでしょう。
    ただし,このファイルに書き込んだコマンドは,シェルが起動される(新しいターミナルウィンドウを開くなど)度に実行されてしまうことに気をつけて下さいね。(あまりたくさんコマンドを実行させると逆にうざいかもしれません。)

  5. 入力し終わったら,同じ名前(tmp)で保存(save)します。

  6. もとのファイル名に戻しておきます。

    $ mv tmp .bash_profile(enter)


シェルの設定ファイルの動作確認

 では,新規作成したり, 改造したりした .zshrcや.bash_profile がうまく働いてくれるか確認してみましょう。

もう一つ別の新しいターミナルウィンドウを開いてみて下さい。その新しいターミナルウィンドウ上で,PATHの値にちゃんと . が追加されているか,以下のコマンドを入力して確かめてみて下さい。

$ echo $PATH(enter)

ちゃんと . が追加されていますか?

 このように新しいターミナルウィンドウを開くと,その新しいターミナルウィンドウ用のシェルが新たに起動されます。そのとき,皆さんの編集した.bash_profileの中身が自動的に実行されるので,そこでPATHの値が変更される,という仕組みになっているのです。

当然次回ログインしたときも,シェルは起動されますので,ユーザーの望みどおりの環境がログインする度に構築される,というわけです。


今日の実習問題と宿題



(注意) プログラムをトレースするときは,ただ単に写すのではなくて,そのプログラムがどのような仕組みで動作をしているのかを“解読しながら”トレースして下さい。

単に手を動かすだけでなく,頭を使って“理解”することが重要です!

(重要)  宿題の締切は次の授業が始まる前までとします。きちんと動作をチェックしてから,提出してください。


戻る