Another introduction to programming language C 09: ポインタと配列3

C言語では配列はint単体やdouble単体とは決定的に違う.これを,プログラマは「C言語の配列は第一級オブジェクトではない」と言う.配列とは,連続的に割り当てられたメモリのことで,C言語には配列をひとつの型として扱う機能を与えられていない.(唯一の例外は配列を構造体でくるむ方法だ.)

例えば int a = 0, b = 10; としたときに a = b; とするとbの中身がaにコピーされる.ところが,以下の例では配列aの内容はbにコピーされない.

int *a = calloc(100, sizeof(int));
int *b;
...
b = a; /* aの内容はbにコピーされない */

配列を配列にコピーするには,そのために設計された関数を使うか,内容を1個1個コピーせざるを得ない.幸いなことに,まだこのレベルでは,配列をまるごとコピーしなければいけないようなことはそうそう起こるものでもない.

関数には引数(ひきすう)を与えられる.引数は好きな数だけ与えることが出来るが,関数の戻り値(関数の値)は1個だけである.

引数はいつもコピーされる.これはどういうことかと言えば,

#include <stdio.h>
void do_something(int a) {
  a = 10;
}

int main(int argc, const char *argv[]) {
  int b = 0;
  do_something(b);
  printf("%d\n", b); /* 0が印字される */
  return 0;
}

というコードで,関数do_somethingは変数bの値を変えないということである.不便であるが,これはC言語の制約である.(C++ではこの制約は緩和される.)

引数に与えられた変数の値を変更したいときには,変数のアドレスを渡してやる.ポインタはコピーされるが,ポインタが指している内容はオリジナルだからである.先ほどのコードは次のように書き換えれば,意図したとおりに動く.

void do_something(int *a) {
  *a = 10;
}

int main(int argc, const char *argv[]) {
  int b = 0;
  do_something(&b);
  printf("%d\n", b); /* 10が印字される */
  return 0;
}

関数は1個しか値を返さないから,複数の値を返したいときにもポインタ引数を使う.例えば2次方程式の解を返す関数を書いてみる.(2次方程式の解が本当に欲しいときには,この関数はあまり役に立たない.これはデモンストレーションである.)2次方程式の係数を次のように定める.

ax2+bx+c=0

関数find_roots(a, b, c, x0, x1)は,係数 a, b, c を受け取り,実数解があればそれらを x0, x1 に入れる.この関数は実数解があった場合は1を,無かった場合は0を返す.

int find_roots(double a, double b, double c, double *x0, double *x1) {
  double d = b * b - 4 * a * c;
  if (d >= 0) {
    double sqrt_d = sqrt(d);
    *x0 = (-b + sqrt_d) / (2 * a);
    *x1 = (-b - sqrt_d) / (2 * a);
    return 1;
  }
  else {
    return 0;
  }
}

使い方は以下の通り.

double x0, x1;
int relust;
result = find_roots(a, b, c, &x0, &x1);
if (result == 1) {
  printf("x = %f and %f\n", x0, x1);
}
else {
  printf("no real solutions.\n");
}

次の関数は,引数の中身を入れ替える.

void swap(int *x, int *y) {
  int z;
  z = *y;
  *y = *x;
  *x = z
}

さて,関数に配列を渡したい場合はどうすればよいだろうか.関数引数はいつもコピーされるから,関数引数に配列を渡せば,配列がコピーされるだろうか.答えは否である.関数に配列を渡すときは,いつでもポインタで渡すことになる.こんな具合である.

void print_all(int *a, int n) {
  int i;
  for (i = 0; i < n; i += 1) {
    printf("%d\n", a[i]);
  }
}

int main(int argc, char *argv[]) {
  int a[10] = { 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 };
  print_all(a, 10);
}

配列とポインタは同じことであるから,print_allの1行目はこうも書ける.

void print_all(int a[], int n) {
  ...

a[]のかわりにa[10]としてもよいが10は単に無視される.int a[n] と書きたくなるが,この書き方は許されていない.  

普通の変数(intとかdoubleとか)の場合はいつもコピーが渡されるので,その変数を書き換えるには参照(ポインタ)を渡すという特別な努力が必要であった.一方,配列の場合は最初からポインタを渡さないといけないので,その配列が引き渡し先の関数によって書き換えられないことは保証されないことになる.もし関数が引数として受け取った配列を書き換えないことを保証したいのなら,C++言語から逆輸入した記法がある.print_allの1行目を次のようにする.

void print_all(const int *a, int n) {
  ...

これで,一応は配列aは書き換えられないことを保証できる.(一応は,と言ったのは,無理矢理書き換える手段が残されているからである.)

Comments