プログラミング演習第7回

関数とポインタ（Call by Value, Call by Reference）

2000 年 5 月 25 日

目次

1 今までの講議に関する確認と補足 1 1.1 小数型の入出力 (printf, sscanf) . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.2 式の値 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.3 実行ファイルの名前を変えるには . . . . . . . . . . . . . . . . . . . . . . . 2

2 関数とポインタ 2 2.1 引数はコピーされる . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 2.2 変数のアドレス . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5

3 練習問題 10

1 今までの講議に関する確認と補足

1.1 小数型の入出力 (printf, sscanf)

この講議では，変数の値の出力は printf，出力には sscanf を使用していますが，小数型 (double) の入出力について確認しておきましょう．小数型 (double) の変数 x にキーボードから入力するには，

char mojiretsu[80];
double x;
printf("数字を入力して下さい >>");
gets(mijiretsu);
sscanf(mojiretsu, "%lf" &x); /* & を忘れないこと */.

のようにします．

これに対して， double 型の変数 x を出力するときは，

printf("%f", x);

または，

printf("%g", x);

とします．入力は， %lf 出力は， %f となり形式が異なります．これに対して，整数型 (int) の変数は，入力も出力も %d を用います．

1.2 式の値

代入文は，式自体が値を持ちます．この値は，左辺に代入された値と同じです．下の例で確認してみましょう．

a = 1;
while (a) {
    if ( (r = m % n) == 0 ) {
        break;
    }
    m = n;
    n = r;
}
printf("%d\n", n);

２行目の while 文の（）の中には条件式以外も書くことが出来ます．ここが 0 のときは，繰り返しを行わず， 0 以外のときは，繰り返しを行います．

３行目の if 文は， r に m を n で割った余りを代入し，その値が 0 であれば，ループを抜けるという意味になります．

1.3 実行ファイルの名前を変えるには

file.c という名前のファイルを cc コマンドでコンパイルすると， a.out という実行ファイルが作成されましたが， UNIX のコマンド mv を使って名前をかえることが出来ます．

     mv  a.out  excute

このようにすると，実行ファイルは， excute という名前になりますから，実行させたいときは， excute とタイプします．また，コンパイルの祭に， a.out というファイルを作らず，直接，実行ファイル excute を作るには，次のようにタイプします．

     cc  file.c  -o  excute

2 関数とポインタ

前回, 前々回と関数の使い方について学習しました．関数の引数にポインタと呼ばれる変数を渡すことで，本格的なプログラムが組めるようになります．ポインタは， C 言語の大きな特徴であり，最も大事な部分ですので，参考書などを良く読んでしっかり理解するようにして下さい．カーニハン・リッチーの「プログラミング言語Ｃ」では，５章でポインタについて扱っています．

2.1 引数はコピーされる

まずは，復習のために ”普通の” 変数を引数に持つ関数を考えましょう．

２つの変数の値を入れ替えて出力するプログラムは次のようになります． /* 変数の値を取り替えるプログラム */

#include <stdio.h>
main()
{
int    x, y, tmp;
x = 11;  y = 20;          /* 初期値を設定します */
printf("%d %d\n", x, y);  /* 初期値が出力されます */
tmp = x;    /* tmp に 11 が代入されます */
x = y;      /* x に 20 が代入されます */
y = tmp;    /* y に 11 が代入されます */
printf("%d %d\n", x, y);  /* 入れ替えた値が出力されます */
}

ここで，変数を取り替える処理を行っている部分を swap という関数にしてみたいと思いますが，次のプログラムは，実は期待通りには動きません． /* 変数の入れ替え（関数版，失敗編） */

#include <stdio.h>
void swap(int xx, int yy); /* 使用する関数は，事前に宣言します
                           * 関数 swap には return 文が無いので

* その型は void です．

*/

main()
{
  int    x, y;
  x = 11;  y = 20;          /* 初期値を設定します */
  printf("%d %d\n", x, y);  /* 初期値が出力されます */
  swap(x, y);               /* swap 関数を呼び出します */
  printf("%d %d\n", x, y);  /* 初期値が出力されます */
}
void  swap(int xx, int yy)
{
  int  tmp;
  tmp = xx;
  xx = yy;
  yy = tmp;
}

このプログラムが何故，変数の交換を行ってくれないか考えてみます．このプログラムでは， main で宣言した変数 x, y が引数として関数 swap に受け渡されていますが，関数 swap は変数そのものでなく，値のコピーを受け取ります. 図 1 を見て下さい．

上のプログラムでは， x, y に代入された値 11, 20 がそれぞれ， swap 内部の変数 xx, yy の初期値として代入されますが． swap の最後の文で main に戻ったとき， main は swap 内部の変数で何がおこったのか知りません．従って， main の変数 x, y は， swap の後でも同じ値を持ち続けます．

注意 1 上の swap ように関数値を返さない関数を void 型関数と呼びます. main

図 1: 変数の値渡し

関数は実は void 型関数です. void 型関数の形式は関数値を返す関数と形式は同じですが, a = swap(x,y) のような指定は関数値を返さないので意味がありません.

注意 2 swap の変数の宣言で int xx, int yy の部分をそれぞれ， int x, int y と変えればうまく動くと思う人がいるかもしれませんが，結果は同じです．関数の内部で宣言された変数は，他の関数の変数と同じ名前であっても，別の変数として扱われます．

2.2 変数のアドレス

C 言語では，使用する変数は（関数も）すべて，事前に宣言しなければなりません．宣言された変数は，コンピュータは宣言された変数の値を保存しておくための箱（領域）を確保します．この箱には住所（アドレス，番地）が付けられています． C 言語の特徴の１つとして，アドレスを変数として扱えるということがあります．アドレスは，変数の箱の位置を指し示すという意味で，ポインタとも呼ばれます．図 2 を見て下さい．

ポインタを使用したプログラムの例を見てみましょう．次のプログラムを良く読み，実行結果を予想して下さい．実際にプログラムを組んで結果を確かめてみて下さい．（ファイル名 7-1.c）

図 2: ポインタ概念図

#include <stdio.h>
main()
{
  int    a;      /* a には整数がはいります */
 /*

* ポインタを値にもつ変数は次のように * をつけて宣言します

* 下の例の場合 pa には整数型の変数のアドレスが入ります

   */
  int    *pa;
  a = 3;

pa = &a; /* &a とは a のアドレスという意味です */ *pa = 5; /* *pa とは pa のアドレスの中身という意味です */

  printf("a = %d\n", a); /* 何が出力されるでしょうか？ */
}

このプログラムでは，変数 a は最初に 3 が設定されたあと，何も代入されていません．にもかかわらず， a の値は書き換えられています．

このように，ポインタ変数を利用すると，変数の値を間接的に変更することが出来ます．これを利用すると関数にアドレスを渡すことでもとの変数を書き換えられることがわかります．（図 3）

図 3: 変数のアドレス渡し

注意ポインタ型の変数にはアドレスが入りますが，この場合でもそのポインタの行き先の変数の型により，宣言の型も異なります．例えば， double 型の変数のアドレスを格納するポインタpx の型の宣言は，

double      *px;

または，

double *     px;

とします．

最初のプログラムを正しく書き換えてみましょう．最初のプログラムと図 3 を参照しながら，次のプログラムの欠けている部分を補い，完成させて下さい．（ファイル名 7-2.c）「プログラミング言語Ｃ」の５章を参照するのも良いでしょう． /* 変数の入れ替え（ポインタ使用） */

#include <stdio.h>
void swap(int *px, int *py); /* ポインタ変数は * をつけて宣言

* 返り値が無いので void で宣言します

                             */
main()
{
  int    x, y;
  x = 11;  y = 20;          /* 初期値を設定します */
  printf("%d %d\n", x, y);  /* 初期値が出力されます */
  swap(&x, &y);             /* swap にアドレスを渡します */
  printf("%d %d\n", x, y);  /* 新しい値が出力されます */
}
void swap(int *px, int *py)
{
  int  tmp;
  tmp = *px;
  *px = *****;    /* ここを補って下さい */
  *py = *****;    /* ここを補って下さい */
}

余裕のある人は，もう少し，例を見てみましょう．次のプログラムは，四則演算を行う関数をポインタを利用して書いたものです．先程と同様，欠けている部分を補ってプログラムを完成させて下さい．（ファイル名 7-3.c）

#include <stdio.h>

/* 使用する関数は事前に宣言します*/

void wasa(int a, int b, int *pwa, int *psa);
void sekishou(int a, int b, int *pseki, double *pshou);
main()
{
int       a, b;
int       wa, sa, seki;
double    shou;
a = 2; b = 7;
wasa(a, b, &wa, &sa);
sekishou(a, b, &seki, &shou);
printf("a と b の和は %d 差は %d です\n", wa, sa);
printf("a と b の積は %d 商は %f です\n", seki, shou);
}
void wasa(int a, int b, int *pwa, int *psa)
{
*pwa = ****;    /* ここを補って下さい */
**** = ****;    /* ここを補って下さい */
}
void sekishou(int a, int b, int *pseki, double *pshou)
{
double    fa, fb;
*pseki = *********;    /* ここを補って下さい */

/* 小数型にするために 1.0 を掛けます． */

fa = 1.0 * a;
fb = 1.0 * b;
*pshou =  ********;      /* ここを補って下さい*/
}

3 練習問題

問題１確立論や統計学で重要な「正規分布」に現れる関数

f(x) = \frac{1}{\sqrt{2}}e^{− \frac{x^{2}}{2}}

を -1.96 から 1.96 まで積分した値の近似値を台形公式，区分求積を用いて求めるプログラムを作成せよ．（ヒント：答えは， 0.950 に近い）分割の個数を 10, 100, 1000, 10000 とし， 0.950 との誤差を比較せよ．

問題２（レポート問題） Euclid の互除法のプログラムを関数化して， main 中で

    euclid(m, n, &gcm);

とすると gcm に m, n の最大公約数が代入されるようなプログラムを作成せよ．問題３正の整数の入力に対し，それが素数であるかどうかを判定するプログラムを作成

せよ．

レポート問題の締め切りは, 6 月 6 日 (火) です.