2023 年度「数学特論」 2023-12-15

§8.1 コンピュータの中での数の表現方法、アドレス

ここまでは、ほとんど数学と同じ感覚でプログラムを書いてきた。 ここで、次にポインタ (pointer) を学ぶため、コンピュータのしくみを少しだけ説明する。

コンピュータの世界は 0 と 1 だけからなると言われるが、 その、0 か 1 かの情報を収める単位を 1 ビット (bit) という。 ふだんは 1 ビットずつ扱うことはなく、 何ビットかをまとめた 1 バイト (byte) という単位でデータを扱う。 1 バイトはたいてい 8 ビットである。(例外もあるらしい。)

以下、8 ビットを 1 バイトと仮定する。 1 バイトには、二進法で書いて 0000 0000 から 1111 1111 までの数を収めることができる。 1111 1111 は十進法では 255 である。

いま、8 ビットを 4 ビットずつに分けて書いたのには意味がある。 二進法は表記が長くなりすぎて使いづらい。 4 ビットをまとめると 24 = 16 だから十六進法となる。 十六進法では、0 から 9 までの数字に加えて、十から十五までを表す数字が必要である。 それには、a から f までのアルファベットを使う。

二進十六進十進
000000
000111
001022
001133
010044
010155
011066
011177
二進十六進十進
100088
100199
1010a10
1011b11
1100c12
1101d13
1110e14
1111f15

この表を見ると、たとえば二進の 1011 0110 は十六進では b6, 十進に直すと 11 * 16 + 6 = 182 とわかる。 このあたりの換算は、Windows の電卓を「プログラマー」モードで使うと簡単である。

コンピュータの中には、1 バイトの情報を収めることのできる「メモリ (memory)」 が並んでいる。 それらには、0, 1, 2, ... と、番号がついている。その番号を「アドレス (address)」 という。 「番地」と思うとわかりやすいかもしれない。

1 バイトでは、0 から 255 までの数しか収められないから、実地の計算の役に立たない。 そこで、何バイトかをまとめて、そこに整数を格納するしくみになっている。 現在の多くのパソコンでは 4 バイトを使って整数を格納する。

いま使っている int 型もそうで、mod 232 = 4294967296 で計算をしている。 代入できる値は -231 = -2147483648 から 231 - 1 = 2147483647 までである。

§8.2 変数のアドレスを出力するプログラム

変数名の前に演算子「&」をつけると、 その変数の値ではなく、その変数の収められたアドレスの意味になる。

printf() でアドレスを出力するときは、%p と書く。 この gcc では、十六進で表記される。

演算子 sizeof のうしろに小かっこに入れて型の名を書くと、 その型が何バイトを占めるか、になる。

#include <stdio.h>

int main() {
  int m, n, i;
  double x, y;
  int a[10];

  printf("int 型は %d バイトです.\n", sizeof(int));
  printf("m のアドレスは %p です.\n", &m);
  printf("n のアドレスは %p です.\n", &n);
  putchar('\n');

  printf("double 型は %d バイトです.\n", sizeof(double));
  printf("x のアドレスは %p です.\n", &x);
  printf("y のアドレスは %p です.\n", &y);
  putchar('\n');

  for (i = 0; i < 10; i++) {
    printf("a[%d] のアドレスは %p です.\n", i, &a[i]);
  }
}

アドレスがわかるとどんな使い道があるか、はこのあと述べる。

§8.3 ポインタ

「ポインタ (pointer)」とは、いままで出てきた変数とは異なり、 変数のアドレスを格納する変数である。

ポインタには、どの型の変数のアドレスを格納するかによって、 int 型へのポインタ、double 型へのポインタ、などの区別がある。

変数 pint 型へのポインタであることの宣言は、 int *p; としておこなう。 int 型へのポインタを二つ以上宣言するときには int *p, *q; のようにする。 ここでは、int 型変数 x, y に加えて、 int 型へのポインタ p, q をも宣言している。

#include <stdio.h>

int main() {
  int x, y;
  int *p, *q;

  p = &x;
  q = &y;

  printf("x のアドレスは %p です.\n", &x);
  printf("y のアドレスは %p です.\n", &y);

  printf("x のアドレスは %p です.\n", p);
  printf("y のアドレスは %p です.\n", q);
}

p = &x;」の & は、次にくる変数のアドレスを示す演算子であった。 よって、この文が実行されると、変数 x に格納されている値ではなく、 x のアドレスが p に格納される。 慣れるまでは「x の番地が p にはいった」と思うとわかりやすい。 次の「q = &y;」も同様である。

関数 printf() でアドレスを出力させるには %p と書くのだった。 p&x とは同じだから、 三つめ・四つめの printf() 文もわかるであろう。

§8.4 アドレスとポインタ

アドレスやポインタが何の役に立つのか、の説明をまだしていないが、 とりあえず、次のプログラムを見よ。これも役に立たないプログラムである。

#include <stdio.h>

int main() {
  int x, y;
  int *p, *q;

  x = 10; y = 20;
  printf("(1) x = %d, y = %d.\n", x, y);

  p = &x; q = &y;
  *p = 30; *q = *p;
  printf("(2) x = %d, y = %d.\n", x, y);

  q = p;
  *q = 100;
  printf("(3) x = %d, y = %d.\n", x, y);
}

*p = 30;」が新しい。 この「*」は、ポインタの前につく演算子で、 「*p」は「ポインタ p に格納されているアドレスに割り当てられている変数」 を意味する。 「ポインタ p がさす変数」と言うこともある。 ここでは、p には x のアドレスがはいっているので、 x に 30 が代入される。 次の「*q = *p」では、 yx の値がコピーされることがわかるだろうか?

その先の「q = p;」について。 ポインタは変数であるから、このように代入が可能である。 これで qx をさすようになったから、 次の「*q = 100;」では x に 100 が代入される。

※ ここで説明した「*」と、 ポインタの宣言のときの「int *p;」の「*」との間に関連をつけることもできるそうだが、 別々に理解しておけばよいだろう。

※ 「&」と「*」とは互いに逆の演算子である。 よって次のように書けるが、こう書くことはまずない。

#include <stdio.h>

int main() {
  int x;
  int *p;

  x = 10;
  printf("x は %d, %d.\n", x, *&x);

  p = &x;
  printf("p は %p, %p.\n", p, &*p);
}

※ 「100 番地には何がはいっているだろう?」と思って、 「p = 100; printf("%d\n", *p);」などと書いてはいけない。 ポインタに代入できるのは、とりあえずは、実在する変数のアドレスのみ、 と思っておくのがよい。

§8.5 ポインタを使った、意味のあるプログラム

関数の自作について不安のある者は 211022.html で復習せよ。

ツルカメ算を解く関数を書いてみよう。 ツルの数とカメの数の二つを返す関数は書けないので、次のように書く。

※ 「ツ」を cu とするのは新日本式ローマ字である。「チチャチュチョ」は ci cya cyu cyo とする。

関数 void curukame(int c1, int c2, int *p, int *q) は、 ツルとカメが合わせて c1 匹、 足の数の合計が c2 本のとき、 ツルの数を *p に、カメの数を *q に入れるものである。

呼び出すときは、curukame(a, b, &x, &y); のようにする。 「合わせて a 匹、 足の数が b 本のとき、 ツルの数を &x “番地”に、 カメの数を &y “番地”に入れてくれ」というのである。 仮に、の話だが、もしも x が 100 番地、y が 104 番地なら、 「100 番地と 104 番地に答えを入れてくれ」と呼び出す。

すると、関数 curukame() の側では、 「100 番地と 104 番地に答えを入れればいいのだな」とわかる仕組みである。

#include <stdio.h>

void curukame(int c1, int c2, int *p, int *q);

int main() {
  int a, b, x, y;

  a = 5; b = 16;

  printf("ツルカメ合わせて %d 匹、足は %d 本のとき、", a, b);
  curukame(a, b, &x, &y);
  printf("ツルは %d 匹、カメは %d 匹です.\n", x, y);
}

/* 全部で c1 匹、足が c2 本のとき、ツルの数を *p に、カメの数を *q に入れる */

void curukame(int c1, int c2, int *p, int *q) {
  *p = (4 * c1 - c2) / 2;
  *q = (c2 - 2 * c1) / 2;
}

もう一つの書き方は、「大域変数」を使う方法である。 大域変数とは、関数の外で宣言される変数で、 プログラムのどこからでもアクセスできる。

#include <stdio.h>

void curukame(int c1, int c2);

int curu, kame;     /* 大域変数 */

int main() {
  int a, b;

  a = 5; b = 16;

  printf("ツルカメ合わせて %d 匹、足は %d 本のとき、", a, b);
  curukame(a, b);
  printf("ツルは %d 匹、カメは %d 匹です.\n", curu, kame);
}

/* 全部で c1 匹、足が c2 本のとき、ツルの数を curu に、カメの数を kame に入れる */

void curukame(int c1, int c2) {
  curu = (4 * c1 - c2) / 2;
  kame = (c2 - 2 * c1) / 2;
}

どちらがよいかは、一概には言えない。

また、この程度の計算なら、独立した関数にするまでもない。これはあくまでも見本である。

§8.6 練習問題

int 型変数 x, y の値を入れかえる関数は、 void swap(int x, int y) としては書けない。 void swap(int *p, int *q) として書いて、 呼び出す際に swap(&x, &y) とする。書いてみよ。 (もちろん、main() も書くのである。)


岩瀬順一