Previous Top Next

第 05 回 配列とポインタ

数列のような、同じ型の変数の列を C 言語では配列という。 例えば文字列は文字型のデータ (char) の配列である。 行列のように二つ以上の添字をもつものも考えられて二次元配列、三次元配列などという。 ここでは配列に関する基本的なことを説明した後、 配列と関わりの深いポインタについて解説する。


配列


配列の宣言は

int a[10];
のように行う。 これによって int 型の変数 10 個が用意され、 それぞれ a[0], a[1],..., a[9] という名前で扱うことができる。 添字が 0 から始まっていることに注意しておく。 カッコの中には別の変数を入れることが出きるので、 これらを適当なループなどで扱うことができる。

一般の変数や配列は宣言と同時に初期化することもできる。

int n = 1;
int a[] = {1,3,5,7};
この場合、配列の長さは必要な長さが自動的にとられ、その指定を省略できる。 典型的なのが文字列の場合で、
char name[] = "abc";
とすると name[] = {'a', 'b', 'c', '\0'} と指定したことと同じになる。 C 言語では文字列は "..." (ダブルクォーテーション) でくくり、 一つの文字は '.' (シングルクォーテーション) でくくる。 '\0' は文字列の最後を表す特別な記号である。

二次元以上の配列も同様で

int a[2][2];
のように宣言する。
int a[2][2] = {{1,2},{3,4}};
などとすれば宣言と同時に初期化もできる。 ただし int a[][2] = {{1,2},{3,4}} は認められるが int a[2][] = {{1,2},{3,4}} は認められない。

課題 05.01
適当な配列を用いて、値の代入や printf による表示などを試せ。

課題 05.02
整数の配列を作り、その値を小さい順に並べ替えるプログラムを書け。 (例えば、バブルソートと呼ばれる以下のような方法がある。 n 個の値があるとする。 まず 1 番目の値と 2 番目の値を比べ 1 番目が大きければ値を入れ替える 次に 2 番目と 3 番目、3 番目と 4 番目、とこれを繰り返す。 最後まで行くと、最大の数が n 番目にあるはずである。 同様に次は n-1 番目まで行う。 これを繰り返せば数列は小さい順に並ぶ。)

簡単な例を見てみよう。 次のプログラムは入力された二つの 2 次正方行列に対して、 その積を計算するものである。

#include <stdio.h>

int main(int argc, char *argv[])
{
  int i, j, k;
  int a[2][2], b[2][2], c[2][2];
  
  /* input a matrix a */
  printf("Input ");
  for (i = 0; i < 2; i++)
    for (j = 0; j < 2; j++)
      printf("a[%d][%d] ", i, j);
  printf(": ");
  for (i = 0; i < 2; i++)
    for (j = 0; j < 2; j++)
    scanf("%d", &a[i][j]);

  /* input a matrix b */
  printf("Input ");
  for (i = 0; i < 2; i++)
    for (j = 0; j < 2; j++)
      printf("b[%d][%d] ", i, j);
  printf(": ");
  for (i = 0; i < 2; i++)
    for (j = 0; j < 2; j++)
    scanf("%d", &b[i][j]);

  /* multiplication */
  for (i = 0; i < 2; i++)
    for (j = 0; j < 2; j++) {
      c[i][j] = 0;
      for (k = 0; k < 2; k++)
	c[i][j] += a[i][k] * b[k][j];
    }

  /* output ab */
  printf("ab = ");
  for (i = 0; i < 2; i++)
    for (j = 0; j < 2; j++)
      printf("%d ", c[i][j]);
  printf("\n");
  
  return 0;
}
これを n 次の行列の積を計算するものに書き換えることを考える。 プログラム中には行列のサイズを表す 2 が多く現れる。 これをすべて、例えば 3 に置き換えれば 3 次の行列を扱うプログラムになる。 しかし複雑なプログラムでは 2 が行列のサイズ以外の意味で使われることもあるだろう。 この場合には行列のサイズを表すものだけを置き換えることになり、 やや面倒な作業となる。

そこで、行列のサイズを表すものを適当な変数にしておくという方法が考えられる。 この場合にはその変数の値を変えればすべての値が変わる。 これは良い方法なのであるが、上のプログラムの場合には利用できない。 配列のサイズは定数でなくてはならず、 コンパイル時に確定していなければならないからである。 そこで用いられるのがマクロである。 プログラム中に、例えば

#define N 3
と書いておくと、コンパイル前に自動的にプリプロセッサマクロ置換を行い、プログラム中のすべての N を 3 に置き換える。 したがって、行列のサイズを変えたいときには、この N の値を書き換えて再コンパイルすればよい。

課題 05.03
上のプログラムのすべての 2 を N に書き換え、更に #include <stdio.h> の後に #define N 3 を加え、実行せよ。

上の方法では行列のサイズはコンパイル時に指定されていなければならず、 実行時に指定することはできない。 これを解決するためには以下のような方法がある。

  1. 配列の宣言以外の部分は変数にできるので、宣言時には十分大きいサイズを確保しておき、 それ以下の範囲の n で実行できるようにする。 この場合、必要以上の配列を確保するので、メモリに無駄ができる。 また扱える n の範囲に上限がある。
  2. malloc などでメモリを動的に確保する。

上記 2 の方法が最も推奨される方法であり、後で説明される。

課題 05.04
上の 1 の方法でプログラムを書き換え、行列のサイズを実行時に指定できるようにせよ。

マクロの最も簡単な使い方は上のように定数を定義することであるが、 引数付きのマクロなどより便利な使い方もある。

課題 05.05
エラトステネスのふるいの方法で 100 以下のすべての素数を求めるプログラムを書け。 ここで 100 という上限はマクロとして定義しておき、後で簡単に変更できるようにせよ。


ポインタ


C 言語の変数は、それが宣言されるとその型に応じた大きさのメモリが確保される。 メモリはプログラムの作業場のようなもので、その中にその変数のための場所が確保されるのである。 この変数に対して、値を代入したり、値を参照するためには、その場所が分からなくてはならない。 このために変数には場所を示すアドレスが与えられる。 変数のアドレスのための変数をポインタ変数、 または単にポインタと呼ぶ。

ポインタ変数の宣言は

int *x;
のように型のあとに * をつける。 また変数 a のアドレスは &a のように & をつけることによって表す。 ポインタ変数 x に対して *x で、アドレス x にある変数の値を参照できる。 例えば x = &a とすると *x = a である。

課題 05.06
次のプログラムを実行し、ポインタに関する理解を深めよ。 (%u は符号なし整数の書式である。)

#include <stdio.h>

int main(int argc, char *argv[])
{
  int a;
  int *x;
  
  x = &a;
  a = 1;
  printf("a  = %d\n", a);
  printf("&a = %u\n", (int)&a);
  printf("x  = %u\n", (int)x);
  printf("*x = %d\n", *x);

  return 0;
}


配列とポインタ


配列を

int a[10];
と定義したとする。 このとき、実際には a の型は整数のポインタ (int *) である。 *a でその値を参照することもできる。 例を見よう。
#include <stdio.h>

int main(int argc, char *argv[])
{
  int a[] = {5,6,3};

  printf("%d\n", *a);
  printf("%d\n", *(a+1));
  printf("%u\n", (int)a);
  printf("%u\n", (int)(a+1));

  return 0;
}
を実行すると出力は
5
6
3216102260
3216102264
のようになる。ただしアドレスは毎回異なる。 *a で a[0] を参照し *(a+1) で a[1] を参照出来ることが分かる。 a+1 の値は a の値より 4 大きい (環境によって 4 ではないこともある)。 これは int 型の変数が 4 バイトを使うためである。 ポインタ変数 a に対して a+1 はその示す型によって異なるものとなる。

多次元の配列のポインタは分かりにくい。


関数とポインタ


これまでの説明でポインタとは何であるかはおおよそ分かったかと思う。 ここではポインタの利用法の一つである関数との関係について解説する。 前に説明したように C 言語の関数は値渡しであるから、 関数内でその変数を書き換えても元の変数は変わらない。 しかし、元の変数の値を書き換えたい場合もあるであろう。 これはポインタを使って実現される。

関数を呼び出すときに引数がどのように扱われているかを考える。 func(a) で関数を呼び出すとき、変数 a のコピーが作られて、それが func に渡される。 したがってコピーを書き換えても a は変わらない。 そこで a のアドレスを関数 func2 に渡すことにしてみる。 関数 func2 は変数 a のアドレスを知っているので、そこにある値を書き換えることが出来るのである。

具体例を見てみよう。

#include <stdio.h>

void plusone(int a)
{
  a++;
}

void plusone2(int *a)
{
  (*a)++;
}

int main(int argc, char *argv[])
{
  int a = 2;

  printf("%d\n", a);
  plusone(a);
  printf("%d\n", a);
  plusone2(&a);
  printf("%d\n", a);

  return 0;
}
出力は
2
2
3
である。 この例では関数 plusone では元の a は書き換わらず、 ポインタを渡した plusone2 では書き換わっている。

C 言語では関数の戻り値は一つの値であるが、ポインタを使うと実質二つ以上の値を返させることができる。 すなわち、戻り値を代入したい変数を複数個用意してそのアドレスを渡せば、 そこに値を代入することによって必要な個数の値を得ることが出来るのである。 実はこの方法はすでに多く用いられている。 関数 scanf は入力された値を変数に代入するために用いるので、 変数名のままではなく &a のようにアドレスを渡していたのである。 しかも複数の値を得ることが出来るということもすでに多くの例で見てきた。

課題 05.07
二つの整数 a, b の値を入れ替える関数 swap を書け。

行列の積を計算する関数を書いてみよう。 簡単のため 2 次正方行列の積を計算するものとする。 行列 a と b の積を c に代入することにする。

#include <stdio.h>

#define N 2

void matmult(int a[N][N], int b[N][N], int c[N][N])
{
  int i, j, k;

  for (i = 0; i < N; i++) 
    for (j = 0; j < N; j++) {
      c[i][j] = 0;
      for (k = 0; k < N; k++)  
	c[i][j] += a[i][k] * b[k][j];
    }
}

void printmat(int a[N][N])
{
  int i, j;

  for (i = 0; i < N; i++) {
    for (j = 0; j < N; j++)
      printf("%4d ", a[i][j]);
    printf("\n");
  }
}

int main(int argc, char *argv[])
{
  int a[2][2] = {{1,2},{3,4}};
  int b[2][2] = {{1,-1},{-1,1}};
  int c[2][2];

  matmult(a, b, c);
  printmat(c);

  return 0;
}
行列を表示するための関数 printmat も用意した。 この例のように関数の戻り値を void としている場合 return を省略することも出来る。 これまでに説明したことだけでは行列のサイズ N を動的に書くことはできない。

課題 05.08
実数配列をベクトルと見て、その和を求めるプログラムを書け。 ただしベクトルの長さも引数として渡し、動的に変更できるようにせよ。


関数へのポインタ


関数の引数に関数を渡したいときがある。 例えば区分求積法を行う関数を書いてみよう。

#include <stdio.h>

float qbp(float a, float b, int n, float (*p)(float))
{
  float step, sum, x;
  int i;

  step = (b - a) / n;
  sum = 0.0;

  for (i = 0; i < n; i++) {
    x = a + step * i;
    sum += (*p)(x);
  }

  return sum * step;
}

float func(float x)
{
  return 2 * x;
}

int main(int argc, char *argv[])
{
  printf("%f\n", qbp(0, 1, 1000, &func));

  return 0;
}
qbp が区分求積法を行う関数である。 はじめの二つの引数は区間の始めと終わりを表し、 次の引数はいくつの区間に分けるかを表す。 最後の引数は関数へのポインタで、 その関数が float を引数とし float を戻り値とすることを意味している。 関数 qbp 内では (*p) を関数名として利用出来る。 qbp を呼び出すには、利用する関数 func をそのアドレス &func で渡せばよい。

関数を引数として渡すことが出来る便利さが理解できるであろう。

課題 05.09
上の qbp では各区間の一番小さい値を用いている。 各区間の中点の値を用いるように書き換え、 それを利用して円周率の近似値を求めよ。


main 関数の引数


これまで、特に説明せずに main 関数の引数を int main(int argc, char *argv[]) と書いてきた。これについて説明する。

unix をはじめとして、多くの OS でコマンドラインからプログラムを実行する場合には、 オプションや処理対象となるファイル名を同時に与える。 例えば C コンパイラ gcc は

home% gcc -lm foo.c
などのように実行される。 このときのオプションなどを処理するために main 関数の引数はある。

main 関数の引数は main(int argc, char *argv[]) で、 argc はプログラム実行時に与えられたコマンドライン引数の個数を表す。 argv は文字列へのポインタで、文字列の列である。 ここに空白を区切りとしてコマンドライン引数が入れられる。 これを確かめる簡単な例を見てみよう。

#include <stdio.h>

int main(int argc, char *argv[])
{
  int i;

  for (i = 0; i < argc; i++)
    printf("argv[%i] = %s\n", i, argv[i]);

  return 0;
}
これを実行すると
home% ./a.out shinshu matsumoto
argv[0] = ./a.out
argv[1] = shinshu
argv[2] = matsumoto
となる。 argv[0] は特別で、コマンド名を表す。 ファイル名 a.out を適当な名前に変えて実行するとこの部分は新しい名前に変わる。 もう一つ簡単な例を見てみよう。
#include <stdio.h>

int main(int argc, char *argv[])
{
  int a, b;
  
  if (argc < 3) {
    printf("usage : %s <integer> <integer>\n", argv[0]);
    return 1;
  }

  sscanf(argv[1], "%d", &a);
  sscanf(argv[2], "%d", &b);
  printf("%d + %d = %d\n", a, b, a+b);

  return 0;
}
このプログラムでは、二つの整数をコマンドライン引数として受け取り、 その和を表示する。 はじめに引数の個数を見て、それが期待している数よりも小さければ メッセージを表示して終了する。 この際、return 1 としている。 これまでは特に説明せずに main 関数の戻り値を 0 としてきた。 数学的な問題を扱うプログラムで main の戻り値が問題になるようなことは多くないが、 大雑把に言えば、正常終了するときは 0 を返し、 そうでないときに 0 以外の数を返す、と思っておけば十分であろう。 さて、このプログラムにはもう一つ説明の必要がある部分がある。 引数として受け取った整数は、受け取った時には文字列として扱われていて、 このままでは整数としての計算はできない。 そこで組み込み関数 sscanf を利用している。 sscanf は名前の通り scanf と類似のものであるが、 標準入力からではなく文字列から入力を受け取ったものとして動作する。 この場合は argv[1] を入力として scanf を実行していることになる。


Previous Top Next