プログラミング 09 - Soft Forum P.C. Club

恐怖の配列, ポインター

概要

intなどの変数を取ってきましたが, 同じようなのがあったときに別々に変数を作るのはどうかという場合がありますね. 例えば, aからzのアルファベットを並べてどれが何文字出てきたかを数えるときに,

int a_count = 0, b_count = 0, c_count = 0, d_count = 0 ...

ぞっとしますね.

intなど同じ型の変数を連続して並べ, 番号付けしたものを配列といいます.

上のものが,

int counts[26] = {0};
counts[0]++;

という風に番号付けされて並べられるわけです. きれいそうですね. 早速見ていきます.

配列

配列は同じ型の領域を並べて, それに番号を振ったものです.

int counts[26];

は, intが26こ並べられている配列で, それぞれ, counts[0], counts[1]という風にしてcounts[25]間で入っており, それにアクセスすることができます.

では, せっかくなので, アルファベットの数を数えてみましょう.

こういうのによく使う英文として, 不思議の国のアリスを用意しました.

command line で

wget "http://www.soft-forum.net/2010/programming/alice.txt"

と打ってみると, downloadできます.

これの中に現れるアルファベットのそれぞれの数を数えます.

これはこのようにできます.

#include <stdio.h>

int main(int argc, const char *argv[]) {
  int ch, i;
  int counts[26] = {0};
  while ((ch = getchar()) != EOF) {
    if (('A' <= ch && ch <= 'Z') || ('a' <= ch && ch <= 'z')) {
      // 小文字に
      if ('A' <= ch && ch <= 'Z') {
        ch += ('a' - 'A');
      }
      // aからはじまって何番目かの数字にする.
      // a なら 0, b なら 1, z なら 25
      ch -= 'a';
      // countsのch番目の数字を増やす.
      ++counts[ch];
    }
  }

  // すべて見ていく
  for (i = 0; i < 26; ++i) {
    printf("%c : %d\n", 'a'+i, counts[i]);
  }
  return 0;
}
./a.out < alice.txt

とするとどのアルファベットがどれだけ出てきたかが見れますね. 一見してeが超多いのとかが目立ちます. 英文の特徴を取れているといえますね. E A Tが多く, Zが極端に少ないのが英文の特徴で, こんなのを利用した例の古典があります.

踊る人形 - Wikipedia


プログラムの解説

解説します.

int counts[26] = {0};

で配列を定義しています. []は配列であるということで, 26は配列の大きさです.

また, 右のものは配列の初期値を決めています.

今迄からそうだったように,

int i;

のiの数字は未定義で, i = 0のように入れなければなりませんでした. 配列も同様に未定義です.

例えば, 大きさ4の配列の初期化は,

int count[4] = {1, 2, 3, 4};

とすると, count[0]には1, count[1]には2という風に初期化されます.

int count[4] = {0};

とすると, そのすべての初期値が0になります.

for (i = 0; i < 26; ++i) {
  count[i] = 0;
}

としてすべて0にする手もありますが, 上のものはC言語仕様で決まっていて, loopまわすよりよっぽど高速なので, このようなloopをまわすよりもこちらをお勧めします.


そして, count[ch]で[]のなかに数字を入れてアクセスしています. このようなアクセスを添え字アクセスといいます.

また, 注意してほしいのは, 配列の添え字は0から始まります. 要素26の配列では添え字でアクセスできるのは0から25までです. そこで, for文のあの書き方が猛威を振るいます.

int i, n = 26;
for (i = 0; i < n; ++i) {
  count[i];
}

これで0から25までアクセスできていますね. このようにfor文で配列を回る書き方は, ものすごい頻度で出てくるので, 覚えておいたほうがいいです.

また, すごく大切なことなのですが, int型とint配列型はそれはもうまったくの別物です. そしてこれから現れるintポインター型とも別のものです. 混同しないようにしてください

ポインター

関数の値はすべてcopyされます. これを値渡しといったりします. C言語においてはこのことに例外はないので(言語的に小さい, 仕様がprimitive) すべての値は値渡しされます.

2つの値をとってswap(交換)する関数を書きます.

#include <stdio.h>
void swap(int, int);

int main(int argc, const char *argv[]) {
  int a, b;
  a = 10;
  b = 20;
  printf("before swap:\na = %d\nb = %d\n", a, b);
  swap(a, b);
  printf("after swap:\na = %d\nb = %d\n", a, b);
  return 0;
}

void swap(int a, int b) {
  int t = a;
  a = b;
  b = t;
}

今までの知識で書くとこんな感じでしょうか? 実行するとこうなります.

before swap:
a = 10
b = 20
after swap:
a = 10
b = 20

swapされていませんね.


C言語においてはすべてコピーされます. つまりswapに渡されたa, bはコピーされてswap関数に渡されます. なのでいくらswap関数の中でコピーされた後の値をいじったとしても, コピー元には当然影響がありません.


ちょっとたとえ話

例えば, 僕が家を掃除してほしいと頼んだとします. するとまず, 私は家をコピーするわけです. そしてコピーした家を掃除してもらいます. 変数の寿命があるので, 掃除が終わるとコピーされた家は消えて, コピー元の家だけが残ります. コピー元の家は当然掃除されていません. これでは何の意味もありません.

ではどうすればいいのでしょうか? 解決策の一つとして返り値でもらうというのがあります. これは今までやってきたことで, コピーされて掃除されたきれいな家で, コピー元の汚い家を上書きするのです. すると家が掃除されます.

これには2つ問題があります.

  1. 掃除するたびに家をコピーとか, costがもったいない
  2. 複数の家を渡してどれもきれいにしてもらうということができない

これを解決するのはどうするのでしょうか. それがポインタという概念です.

まず私は紙に家の住所を書きそこまでいって掃除してきてくれるように頼みます. 掃除係の人は, まずその住所の書いてある紙をコピーし(値渡し), その住所まで言って掃除します. 変数の寿命で消えるのは紙だけです. 後で家に帰ってみれば, 当然家は掃除されているわけです.


長々とたとえ話をしましたが, この住所に当たるものがポインターです. C言語的には「メモリ領域のアドレス」なので, まさしく住所なわけです.

ではswapしてみましょう.

#include <stdio.h>
void swap(int*, int*);

int main(int argc, const char *argv[]) {
  int a, b;
  a = 10;
  b = 20;
  printf("before swap:\na = %d\nb = %d\n", a, b);
  swap(&a, &b);
  printf("after swap:\na = %d\nb = %d\n", a, b);
  return 0;
}

void swap(int* a, int* b) {
  int t = *a;
  *a = *b;
  *b = t;
}
before swap:
a = 10
b = 20
after swap:
a = 20
b = 10

交換されていますね.

具体的な文法の説明をします.

swap(&a, &b);

&をつけると, その変数のメモリのアドレス, つまりポインターが取得できます.

アドレス演算子といいます.

ためしにアドレスを見てみましょう.

#include <stdio.h>

int main(int argc, const char *argv[]) {
  int a, b;
  a = 10;
  printf("a = %d\naddres = %p\n", a, &a);
  return 0;
}

addressは%pで取ります. 16進の数値が出てきましたか? これがそのメモリの領域の番地です.

また, ポインターをとる関数のプロトタイプ宣言するのは以下のように書かれています.

void swap(int*, int*);

これはint型ポインター2つを取って何も返さない関数swapのプロトタイプ宣言です.

ポインターは型ごとに別にあって,

int i = 20;      // int型
int *p1 = &i;    // int型pointer
char ch = 'a';   // char型
char *p2 = &ch;  // char型pointer

となっています.

ちなみに,

char* p;
char * p;
char *p;

どれも大丈夫です. char型pointerとして解釈されます. アスタリスクの位置は時に論争の種になります.

また, ポインターから値を取り出す方法があります.

void swap(int* a, int* b) {
  int t = *a;
  *a = *b;
  *b = t;
}
int t = *a;

のようにpointerにアスタリスクをつけると, 中の値を参照することができます. 代入も可能で,

*a = *b;

とすれば, pointer aのさしているメモリ領域にpointer bのさしているメモリ領域の中身を入れるということになります. (ちなみにこれもコピーですね)

このようにポインターを使えば, 遠く離れたメモリの中身を書き換えるということができるのです.


ちなみに, K&Rではポインタの宣言について以下のように解説されています.

int *ip;

は*ipという表現がintであることを表す.


scanf

scanfの引数の意味がこれでわかります.

int a, b;
scanf("%d %d", &a, &b);

としてscanfに渡していた&a, &bはポインターだったのです.

int ch;
ch = getchar();

のように戻り値としてもらえば別ですが, ポインターじゃなければscanf結果でa, bの中身が変わるのはおかしいはずです. しかしなぜ変わるのかというと, ポインターを渡して, 住所を見て, メモリ領域を書き換えに行っていたからなのです.


補足

次は多分文字列とはなんだったのか, array-to-pointer conversionとは何か, pointer演算とは何かとかいう話をすると思います.

Soft Forum P.C. Club