レポート見本 1

データ構造とアルゴリズム I レポート課題

題名
関数とポインタ
学籍番号
05kc999
氏名
坂本直志

課題

課題 1-1

次の条件を満たす関数 dispArray() を作りなさい。

  1. 入力は整数型のポインタ
  2. 入力されたポインタに対して、順に格納されている正の整数を 10 個ずつ 表示する。
  3. 入力に -1 が入っていたら終了する

例えば整数型の配列 a[] が 1 から 15 まで順に入っていて、最後に -1 が入っ ている時、dispArray(a) は次のような出力をする。

1 2 3 4 5 6 7 8 9 10
11 12 13 14 15

課題 1-2

次の条件を満たす関数 square() を作りなさい。

  1. 入力は整数型のポインタと整数値 n
  2. 入力データはポインタから順に正の整数が並び、最後に -1 の値で終了す る
  3. この関数は入力されたポインタが参照する正の整数の要素に対して、 要素の値を二乗して、 n で割った余りを求め、その得られた値を要素の値と して格納する。

例えば 1, 2, 3, -1 という配列の先頭番地と 3 を square() に与えると 1, 1, 0, -1 となります。

課題 1-3

課題 1-1, 1-2 で作成した dispArray(), square() に対して次のプログラム を動かし結果を報告しなさい。


#include <stdio.h>
void dispArray(int *p);
void square(int *p, int n);
#define N 64
int main(void){
  int *a[5];
  int b[]={-1};
  int c[]={1,-1};
  int d[]={1,2,3,-1};
  int e[N+1];
  int size[]={1,2,4,N+1};
  int i,j;
  for(i=0;i<N;i++){
    e[i]=i;
  }
  e[N]=-1;
  a[0]=b; a[1]=c; a[2]=d; a[3]=e; a[4]=NULL;
  for(i=0;a[i]!=NULL;i++){
    dispArray(a[i]);
    for(j=0;j</* 学籍番号の下三桁 */; j++){
      square(a[i],size[i]-1);
    }
    dispArray(a[i]);
  }
  return 0;
}

基礎知識

ポインタ

C 言語では変数の格納されている番地を扱うことができる。 変数名の前に & を付けると、その変数の番地を意味する。 また、変数の番地を取り扱う変数も用意されている。 そのような変数をポインタと言う。 ポインタは格納される値の型で区別される。 int の値が入る変数のポインタは int 型のポインタなどと言う。 そして int 型のポインタの宣言は int *ポインタ名 のよ うにする。 ポインタに対して、その番地に入っている値は*ポインタ名で表す。 以下に簡単な例を示す。


int x, *y;
x=1;
y=&x;
printf("%d\n",*y); /* 1 が表示される */

C 言語では配列変数はポインタにより実装されている。 配列 a[0], a[1] に対して、 a は a[0] の番地を示している。 つまり a と &a[0] は同じになる。 また、ポインタに対しても [値] とすると配列と同じように要素にアクセスで きる。 さらに、ポインタに 1 を足すとメモリの番地として 1 増えるのではなく、配列 として次の要素を指すようになる。 つまり次のような操作が許される。


int a[2];
int *x;
a[0]=5;
a[1]=6;
x=a;
printf("%d\n",*x); /* 5 が表示される */
printf("%d\n",x[1]); /* 6 が表示される */
x+=1;
printf("%d\n",*x); /* 6 が表示される */

つまり配列の宣言とポインタの宣言は領域を確保するかしないかだけで、基本 的には同じような意味となっている。

関数

C 言語ではプログラムを分割、再利用するために関数と言う概念がある。 関数は関数名(引数1,引数2, ...)という形で呼び出すと値を返す。 返してきた値を変数に代入するには変数=関数名(引数1,引数2, ...)という構文になる。 C 言語でプログラムを実行する時に呼び出されるのが main 関数である。

一方、関数の内部で定義された変数はローカル変数と呼び、他の関数か らアクセスできない。 関数の外側でも変数の定義ができる。 外側で定義した変数をグローバル変数と呼ぶ。 グローバル変数にはあらゆる関数からアクセスができるので、関数の情報を受 け渡すのに使用できるが、受渡しがうまく行ってない場合に原因を突き止める のが難しい。

C 言語の関数は値呼び出しである。 つまり関数の引数は関数側にとっては値が代入されているローカル変数となる。 したがって、関数の内部で値を変更しても、呼び出し側の引数の値を変更すること はできない。 そのため、変数の内容を関数で変更させるには、変数の番地を関数に与え、関数 内部ではポインタにより変数にアクセスする必要がある。

番兵

番兵とは複数のデータを扱う際に、データの終りにさらにデータの終りを示す データを与えるものである。 番兵を用いることによりデータの個数を意識しなくても複数のデータを処理す ることができるようになる。 配列変数を関数で処理する場合も、番兵を与えておけばデータの個数を関数に 渡す必要がなくなる。

プログラムの解法

課題1-1

データの個数を 10 個ずつ処理することになるので、数を数える変数を宣言し て 10 個毎に改行を行うようにする。

データの入力はポインタで与えられ、データの終りは -1 という番兵で与えら れるので、一番外側の繰返しは注目している配列の値が -1 でないという条件 で行う。 そして、注目している値を出力した後、数を数え 10 の倍数になっていれば改 行を出力する。

このようにして作成した dispArray 関数を付録に示した。 また、下のテストプログラムと結合したところ、題意を満たしていることがわ かった。


void dispArray(int *p);
int main(void){
  int a[]={1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,-1};
  dispArray(a);
  return 0;
}

課題1-2

square は与えられたポインタに対して、それを番兵付きの配列と見なし、そ れぞれの数に対して二乗して、 n で割った余りを代入する。したがって、ア ルゴリズムは次のようになる。

与えられたポインタに対して、番兵が来たら終了する。 そうでなければ、ポインタの指す値 x に対して、 (x*x)% n を x に代入する ことを繰り返す。

このようにして作成した square 関数を付録に示した。 また、下のテストプログラムとさらに課題 1-1 で作成した dispArray 関数と 結合したところ、題意を満たしていることがわかった。


void dispArray(int *p);
void square(int *p,int n);
main(){
  int a[]={1,2,3,-1};
  int b[]={-1};
  int c[]={1,2,3,4,5,6,7,8,9,10,-1};
  dispArray(a);square(a,4);dispArray(a);
  dispArray(b);square(b,1);dispArray(b);
  dispArray(c);square(c,11);dispArray(c);
}

課題1-3

課題 1-3 ではここまで作成した dispArray() 関数と square()関数を使用し て様々なデータを与えている。 実行例を付録に示す。 ここでは値がない場合と、値が一つの場合、値が 3 つの場合と、値が 64 個 の場合を考え、それぞれ学籍番号の下三桁(著者の場合 999)の数の回数だけ square()関数を実行するものである。

値がない場合
square前も後もなにも表示されない(改行のみ表示)が正しい
値が一つの場合
square前も後も値が一つ表示される
値が 3 つの場合
square 前は 1 2 3 と表示される。 square 後は(秘密)と表示された。
値が 64 個の場合
square 前は 0 から 63 までの数が表示される。 これが 999 回 square された結果は(秘密)となった。

考察

本来二乗して余りを求めるという操作を様々な数に対して行うと、結果が予想 できなくなっても不思議ではないが、実際は規則的なパターンが得られた。 なぜ、このパターンが得られるのか、考えてみた。

まず、64 個もの値を 999 回も繰り返すと分からなくなるので、小さな値で少 ない回数繰り返すよう、次のようなプログラムを作成した。


#include <stdio.h>
void dispArray(int *p);
void square(int *p, int n);
int main(void){
  int *a[5];
  int b[]={1,2,3,4-1};
  int c[]={1,2,3,4,5,-1};
  int d[]={1,2,3,4,5,6,-1};
  int e[]={1,2,3,4,5,6,7,-1}
  int size[]={5,6,7,8}
  int i,j;
  for(i=0;i<N;i++){
    e[i]=i;
  }
  e[N]=-1;
  a[0]=b; a[1]=c; a[2]=d; a[3]=e; a[4]=NULL;
  for(i=0;a[i]!=NULL;i++){
    dispArray(a[i]);
    for(j=0;j<10; j++){
      square(a[i],size[i]-1);
      dispArray(a[i]);
    }
  }
  return 0;
}

このプログラムを実行した所、(秘密) には特別なパターンが得られることが わかった。 つまり、割る数と二乗を繰り返すと言うことに密接な関係があることがわかる。 調べた所、この関係はオイラーの定理により導かれることがわかった。 オイラーの定理とは(以下省略)

まとめ

今回は関数とポインタを使用した関数 dispArray と square を作成し、課題 1-3 で組み合わせて実行した。

考察すべき点であるが、今回は簡単過ぎてなにも考えずに完璧にプログラムで きたので特になにも書く事はない。 また課題自体も完璧であるのでなにも指摘すべきことがない。

オイラーの定理は常識なのでプログラム実行前からどうなるかはわかった。

参考文献

  1. 講義資料 http://edu.net.c.dendai.ac.jp/ad/2/2006/
  2. B.W.カーニハン, D.M.リッチー著, 石田晴久訳「プログラミング言語C」第二版。共立出版(1989)

付録

課題1-1のプログラム

課題1-2のプログラム

課題1-3の実行例

坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科