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

§9.1 配列とポインタ

実は、配列名はその先頭要素のアドレスである。 よって次のプログラムは同じ答えを出す。

#include <stdio.h>

int main() {
  int a[10];

  printf("%p.\n", &a[0]);
  printf("%p.\n", a);
}

§9.2 ポインタへの整数の足し引き

配列の要素 a[i] という書き方はすでに学んだ。 実は、a[i]*(a+i) である。 ここで a+i は、a が 100 番地のとき 100+i 番地、ではない。 もしも a[ ]int 型の配列であり、 一つの int 型が 4 バイトだとしたら、100+4*i 番地、となる。 この「4*」はコンパイラが自動でやってくれるので、考えなくてよい。 よって、*(a+i) は配列 a[ ]i 番目の要素 a[i] を意味する。

これで、次のプログラムの三つ目の for までは理解できるであろう。

#include <stdio.h>

int a[10];

int main() {
  int i, *p;    /* int 型変数 i と int 型へのポインタ p とをひとまとめに宣言できる */

  for (i = 0; i < 10; i++) {             /* a[i] を i の二乗で埋める */
    a[i] = i * i;
  }

  for (i = 0; i < 10; i++) {             /* a[i] を出力。前にもやった */
    printf("%d ", a[i]);
  }
  putchar('\n');

  for (i = 0; i < 10; i++) {             /* 別の書き方でもう一度 */
    printf("%d ", *(a+i));
  }
  putchar('\n');

  for (p = a; p < a + 10; p++) {         /* また別の方法でもう一度 */
    printf("%d ", *p);
  }
  putchar('\n');

}

四つ目の for ループでは、 ポインタ pa[0] のアドレスを入れ、 その p の値を 1 ずつ増やしている。 このときも、アドレスは 1 番地ずつ増えるのではなく、 int 型は 4 バイトなら、4 番地ずつ増えてゆく。

ここで、不思議に思った人がいるかもしれない。 四つ目の for では、pa + 10 まで増えて、 その時点で継続条件を満たさなくなってループを抜ける。 a[10] は存在しないから、p は“ないもの”を指す。 しかし、これは許されるという規則である。 K&R2 の 5.4, A7.7 参照。 ただし、*p を読み書きしようとしてはならない。

なお、反対側の、a の一つ手前は許されない。 逆順に出力するつもりで、次のように書くのは間違いである。

  for (p = a + 9; p >= a; p--) {         /* これは間違い! */
    printf("%d ", *p);
  }

§9.3 文字列

C 言語には、文字列型はない。 文字列は、char 型の配列として扱う。 たとえば char a[128]; と宣言しておいて、 そこに "hello" を収めるとは、 a[0]'h' を、 a[1]'e' を、 a[2]'l' を、 a[3]'l' を、 a[4]'o' を入れ、終わりの印として a[5]'\0' を入れることである。 最後に置く '\0' は、どの文字とも異なる数値である。 その手前までを文字列と呼ぶか、 '\0' も含めて文字列と呼ぶかは、 時と場合によると思う。

プログラム例は次の節であげる。

§9.4 文字列の出力

#include <stdio.h>

int main() {
  char a[6];
  char b[100] = "hello";
  char c[ ] = "hello";

  a[0] = 'h';
  a[1] = 'e';
  a[2] = 'l';
  a[3] = 'l';
  a[4] = 'o';
  a[5] = '\0';

  printf("puts() で三行、出力します.\n");
  puts(a);
  puts(b);
  puts(c);

  printf("print() で「%s」と出力します.\n", a);

  printf("fputs() で出力します.\n");
  fputs(a, stdout);
  putchar('\n');
  printf("これでこのプログラムは終わりです.\n");
}

まずは、最初の、文字列を宣言するところを説明する。 char a[6]; では、6 個の要素からなる char 型を用意する。 char b[100] = "hello"; では、100 個の配列を用意し、 初めのほうから "hello" を入れる。 つまり、前の節で示したようになる。 b[6] 以降は不明である。 char c[ ] = "hello"; では、配列の大きさはコンパイラにまかせて、 そこに "hello" を入れる。配列の大きさは自動的に 5 + 1 = 6 となる。

a[ ] には文字列がはいっていないから、6 行を使って、代入している。

a, b, c の三つとも、 格納されている文字列は同じ "hello" である。

文字列の出力には、三つの関数が使える。

まずは puts()。かっこの中には、文字列の先頭アドレスを書く。 この関数は、指定された文字列を出力したあと、改行も出力する。

次に、前から使っていた printf()。 文字列を出力する際は %s と書く。自動的に改行はしない。

三つ目は、fputs() である。これは二つの引数をとる。 初めのは文字列の指定。あとのは、いまは stdout と書くと覚えよう。 これは標準出力のことで、リダイレクトやパイプを用いていなければ画面のことである。 この関数も、自動的に改行はしない。

puts() 以外は、自分で改行するように書かないと改行しない。

§9.5 文字列の入力(勧めないバージョン)

#include <stdio.h>

#define MAXLINE 128

int main() {
  char line[MAXLINE];

  while (gets(line) != NULL) {      /*【お勧めしません!】*/
    puts(line);
  }
}

puts() と対になる gets() で文字列の入力ができる。 この関数は、キーボードから一行に読み込み、行末の改行は文字列に含めず、 その代わり、そこに '\0' を置く。 入力が終わった場合、またその場合のみ、NULL を返す。 よって、上のプログラムは §6.1 のと同じように動く。 (NULL についてはあとで述べる。)

しかし、この関数は現在では勧められない。 理由は、ユーザーが入力する文字数に制限を設けられないため、 作者の予期せぬところを、入力された文字が“破壊”するかも知れないからである。

次のプログラムは、配列のサイズを 4 にしたもの。 手元の gcc でコンパイルしたところでは、 配列 line[ ] の直後に n がくるため、 ユーザーが 4 文字以上の長い文字列を入力すると変数 n が書き換わってしまう。

#include <stdio.h>

#define MAXLINE 4

int main() {
  int n;
  char line[MAXLINE];

  printf("%p\n", &line[MAXLINE]);
  printf("%p\n", &n);

  n = 123;
  while (gets(line) != NULL) {      /*【お勧めしません!】*/
    puts(line);
  }
  printf("n の値は %d です.\n", n);
}

特に危険なのは、将来、コンピュータの管理者になって、 一般ユーザーに使わせる、管理者権限で動くプログラムを書くときである。

§9.6 文字列の入力(勧められるバージョン)

関数 fgets() は三つの引数をとる。 一つめは入力された文字列の格納先、 二つめは格納先のサイズ('\0' の分も含む)、 三つめはいまのところは stdin である。 これは標準入力を意味する。 ただし、行末の '\n' までを文字列とみて配列に格納する。

fputs() は自動的に改行はしないから、 次のプログラムは §6.1 のと同じように動く。

また、一行の長さが MAXLINE 以上でも正しく動く。 その実験の際には、MAXLINE の値を小さくして実験すると楽である。 fput() の行の前に printf("###"); を挿入してみると、 どうして正しく動くのかわかるであろう。

#include <stdio.h>

#define MAXLINE 128

int main() {
  char line[MAXLINE];

  while (fgets(line, MAXLINE, stdin) != NULL) {
    fputs(line, stdout);
  }
}

ただし、'\n' が常に文字列の最後についてしまうのは扱いにくい。 よって、危険は理解したうえで、gets() を使ってゆこう。

§9.7 文字列関連の関数 strlen(), strcpy()

次のプログラムは、入力行の最大の長さと、 その最大を与える行(の一つ)を出力する。 ただし、入力は空でないと仮定している。 入力リダイレクトを使えば、 ファイルの中で最大の長さをもつ行を知ることができる。

#include <stdio.h>
#include <string.h>

#define MAXLINE 128

int main() {
  int len, maxlen;
  char line[MAXLINE];
  char longest[MAXLINE];

  maxlen = 0;

  while (gets(line) != NULL) {      /*【お勧めしません!】*/
    if ((len = strlen(line)) > maxlen) {
      maxlen = len;
      strcpy(longest, line);
    }
  }
  printf("一番長い行は %d 文字あり、", maxlen);
  printf("その行(の一つ)は「%s」です.\n", longest);
}

関数 strlen() は文字列を引数にとり、その文字列の長さを返す。 ただし、終端を意味する '\0' は数に含めない。 関数 strcpy() は文字列二つを引数にとり、第二のを第一に、 終端を意味する '\0' も込めてコピーする。 これらの関数を使うには string.h#include が必要である。 また、strcpy() では第一の配列がコピー可能なだけの長さをもつと仮定している。 さらに、二つの配列の共通部分が空でない場合の動作は保証されない。

練習:strlen(), strcpy() は元々ある関数だが、 同じように動く関数を自作することが可能である。 mystrlen(), mystrcpy() という名前で書いてみよ。 main() は上のプログラムを利用すればよいだろう。

§9.8 文字列関連の関数 strcmp()

関数 strcmp() は二つの文字列を引数にとり、 辞書式順序でどちらが先かを返す。 負なら一つ目が先、0 なら等しい、正なら二つ目が先、である。 この関数を使うにも string.h#include が必要である。

#include <stdio.h>
#include <string.h>

#define MAXLINE 128

int main() {
  char line1[MAXLINE];
  char line2[MAXLINE];
  int r;

  while (gets(line1) != NULL) {     /*【お勧めしません!】*/
    gets(line2);                    /*【お勧めしません!】*/

    r = strcmp(line1, line2);
    if (r < 0) {
      printf("一つ目のほうが前.\n");
    } else if (r == 0) {
      printf("両者は等しい.\n");
    } else {
      printf("二つ目のほうが前.\n");
    }
  }
}

§9.9 課題2

strcmp() は元々ある関数だが、 同じように動く関数を自作することが可能である。 mystrcmp() という名前で書いてみよ。 返り値は int 型とし、 二つの文字列を最初から比べていって '\0' まで等しければ 0 を返し、 そうでなければ、異なっている最初の文字の差を返せばよい。 次にプログラム例をあげるが、これでは等しいときに 0 を返すとは限らない。 直してみよ。

int mystrcmp(char *p, char *q) {
  int i;

  for (i = 0; p[i] == q[i]; i++) {
    ;
  }
  return p[i] - q[i];
}

main() は §9.8 のプログラムを利用すればよい。

レポートの本文冒頭に、氏名を、なるべく大学に届けるある通りの文字で書け。 次に、プログラムソースファイルを、添付ファイルではなく、本文に貼りつけよ。 それから、このプログラムを使った実験結果を貼れ。

件名は「??? kadai2」(←全て半角文字、小文字、 ??? は自分の履修者番号、その後ろに半角スペース一つ、 kadai2 の間にはスペースを入れない)とせよ。

§9.10 動的メモリ割り当ての関数 malloc()

いままで、変数や配列を使うときは必ず「宣言」をした。 そうではなく、プログラムが動き出してから、必要なメモリを取得することができる。。

次のプログラムは、入力行をすべて保持するものである。

char text[10000][10000]; などとして二重配列を宣言し、 そこに格納する方法もあるが、 常識的には一行は長くても 200 文字ぐらいなので、 これでは効率がよくない。

そのため、別のやり方を考える。 char *p[LINES]; として、 LINES 個の char へのポインタを宣言している。 こう書くとポインタの配列となる、という約束である。

関数 malloc() の引数は必要なメモリのサイズ、 返り値は割り当てられたメモリの先頭アドレスである。 請求するメモリのサイズは入力行の長さ + 1 とした。 この + 1 は、文字列の終わりの印 '\0' のためである。 そしてそこに入力行をコピーする。 関数 malloc() を使うには stdlib.h#include が必要である。

関数 malloc() はメモリがとれない場合は NULL を返す。 NULL は、有効なメモリのアドレスにはならないアドレスである。 だから、厳密には、関数 malloc() を呼んだあとには、 帰ってきた値が NULL でないかのチェックが必要である。 (ほかに、入力行が長すぎた場合、入力行が多すぎる場合のことも考えねばならない。)

また、 「終わりの印」とコメントをつけた箇所では、 NULLp[n] に代入することで、 その手前で入力が終わっていることを示すために使っている。

割り当てられたメモリの先頭アドレスを出力」とコメントをつけたところでは、 割り当てられたメモリの先頭アドレスを出力している。 ここの gcc では、十六進で末尾が 0, すなわち、16 バイト単位で割り当てるようである。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAXLINE 1024
#define LINES 1024

int main() {
  int len, n;
  char line[MAXLINE];
  char *p[LINES];

  for (n = 0; gets(line) != NULL; n++) {    /*【お勧めしません!】*/
    len = strlen(line);
    p[n] = malloc(len + 1);                 /* +1 は '\0' のため */
    strcpy(p[n], line);
  }
  p[n] = NULL;                              /* 終わりの印 */

  for (n = 0; p[n] != NULL; n++) {          /* 割り当てられたメモリの先頭アドレスを出力 */
    printf("%p.\n", p[n]);
  }

  for (n = 0; p[n] != NULL; n++) {          /* 保持された行を出力 */
    puts(p[n]);
  }
}

おまけ:gcc のはいっていない Windows PC で文字コードを知る方法

「沢」の旧字体であるところの「澤」の文字コードが知りたくなったとする。

この文字だけを収めたテキストファイル x.txt を用意する。 このファイルは 2 バイトである。 別に、たとえば半角スペース 2 つだけからなるテキストファイル y.txt を用意する。

そして「fc /b x.txt y.txt」を実行する。 fc はファイル比較 (file compare) のプログラムであるが、 /b をつけるとバイナリファイルと見て比較する。よって

Z:\>fc /b x.txt y.txt
ファイル x.txt と Y.TXT を比較しています
00000000: E0 20
00000001: 56 20

が画面に出て、「澤」のコードは E0, 56 と知れた。 その右の 20, 20 は半角スペース 2 つのコードである。


岩瀬順一