CONTENTS / BACK-PAGE / NEXT-PAGE

コラム1

定義と宣言(Definition & Declaration)

 これまでの講義やQ&Aで関数の宣言と定義について詳しく解説していな いので,ここで解説する.


例によって,用語の解説から行う.
まず以下のプログラムを見ていただきたい.


#include <stdio.h>   ← 標準入出力関数の使用を宣言
main()
{
    int power(int j);  ←  int型の引数を一つ持ちint型の値を返すpower関数の宣言
                        この様に関数の使用を宣言している所を宣言部と呼ぶ.
    int x, y;          ←  使用する変数の宣言定義

    scanf("%d", &x);
    y = power(x);                 ←  この時 xを実引数と呼ぶ.
    printf("%d! = %d\n", x, y);
}

int power(int a)         ←  power関数の定義.この時 aを仮引数と呼ぶ.
{                                            以下{から}までを定義部と呼ぶ.
    int dum;

    if (a == 0)
            dum = 1;
    else
            dum = power(a - 1);
    return dum;
}

このプログラムで注目していただきたいのは,宣言と定義の違いである.
(間違ってもこれを切りとってコンパイルしてエラーが出るなどと投稿しないように)

宣言とは関数の中(あるいは外)で使用する前に行われ,定義とはその関数そのものである.


 ここでは,関数の宣言と定義について基本的なことと,#includeについて述べる.

 関数の宣言という機能(Function Proto Type)は,C言語の第2版から正式な機能として追加された(実は第1版と第2版の大きな違いは,この関数宣言があるかないかと言う事である).第1版ではその様な機能はなく,ユーザは好きなところで勝手に関数を呼びだし,勝手に関数を追加できた.ところが,この様な無法状態において以下のようなバグが検出されるようになった.


                    ← 関数の宣言がないので,#include機能もない.
main()
{
    int i, k, j;
    ………………………………
    k = afunc(i, j);
    ………………………………
}

int afunc(a, b, c)
int a, b, c;        ← 第1版のCでは実引数はこの様に書いた.
{
    ………………………………
}

 つまり,宣言部がなく,定義部しかなかったのである.現在,我々がパソコンで使っているコンパイラでは,これはエラーとしてちゃんとコンパイル時に検出してくれるが,昔のCではこの呼出が可能であり,かつその動作の保証は各コンパイラメーカに任されていた.そこでANSI(米国のJISのようなもの)は関数について,使用前に宣言し,その宣言と定義部(本体)を比較し,(引数の個数や型が)異なる場合はエラーとしてコンパイラに警告させる機能を加えた.これにより上記のバグはなくなり,プログラマはコンパイル時により多くのエラーを発見することが出来るようになった.

 では,宣言に関する規則はどうなっているのであろうか.文法的には,どこで宣言しても構わないが,”使用前に宣言する事”である..では,使用する前にとは,具体的にどこを指すのであろうか.その前に,通用範囲(scope)について述べておく.

 通用範囲とは通常,ある変数が”使用できる範囲”を表す言葉として用いられる.我々が既に学んだものは,

 変数の宣言−1)ある関数内で宣言した変数は,その関数内でしか使えない.
ということであった.これ以外にも,

 変数の宣言−2)関数の外で宣言した関数は,どの関数からでも使える.
 変数の宣言−3){ }でくくられた範囲で宣言されたものは,その範囲内でしか使えない.
というものがある.

 変数の宣言−2)はいわゆる大域変数(グローバル変数)で,1),3)は局所変数と呼ばれる.(図−1参)


ex-1.

int g;

main()
{
    ………………………………
    ………………………………
}

int afunc(int a)
{
    ………………………………
    ………………………………
}

int otherfunc(cahr c)
{
    ………………………………
    ………………………………
}
図−1 大域変数

 大域変数(gloval variable)gはmain関数からでも,afunc関数からでも,otherfunc関数からでも参照できる.


 この機能は,複数の関数で,同時に同じ変数を必要とする場合に有効である.例えばスタックを操作する関数,pushとpopは同時にスタックを共有する必要がある.(図−2参)


ex-2.

 スタックとはファーストインラストアウト(First In Last Out)とも呼ばれ,一番上にあるものしか取り出せないものである(ハノイの塔がいい例であろう).この場合,スタック自身はいつ入れて,いつ取り出されるか分からないので,popとpushで共有する必要がある.


 ex-2.の場合,stackとposの宣言部は,popとpushの定義部の前であれば,どこであってもかまわない.つまり,逆に必ずpopとpushの前(直前でなくとも良い)で宣言しなければ成らない.

 余談であるが,図−2の様な場合を除き,大域変数は使用すべきでない.関数の独立性が低くなってしまうからである.ex-2.では,popとpushは常にひとまとまりで使用し,stackとposを必要とすることになる(この4つをワンセットで使う分には問題ない).

 変数の宣言−3)は通常ではあまり現れない.下手に使用すると混乱を招くだけである.(図−3参)


ex-3.

 このプログラム(の部分)は一見間違いに見えるかも知れないが,きちんと動く.ちなみに,図中の番号は異なるiを指し,同じ番号で指されているものは同じiを表す.


 筆者は卒業研究で,これが何重にも重なったプログラムを扱わなければならない事があったが,非常に見づらく,処理プログラムを作成しなければならない程であった.これは”ちょっと使わせてもらおう”的な発想によるものであろうが,使用はお勧めできない. いずれの場合にしろ,使用前に宣言すると言うことが,大事である.

 では,ここで話を関数の宣言に戻そう.関数をどこで宣言するかということは,その関数をどの範囲で使用するか,と言うことによって決まる.つまり変数の宣言と同じで,関数の外で宣言すれば,それ以降の関数で全て呼び出すことが出来る.ある関数内で宣言すれば,その関数からしか呼ぶことが出来ない.また,{ }内で囲んだところで宣言してしまえば,その中でしか呼ぶことが出来ない(但し,コンパイラによっては呼び出すことができる).結局,関数の宣言位置も変数の宣言位置と同様に,

関数の宣言−1)関数内で宣言する.
         →その関数内でしか呼び出すことが出来ない.

関数の宣言−2)関数の外で宣言する.
         →その宣言以降の関数全てから呼び出すことが出来る.

関数の宣言−3){ }内で宣言する.
         →{ }内でしか呼び出すことが出来ない.
という事が言える.

関数が必ず{ }でくくられていることを考慮すれば,1)も3)も同じ事を言っている.

 では,グローバル変数やほとんどの関数を全て一番上で宣言すればいいかというと,そうでもない.確かにそうすれば,どの関数からも呼び出す(参照する)ことが可能であるが,これは余りいいことではない.出来れば,関係ない関数からは呼び出せない(参照出来ない)ようにしておいた方がよい.やたらと必要ないところから,呼び出せるようにしておくのは,得策ではない.関数を作成する際には,ここの関数間の独立性を高め,必要な関係だけ結ぶようにしておくことが重要である.例えば,先のex-2.においてstackとposは一番上ではなく,popとpushの直前においておくことが望ましい.この二つの変数は,この二つの関数からしか参照しないためであり,他の関数から参照する必要性は一切起こり得ないからである.(もし起こり得た場合は,アルゴリズムを考え直した方が良い)

 関数の定義部(本体)は,ファイル内のどこにあってもよい.同じファイルでなくてもインクルードファイルの中にあればそれでよい(後述する).要は,使用する前に宣言しておけば良いのである.

 さて,関数の宣言が定義されると,使用する関数を事前に宣言しておかなければならなくなった.しかし,いちいち

int printf(………………………………); /*驚いてはいけない.printfも */
int scanf(………………………………); /*scanfも ちゃんと返り値がある*/
 char getchar();

などと宣言するのは面倒であり,既に多く用意されている関数を,(宣言するために)全て覚えるのも大変である.(事実我々はこの様な宣言を行わずに,これらの関数を用いている)

 そこで,システムが予め用意した関数はユーザがいちいち宣言しなくてもすむように,ヘッダファイルというものにまとめて宣言されている.ヘッダファイルは通常,拡張子がhのもので,既に学んだもので stdio.h,stdlib.h,ctype.h,math.h などがある(詳しくは各書籍を参考されたい).これらのファイルには,その名前が示し通りも関数が宣言されている.例えば,

 stdio.h   →  Standard I/O     : 標準入出力関数群
                    printf, scanf, getchar, putcharなどの関数とEOF.

  stdlib.h  →  Standard Library : 標準ライブラリ群
           atoi, itoaなど.(まだ,解説していない)

  ctype.h   →  Character Type   : 文字操作関数群
                    isalpha, isdigit, tolower, toupperなど.

  string.h   →  String           : 文字列操作関数
                    strlen, strcat, strcpyなど.

  math.c   →  Mathmaticals     : 数学用関数群
                    sin, cos, log, sqrtなど.
などである.これらのヘッダファイルは通常ファイルの先頭で

#include <stdio.h>
として,その使用を宣言される.

では,具体的にどのようなことが行われているのか解説しよう.

 Cでは#で始まる行はすべてプリプロセッサ(Preprocessor:前処理系とも言う))というものが処理する.つまりソースプログラムの#includeや#defineなどをコンパイラに通す前に処理する(プリプロセッサが処理するものについてはここでは言及しない.詳しくは各書籍を参考にされよ).

#includeも#defineも処理自体は簡単で,

#include →  ヘッダファイルをその位置に張り付ける.
#define →  定義された文字列を,元の文字列に書き直す.
という事だけである.

 つまり,先頭に#include<stdio.h>と宣言すると,その位置に(つまり先頭に)stdio.hの内容が全てコピーされる(たった一つの関数しか使わなくても).そうしておけば,以降の関数では全て,stdio.h内の関数を全て呼び出すことが出来る.

#defineは
#define MAX 100
と書いてあれば,ソースプログラム内の,MAXを全て100に書き換えるだけである.

 さて,話が少しそれるが,このようなヘッダファイルは,システムが用意するだけでなく,ユーザの方で自由に作っておくことが出来る.今まで作った関数を,mylab.hなどのファイルに取っておき,それを#include "mylab.h"と宣言すれば,その中ある関数は新しいプログラムで自由に使うことが出来るようになる.(#include <>と#include ""の違いは後で述べる.)

 筆者は永年,Cを使っているので個人で作成したヘッダファイルが4つほどある.以下にその一つ,base.hを載せるので参考にされたい.

base.h

/*
   Base.h      Made By T.Sugaya  1990-

    int getline(char s[], int lim)
                           : 最大limの長さの文字列をsに格納し,その長さを返す。
    void swap(int *a, int *b)  : 整数a, bの内容を交換する。
    void reverse(char *a)      : 文字列 aの内容を反転する。
    char *itoch(int a)         : 整数 aを文字列に変換する。
    char *smalls(char *a)      : 文字列 aの内容を全て小文字に変換する。
    char *larges(char *a)      :                     大
    char *lamalls(char *a)
                       : 文字列 aの最初の1文字を大文字,後は全て小文字にする。
    int ipow(int a, int b)     :  aの b乗を求める。
    int comb(int a, int b)     :  aCbを求める.
    int func(int x)            :  x!を求める.
    int rnd(unsigned long a)   :  乱数を求める.
    int rnd2()                 :  時間を基に乱数を求める.


*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>

int getline(char s[], int lim)
{
    int c, i;

    for (i = 0; i < lim - 1 && (c = getchar()) != EOF && c != '\n';  ++i)
        s[i] = c;
    if (c == '\n') {
        s[i] = c;
        ++i;
    }
    s[i] = '\0';
    return i;
}

void swap(int *a, int *b)
{
    int c;

    c = *a;
    *a = *b;
    *b = c;
    return;
}

void reverse(char *a)
{
    char b, *c = a;

    while(*c != '\0')
        c++;
    c--;
    while(a < c)
    {
        b = *a;
        *a++ = *c;
        *c-- = b;
    }
    return;

}/*  End Of Reverse  */

char *itoch(int a)
{
    char *dis, *dum;
    void reverse(char *a);
 
    dis = dum = (char *)malloc(20 * sizeof(char));
    do{
        *dis++ = a % 10 + '0';
    }while ((a /= 10) > 0);
    *dis = '\0';
    reverse(dum);
    return dum;

}/*  End Of Itoch  */

char *smalls(char *a)
{
    char *b = a;

    while(*a != '\0')
        *a = tolower(*a++);
    return b;

}/*  End Of Smalls  */

char *larges(char *a)
{
    char *b = a;

    while(*a != '\0')
        *a = toupper(*a++);
    return b;

}/*  End Of Larges  */

char *lamalls(char *a)
{
    char *b = a;

    *a = toupper(*a++);
    while(*a != '\0')
        *a = tolower(*a++);
    return b;

}/*  End Of Lamalls  */

int ipow(int a,int b)
{
    int i, dum = 1;

    if (!b)
        return 1;
    for (i = 0; i < b; i++)
        dum *= a;
    return dum;

}/*  End Of Ipow  */

/*********  Combinator   ******************************************************/

int comb(int a, int b)
{
    int c;
    int func(int x);

    b = (b > a - b) ? (a - b) : b;
    c = func(a)/(func(a - b) * func(b));

    return c;

}  /*  End Of comb  */

int func(int x)
{
    int dum;

    if (x == 0)
        dum = 1;
    else
        dum = x * func(x - 1);

    return dum;
}  /*  End Of func  */

unsigned rnd(unsigned long a)
{
    return((23 * a) % 32749);
}/*  End Of rnd  */

unsigned rnd2()
{
    unsigned rnd(unsigned long a);
    long tp;

    return (rnd(time(&tp)));
}/*  End Of rnd2  */
 この中の関数は,全て標準では用意されていないが,かなりの頻度で使用するので筆者が用意し必要に応じて用いている.ファイルのサイズさえ気にならなければ,全てのプログラムの先頭に,
#include "base.h"
と書いておくだけですむ(base.hの先頭に標準ヘッダファイルが宣言してあることに注意.これで全てのファイルが取り込まれる).そして,main()でswap関数を使用するのであれば,
main()
{
    void swap(int, int);
    ………………………………
    ………………………………
としておけばよい.宣言も面倒なら,base.hの最後に全ての関数の宣言部を記述しておけば良い(ここまですると,プログラミングの際に混乱を招きそうなので,筆者は定義部のみをヘッダファイルに置いている).

 欠点は(全てのファイルを取り込んでしまう為)ファイルサイズが膨大になり,メモリとディスク容量が無駄になる点である.しかし,これを気にしないですむ環境(例えばUNIX環境)では非常に楽である.

ちなみに,このbase.hは筆者個人で使う(人に見せるためのプログラムではない)ので,詳しい解説は省かせていただく.

 #includeの<>と""の違いは,そのファイルの置いてある場所(ディレクトリ)である.ソースプログラムと同じディレクトリに置いてあるなら""で,システムが指定したディレクトリ(通常はinclude)に置いてあるなら<>で,ファイル名をくくる.例えばこのbase.hをソースファイルと同じデレィクトリに置いた場合は #include "base.h"になるし,ソースプログラムがあるディレクトリの親ディレクトリにあるなら #include "..\base.h"となる.筆者は通常,ソースプログラムと同じ場所に置いておくので,上記のような宣言になる.また.拡張子は必ずhである必要はなく,cでもよい.但し,main()だけは一つであることが条件なので注意する必要がある.(図−4参)


ex-4.

A file whose name is test1.c

    #include <stdio.h>

    int thefunc(int a, char c);

    main()
    {
        ………………………………
        ………………………………
    }

    int thefunc(int a, char c)
    {
        ………………………………
        ………………………………
    }

Another file whose name is test2.c

    #include "test1.c"

    main()
    {
        ………………………………
        k = thefunc(1, "c");
        ………………………………
    }
a)失敗例
 test1.cはmain()を含んでいるので,test2.cでインクルードできない.

A file whose name is test1.h

    #include <stdio.h>

    int thefunc(int a, char c)
    {
        ………………………………
        ………………………………
    }

Another file whose name is test1.c

    #include "test1.h"

    main()
    {
        ………………………………
        ………………………………
    }

Another file whose name is test2.c

    #include "test1.h"

    main()
    {
        ………………………………
        k = thefunc(1, "c");
        ………………………………
    }
b)成功例
 ある関数を複数のファイルで使用する場合は,ヘッダファイルに入れておき,それぞれでインクルードすると良い.

図−4 ファイルのインクルード
 どのファイルも同じディレクトリにあるとする.


 最後に宣言時の引数名であるが,これは必ずしも定義部と同じ物である必要はなく,また,なくてもよい.要は,何型の引数がいくつあるかさえ分かれば良いのである.例えば,base.h内のgetlineを使用する際の宣言は int getline(char [], int); でもよい.

 さて,以上で関数の宣言と定義についてお分かりにいただけたであろうか.例によって,ご質問を承るので,どしどしおよせいただきたい.

一休み  コンピュータの計算精度

身近にある,コンピュータ,電卓等で以下の問題を解いてみてください. コンピュータを使用する場合は,色々な言語で試してみることをお勧めします.
 問題) 1/nを,n回,たしてみる.(但し,nは自然数)
当然,答えは”1”になるはずなのですが,,,,,.あれれ,なりませんね.

計算が得意なコンピュータも,計算精度については結構弱いものです. 計算精度をあげるには,,,,,,.いずれ講義があるでしょう.

コラム2

名前について(Naming)

 名は体を表す.これはプログラムを作成する際,非常に重要なことであろう.

 標準ヘッダファイルを覗くと,実に多くの関数が存在し,かつ同じファイル内には似たような名前があるのに驚く.しかし,同じ様な機能が集められて一つのヘッダファイルを構成していることを考えれば,むしろこれは当り前のことかも知れない.多く存在する関数も中で他の関数を呼び出していることは当り前で,それゆえ名前も似てくる.

 逆に我々が関数や変数を使用する場合,その性質を良く表す名前を用いなければならない.x, yはともかくa, bは練習以外では用いるべきではない.関数名も同様である.

 なぜか,これは以下の2点による.

1)なるべく注釈や,解説がなくてもよいことが望ましい.

2)独立性を高めるため,内部(定義部)を公開しない方がよい.
  つまり,ソースを見て解析することはしない方がよい.

 どんなプログラムでも注釈や解答を入れなくてすむにこしたことはないが,長すぎるものは良くない.変数や関数の名前とわずかな注釈で,すます方がよい.

 どちらかというと,2)の方が問題である.我々は通常,printfやscanf,strcpy, sin, sqrt, tolowerなどの標準関数が,どの様にして実現されてるか知らないが,その名前と引数の型,個数を知っているので呼び出して使うことが出来る.つまり,他のユーザに公開するのは名前と引数の型と個数,機能だけでよいのである.それがどこでどの様に実現されているかは,その関数の制作者が責任を負えば良いことである.

 名前が機能を表しているものは非常に多い.printf(PRINT Function), strcpy(STRing CoPY), EOF(End Of File)など標準関数(変数)はそのほとんどが,名前が機能を表している.


CONTENTS / BACK-PAGE / NEXT-PAGE