Hatena::Diary 2005-10-17 φ(-_-) ■[lang]プログラミング言語 C 基本にもどって、ちょっと復習 ... C においては、ポインタと配列との間に強い関係がある。この関係が強いので、ポ インタと配列とは同時に論じなければならない。 a[i] という記法は、(ブロックの) 先頭から i番目の位置の配列要素を参照する。 pa が整数のポインタだとすると、 int *pa; と宣言され、代入 pa = &a[0]; によって a の 0番目の要素を指すように pa がセットされる。つまり pa は a[0] のアドレスを内容とする。 これで代入文 x = *pa; により a[0] の内容が x にコピーされることになる。 pa が a[0] を指すとすると、 *(pa + 1) は a[1] の内容を参照する。 これらの説明は、配列 a の変数の型やサイズにかかわらずあてはまる。「1 をポイ ンタに加える」ことや、その拡張であるすべてのポインタ演算の意味は、pa + 1 は (pa の) 次のオブジェクトを指し、pa + i は pa の先の i番目のオブジェクトを指 すということである。 定義により、配列型の変数あるいは式の値は、配列の 0番目の要素の番地である。 そこで、代入 pa = &a[0]; の後では、pa は a と同じ値をもつ。配列の名前はその先頭の要素の位置と同義で あるから、(上の) 代入式は次のようにも書ける。 pa = a; a[i] の計算において C ではそれはただちに *(a + i) に変換される。この 2つの 形はまったく同等である。この等式*1の両辺に &演算子を適用すると、&a[i] と a + i とは同じだということになる。 a + i は a の後の i番目のアドレスである。 これを裏返していうと、pa がポインタであるとき、式の中では添字を使ってもよい ということになる。 (p119-120) *1: a[i] == *(a + i) 2005-10-18 φ(-_-) ■[lang]プログラミング言語 C (ある関数での戻り値 = 返り値がポインタの場合には) C では、データを正しく指 すポインタは 0 にはならないことを保証しているから、0 という戻り値は異常事態 の発生を知らせるのに使うことができる。 一般にポインタは、他の変数と同様に初期化できる。ただし、通常意味のある唯一 の値は、ゼロか、または適当な型をもつ前に定義されたデータのアドレスに関する 式か、である。 ポインタと整数は相互に交換可能ではない。 (ただし) ゼロは唯一の例外である。 定数ゼロはポインタへ代入してよく、ポインタは定数ゼロと比較することができる 。 記号定数 NULL は、ポインタに対する特別な値であることを明確に示す記号として 、ゼロの代わりによく使われる。 NULL は (ヘッダファイル) の中にお いて定義されている。 (p124-125) 2005-10-19 φ(-_-) ■[lang]プログラミング言語 C 先に進む前に、3月27日の日記を訂正。(p117-118) /* getint.c */ #include #include /* isspace, isdigit に必要 */ #define MAX 100 int get_int(int *); int get_ch(void); void unget_ch(int); main() { int n, m = 0; int num[MAX]; for (n = 0; n < MAX && get_int(&num[n]) != EOF; n++) ; while (m < n) { printf("%5d ", num[m]); m++; } putchar('\n'); return 0; } int get_int(int *pn) { /* get_int : 入力から 整数を取り出して *pn に 格納 */ /* ファイル終了時には EOF を、数の入力時には 正の値を 返す */ int c, sign; /* sign は 正負記号 */ while (isspace(c = get_ch())); ; /* 関数 isspace は c が space のとき 真を返す */ /* スペースは スキップ */ if (!isdigit(c) && c != EOF && c != '+' && c != '-') { unget_ch(c); return 0; } /* 関数 isdigit は c が 数のとき 真を返す */ /* 文字が 数でない (!) ときは 0 を 返して 終了する */ sign = (c == '-') ? -1 : 1; /* 負の記号を 記憶 */ if (c == '+' || c == '-') c = get_ch(); for (*pn = 0; isdigit(c); c = get_ch()) *pn = 10 * *pn + (c - '0'); *pn *= sign; if (c != EOF) unget_ch(c); return c; } char buf[MAX]; int buf_pt = 0; int get_ch(void) { return (buf_pt > 0) ? buf[--buf_pt] : getchar(); } void unget_ch(int c) { if (buf_pt >= MAX) puts("caution: buffer oversized\n"); else buf[buf_pt++] = c; } コンパイルして、random.txt で確認。 $cc -c getint.c $cc -o getint getint.o $./getint < random.txt (追記) ちょっと訂正。 2005-10-22 φ(-_-) ■[lang]プログラミング言語 C ポインタそれ自体は変数であるから、他の変数とまったく同様に配列に格納するこ とができる。このことを、一連のテキスト行をアルファベット順に分類するプログ ラム、すなわち UNIX のプログラム sort の (余分な機能を取り去った) 裸の version を書くことで示してみよう。 この場合、可変長のテキスト行を効率よく、かつ便利に扱うデータ表現が必要とな る。 分類される全行が、1つの長い文字配列内に始めから終わりまで格納されるとすると 、各行はその先頭文字へのポインタでアクセスできる。 2つの行はそのポインタを (関数) strcmp に渡すことによって比較 (することが) 可能である。順番になって いない 2つの行を交換するときには、テキスト行自体ではなく、ポインタ配列内の ポインタを交換すればよい。 これによって、実際の行を動かすための複雑な記憶管理と high overhead という 2 つの問題が解消されるのである。 さて、ここでのソートプロセスは次の 3つのステップからなる。 入力全行を読み込む→それをソートする→順番にプリントする ソートのステップはちょっと後まわしにして、データ構造および入出力に注意を向 けてみよう。 入力ルーティンでは各行の文字を集めて格納し、各行へのポインタの配列をつくら なければならない。ソートやプリントで実際に入用となる情報として、入力した行 数も数える必要がある。関数では有限個の行数を扱えるだけだから、入力が多すぎ たときは -1 のような規定外の数を返すことができる (ようにする)。 #define MAX_LINES 5000 /* max #lines to be sorted */ char *line_ptr[MAX_LINES]; /* poiinters to text lines */ #define MAX_LEN 1000 /* max length to any input line */ int get_line(char *, int); int read_lines(char *line_ptr[], int max_line) { int len, n_lines; char *p, line[MAX_LEN]; n_lines = 0; while ((len = get_line(line, MAX_LEN)) > 0) if (n_lines >= max_lines || (p = malloc(len)) == NULL) return -1; else { line[len - 1] = '\0'; /* delete newline */ strcpy(p, line); line_ptr[n_lines++] = p; } return n_lines; } int get_line(char p[], int limit) { int c, i; for (i = 0; i < limit -1 && (c = getchar()) != EOF && c != '\n'; ++i) s[i] = c; if (c == '\n') { s[i] = c; ++i; } s[i] = '\0'; return i; } ここでの新しい要点は、line_ptr の宣言である。(←ポインタの配列つまりポイン タのポインタ) char *line_ptr[MAX_LINES] は line_ptr が MAX_LINES 個の要素をもつ配列であり、その各要素が char へのポ インタであることを示す。すなわち、line_ptr[i] が文字へのポインタであり、 *line_ptr[i] はそれが指す文字、すなわち i番目に格納されたテキスト行の文字で ある。 line_ptr 自体は配列の名前であるから、ポインタとして扱うことが可能で (出力行 を書き出す) write_lines は以下のように書くことができる。 void write_lines(char *line_ptr[], int n_lines) { while (n_lines-- > 0) printf("%s\n", *line_ptr++); } *line_ptr は最初、先頭の行を指している。インクリメントするごとに、それは次 の行に進められ、同時に n_lines (行数の合計) もカウントダウンされる。 (p131-134) (追記) ここで read_lines での malloc の扱いを参考にした Answers to Exercise に は、K&R chap.5-7 で扱っている二次元配列を使った例が載っています。 http://users.powernet.co.uk/eton/kandr2/krx507.html 2005-10-23 φ(-_-) ■[lang]プログラミング言語 C 少しだけ関数 read_lines のコードをチェック。 char *p, line[MAX_LEN]; while ((len = get_line(line, MAX_LEN)) > 0) if (n_lines >= maxlines || (p = malloc(len)) == NULL) return -1; else { line[len - 1] = '\0'; strcpy(p, line); line_ptr[n_lines++] = p; } 文字列を copy する関数 strcpy は、その仮引数にポインタをとっている (p128-129)。 「関数定義の仮引数としては、char s[] と char *s はまったく同一である (p121)」 のだから、ここで宣言されている配列 line も、ポインタの p と同じように、strcpy の引数として呼び出しが可能だ、ということになる。 strcpy(p, line); それから、コードのゴシックのところは、その基本の骨組みは、関数 strdup のコード を適用しているようだ (p173)。 char *strdup(char *s) /* make a duplicate of s */ { char *p; p = (char *)malloc(strlen(s) + 1); /* + 1 for '\0' */ if ( p != NULL) strcpy(p, s); return p; } これで入出力はかたづいたから、ソートへ進もう。 クイックソートにはやや変更が必要である。同様に swap ルーティンにもほんの少 しだが変更がいる。 v = lineptr の任意の要素は文字ポインタだから、tmp もそうあるべきで、これに より互いに copy 可能となる。(p134) void q_sort(char *v[], int left, int right) { int i, last; /* do nothing if array contains fewer that two elements */ if (left >= right) return; /* move partition elem to v[0] */ swap(v, left, (left + right)/2); last = left; for (i = left + 1; i <= right; i++) /* partition */ if (strcmp(v[i], v[left]) < 0) swap(v, ++last, i); swap(v, left, last); /* restore partition elem */ /* The same process is then applied recursively to the two subsets */ q_sort(v, left, last - 1); q_sort(v, last + 1, right); } void swap(char *v[], int i, int j) { char *tmp; tmp = v[i]; v[i] = v[j]; v[j] = tmp; } 完成したプログラムは次のとおり。 /* prim_sort.c */ #include #include #include #define MAX_LINES 5000 /* max # lines to be sorted */ #define MAX_LEN 1000 /* max length of any input line */ char *line_ptr[MAX_LINES]; int read_lines(); int get_line(); void write_lines(); void q_sort(); void swap(); /* primitive sort program */ main() { int n_lines; /* numbers of input lines read */ if ((n_lines = read_lines(line_ptr, MAX_LINES)) >= 0) { q_sort(line_ptr, 0, n_lines - 1); write_lines(line_ptr, n_lines); return 0; } else { puts("error: input too big to sort"); return -1; } } int read_lines(char *line_ptr[], int maxlines) { int len, n_lines; char *p, line[MAX_LEN]; n_lines = 0; while ((len = get_line(line, MAX_LEN)) > 0) if (n_lines >= maxlines || (p = malloc(len)) == NULL) return -1; else { line[len - 1] = '\0'; /* delete newline */ strcpy(p, line); line_ptr[n_lines++] = p; } return n_lines; } int get_line(char s[], int limit) { int c, i; for (i = 0; i < limit - 1 && (c = getchar()) != EOF && c != '\n'; ++i) s[i] = c; if (c == '\n') { s[i] = c; ++i; } s[i] = '\0'; return i; } void write_lines(char *line_ptr[], int n_lines) { while (n_lines-- > 0) printf("%s\n", *line_ptr++); } void q_sort(char *v[], int left, int right) { int i, last; if (left >= right) return; swap(v, left, (left + right)/2); last = left; for (i = left + 1; i <= right; i++) if (strcmp(v[i], v[left]) < 0) swap(v, ++last, i); swap(v, left, last); q_sort(v, left, last - 1); q_sort(v, last + 1, right); } void swap(char *v[], int i, int j) { char *tmp; tmp = v[i]; v[i] = v[j]; v[j] = tmp; } コンパイルして、(ちょっと irregular だけど) code file で確認。コマンド sort と も比較してみる。 $cc -c prim_sort.c $cc -o prim_sort prim_sort.o $./prim_sort < prim_sort.c | less (q で 終了) $sort prim_sort.c | less 2005-10-27 φ(-_-) ■[lang]プログラミング言語 C 多次元配列はポインタ配列とは違うものなので、後まわしにして、 n番目の月の名前からなる文字列のポインタを返す month_name(n) を書く問題を考 えてみよう。これは (関数内のみで働く) internal な static 配列にはもってこい の応用例である。 month_name には、文字列 (character string) を入れるための private な配列が含まれていて、呼び出されたときには (引数 n に対応した) 文字 列のポインタを正しく返す。この section では、配列 name をどのようにして初期 化するかを示す。 char *month_name(int n) { static char *name[] = { "Illigal month", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; return (n < 1 || n > 12) ? name[0] : name[n]; } name の宣言は、文字列のポインタ配列だが、これは前に述べた sort の例での line_ptr と同じである。初期化式 (initializer) は文字列のリストであり、各初 期値は配列内の対応する位置に代入される。 i番目の文字列 (string) は別の場所 に置かれ、そのポインタが name[i] に格納される。配列 name の大きさは指定され ていないので、コンパイラが初期値式を計算して正確な数値を代入する。p137-138 関数 month_name の戻り値は char型のポインタであり、これは char型の配列と置き換 えることができる ... はず。確認のため、簡単なプログラムを 2つつくってみた。 /* month_array.c */ #include char *month_name(); main() { int i; for (i = 0; i < 13; i++) printf("%2d -> %s\n", i, month_name(i)); return 0; } char *month_name(int n) { static char *name[] = { "Illigal month", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; return (n < 1 || n > 12) ? name[0] : name[n]; } /* month_view.c */ #include char *month_name(); main() { int c; printf("Input any number > "); fscanf(stdin, "%d", &c); printf("%d means %s\n", c, month_name(c)); if (c == 1) puts("Hm, January is my birthday month ... Did you know? [N/n]"); return 0; } char *month_name(int n) { static char *name[] = { "Illigal month", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; return (n < 1 || n > 12) ? name[0] : name[n]; } 2005-10-29 φ(-_-) ■[lang]プログラミング言語 C C の初心者はときどき 2次元配列と、例としてでてきた name のようなポインタ配 列との違いについて混乱することがある。 下のような定義が与えられたとする: int a[10][20]; /* 2-dimensional array */ int *b[10]; /* pointer array */ このとき、a[3][4] と b[3][4] とでは両方とも構文としては規則に適ったある単一 の int を参照している。 しかし a は本物の 2次元配列なので 200個の int サイズの location が別に確保 されていて、a[row, col] の element を求めると 20 x row + column というお約 束の直交的な添字計算が実行される。 b では、しかしながら、定義によって 10個のポインタが割り当てられるだけで、そ れら (← location) は初期化されていない。 初期化は、static あるいはコードによって明確に行なわないといけない。 b のそれぞれの element が (実際すべての) 20個の element の配列を指すために は、200個の int プラス、ポインタ用の cell が別に確保されることになる。 ポインタ配列の有利な点で重要なのは、配列に含まれる row が異なった長さでもか まわないことである。つまり、b のそれぞれの element が (すべて) 20個の element をもつベクタを指す必要はない。 (ある row では) 2つの element を 指し、あるものは 50個、またまったく何も指さなくてもかまわない。 ここまでの議論では整数 (integer) に関してのみ扱ったが、ポインタ配列がしばし ば用いられるのは、関数 month_name のように異なった長さの文字列を格納すると きである。(p138-139) ポインタ配列の宣言とベクタとを、同じく 2次元配列のそれと比較してみよう: char *name[] = {"Illigal month", "Jan", "Feb", "Mar"}; (vector) name: . -----> Illigal month\0 . -----> Jan\0 . -----> Feb\0 . -----> Mar\0 char aname[][15] = {"Illigal month", "Jan", "Feb", "Mar"}; aname: Illigal month\0 Jan\0 Feb\0 Mar\0 0 15 30 45 ここでの KandR の説明のしかたは、相当凝縮されていますネ ... 2005-11-03 φ(-_-) ■[lamg]プログラミング言語 C hash関数によるテーブル参照がわからないので、訳しながら読んでみる。 このセクションでは、構造体のより特色のある面の説明のために、table-lookup package の中身を書くことにしよう。 そのコードには、マクロやコンパイラにおける symbol table management に特徴的 なルーティンが見出される。例えば、#define での statement を考えてみよう。 #define IN 1 という line が見つかると、name "IN" とそれに置き換わる text "1" とが 1つの table に格納される。 その後、この statement の中で name "IN" がでてくると state = IN; "1" と取り換えられる。 (このように) name と置き換える text とを処理するには、2つのルーティンが (必 要になる)。 install(s, t) によって、name "s" とそれを置き換える text "t" とを 1つの table に記憶させる。ここでの s と t とは文字列である。 lookup(s) では、table から "s" を検索し、探しだすとその place のポインタを 返し、なければ NULL を返す。 この algorithm を hash-search といい、入ってくる name は符号がつかない小さ な整数へと変換され、それがポインタ配列の index として使われている。 配列の element は、それの hash値をもつ name が記された block のリンクリスト の初め (beginning) を指している。 hash値をもつ name がない (element は) NULL である。 (element) (block) (block) ・-----> ・ -----> 0 (list) 0 ・→ ・→ name 0 ・→ ・→ defn ・-----> 0 0 ・→ name ・→ defn list の中の block は構造体であり、name と text それに次の block に繋げる next への (それぞれの) ポインタからなっている。 next が NULL ポインタを指す と、それは list の終わりをあらわす。 struct nlist { /* table entry */ struct nlist *next; /* next entry in chain */ char *name; /* defined name */ char *defn; /* replacement text */ }; ここではポインタ配列が (次のように使われている)。 #define HASHSIZE 101 static struct nlist *hashtab[HASHSIZE]; lookup および install で使われる hash 関数は、文字列の中の各文字の value を 混ぜ合わせながら加算して、さらに array size で割ってその余り (modulo) を ( 戻り値として) 返している。 これは最良の algorithm ではないが、しかし短いし効果的である。 /* hash: form hash value for string s */ unsigned hash(char *s) { unsigned hashval; for (hashval = 0; *s != '\0'; s++) hashval = *s + 31 * hashval; return hashval % HASHSIZE; } hash値がマイナスにならないことは unsigned 演算によって保証されている。 hash処理によって、配列 hashtab に list の starting index がつくられる。文字 列が見つかると、そこから始まってる block の list に含まれている (ことにな る)。 検索は lookup によって行なう。 lookup では、その entry がすでに含まれていれ ばそのポインタを (戻り値として) 返し、なければ NULL を返す。 /* lookup: look for s in hashtab */ struct nlist *lookup(char *s) { struct nlist *np; for (np = hashtab[hash(s)]; np != NULL; np = np->next) if (strcmp(s, np->name) == 0) return np; /* found */ return NULL; /* not found */ } lookup での for ループは、リンクリストをたどっていくときによく使われるやり 方である。 for (ptr = head; ptr != NULL; ptr = ptr->next) install では、lookup を使ってその name がすでに含まれているかどうかを調べて いる。もしあれば新しい定義が古いそれと入れ替わる。そうでなければ新しい entry を作成する。 また install では、何らかの理由で新しい entry をつくる余地がないときは NULL を返すようにする。(p174-176) struct nlist *lookup(char *); char *strdup(char *); /* install: put (name, defn) in hashtab */ struct nlist *install(char *name, char *defn) { struct nlist *np; unsigned hashval; if ((np = lookup(name)) == NULL) { /* not found */ np = (struct nlist *)malloc(sizeof(*np)); if (np == NULL || (np->name = strdup(name)) == NULL) return NULL; hashval = hash(name); np->next = hashtab[hashval]; hashtab[hashbval] = np; } else /* already there */ free((void*)np->defn); /* free previous defn */ if ((np->defn = strdup(defn)) == NULL) return NULL; return np; } う〜ん、ややこしいヨ ... (;_;) 2005-11-05 φ(-_-) ■[lang]プログラミング言語 C どうやら hash search と skip list の間には、構造体へのポインタを使った共通する algorithm があるらしい、というところまでは判明。(これ以上は、初心者ではムリみた い ...) ポインタ配列とは別なので、とばしていた多次元配列に戻って、 実際には、ポインタ配列よりも使用されることは少ないが、C では直交的な多次元 配列が使える。 C においては、二次元配列は実際には一次元配列であって、そこでの要素がそれぞ れに 1つの配列になっている。したがって、添字は、 daytab[i][j] /* [row][col] */ のように書くのであって daytab[i, j] /* WRONG */ はまちがいである。 この記法以外の点では、二次元配列は他の言語とほぼ同様に扱うことが可能である 。要素は行ごとに格納され、要素がメモリの順にアクセスされるときは (配列の) 最も右の要素、つまり列 (column) が最も早く変化する。 配列は大カッコ { } で囲まれた初期値リストで初期化される。 二次元配列を関数に渡す場合は、列の数 (number of colums) がなければならない 。渡されるのは行の配列のポインタだから行の数 (number of rows) は無関係であ る。 (つまり) func(int daytab[][13]) { .... } あるいは、 func(int (*daytab)[13]) { .... } と書ける。これは、引数が 13個の配列のポインタであることを示している。(下の 式で) カッコ ( ) が必要なのは、中カッコ [ ] が * よりも優先度が高いからであ る。(p135-137) 2005-11-06 φ(-_-) ■[lang]プログラミング言語 C 二次元配列を使って、表計算するコードが下の本に載っていた*1ので、ちょっといじっ てみる。 ・プログラミングの力を生みだす本 (isbn:4274132072) total と average の二重ループでは、 row と column の順が逆になっています。 表はこんな感じで、 ┌─────┬───┬────┬───┬───┬───┐ │ │col 0 │col 1 │col 2 │col 3 │total │ ├─────┼───┼────┼───┼───┼───┤ │row 0 │1 │2 │3 │4 │ │ ├─────┼───┼────┼───┼───┼───┤ │row 1 │2 │3 │4 │5 │ │ ├─────┼───┼────┼───┼───┼───┤ │row 2 │3 │4 │5 │6 │ │ ├─────┼───┼────┼───┼───┼───┤ │average │ │ │ │ │ │ └─────┴───┴────┴───┴───┴───┘ /* table.c */ #include main() { int data[4][5]; int i, j, sum; puts("Input data"); for (i = 0; i < 3; i++) { for (j = 0; j < 4; j++) { printf("%d %d > ", i, j); fscanf(stdin, "%d", &data[i][j]); } putchar('\n'); } for (i = 0; i < 3; i++) { sum = 0; for (j = 0; j < 4; j++) sum += data[i][j]; data[i][4] = sum; } for (j = 0; j < 4; j++) { sum = 0; for (i = 0; i < 3; i++) sum += data[i][j]; data[3][j] = sum / 3; } data[3][4] = 0; puts("total and average"); for (i = 0; i < 4; i++) { for (j = 0; j < 5; j++) printf("%4d ", data[i][j]); putchar('\n'); } } *1:p131