[←1つ前] [→1つ後] [↑質問一覧] [↑記事一覧] [ホームページ]
前回は文字列を特集しましたが、文字列と密接に関係するのがポインタです。 初心者にとっては鬼門と呼ばれることもあるポインタですが、C言語を使いこなす には必要不可欠な概念でもあります。
経験的には、プログラムが思った通りに動作しない場合、ポインタの使い方を 間違っている確率は非常に高いようです。ポインタを十分理解することが、トラ ブルを少なくする近道の一つです。
C言語に関する入門書の殆どが、ポインタとは何かというテーマで解説した章を 含んでいるはずなので、それらを読んでください。簡単なイメージとしては、実 体そのものではなく、実体がどこかにあるという情報を間接的に持つと理解する ことができます。
NULL
とは何か。
ところで、ポインタを使う場合、どこも指していないことを明確に示したいこ
とがよくあります。このような時に使う値が
NULL
です。すなわち、
NULL
はどの実体も指さないことが保証されています。
例えば、リスト構造を処理する場合、リストは次の要素へのポインタを使って
表現されます。最後の要素は、次に指すべき要素がないので、
NULL
という値を入れておくのが簡単です。
struct list { struct list *next; char *data; }; void foo(list *li) { while (li != NULL) { /* 次がなくなるまで */ bar(li->data); /* データを処理する */ li = li->next; /* li が次のリスト要素を指すようにする */ } }
NULL
ポインタの代わりに0を使ったコードを見たことがある。なぜ
NULL
の代わりに0を使ってもよいのか。
if (p != 0)
のような表現が現われると、コンパイラは比較の左辺がポインタである場合には、
右辺もポインタであると考え、従って0をヌルポインタとして解釈することになり
ます。
ポインタを初期化したり代入する場合は、左辺にポインタがあることによって、 コンパイラはポインタの処理をしなければならないことを理解し、もし右辺に0が 現われたらそれをヌルポインタとして解釈します。比較の場合は、演算子の左右 いずれかにポインタが現われたら、もう片方に現われた0をヌルポインタとして解 釈します。
この場合、0と書くと、コンパイラはそれをヌルポインタではなく整定数の0と
解釈してしまいます。これはヌルポインタとは異なった値や大きさを持つかもし
れません。この失敗を避けるためには、
NULL
を使うか、あるいは次のようにキャストを使うことによって、引数がポインタで
あることを明示的にコンパイラに知らせる必要があります。
foo((char *) 0);関数の引数が可変個の場合も、引数にポインタを与えるときは、呼び出し側で 型を明示しておく必要があります。有名な例が、UNIXのシステムコールの
execl
です。
execl("/bin/sh", "sh", "-c", "ls", (char *)0);(参考 comp.lang.c FAQ 5.2)
NUL
と
NULL
の違いは何か。
NUL
というのは、値が0である文字コードを意味しており、ポインタとは何の関係もあ
りません。null文字と表現することもあります。
NULL
というのはどこも指していないポインタを意味します。従って、これらは全く別
の概念です。
ただし、処理系によっては、たまたま
NUL
と
NULL
が同じ値になってしまう場合があって、たまたま間違ったままでも動作すること
があります。もちろん、それは望ましいコードとは言えないのですが。
char *strcpy(char *dst, char *src) { char *tmp; tmp = dst; while ((*src++ = *dst++ ) != NULL) ; return tmp; }
(*src++ = *dst++)
という式の型は
char
になるはずです。これはポインタではないので、
NULL
と比較するのはナンセンスです。おそらく、このコードを書いた人は、
NULL
と
NUL
を間違えたのでしょう。
strcpy
は、文字列のコピーをする関数、すなわち、ポインタが
NUL
を指している所まで(それを含めて)内容をコピーする関数です。
""
と書いた場合に該当します。
strlen
の引数にすると、0という値が戻ってくるはずです。
すなわち、ヌルストリングとは、いきなり文字列の終了となる文字列、言い換 えれば、文字数が0個の文字配列の実体を指しているポインタであると解釈できま す。これはヌルポインタとは異なります。ヌルポインタは、どこも指していない ポインタです。
NULL
は何と発音するのか。「ナル」と発音すべきだと言われた。
NULL
はアルファベットで表現されているが日本語でありかつヌルと発音するのだ、と
強引に解釈しても、実害はないと思われます。
NULL
に対応する日本語が「ヌル」であって、英語でどう発音しようが無関係だ、と開
き直ることもできそうです。
JISでは
NULL
に対して「空ポインタ定数」という表現を使っています。
ptr
に対して、
if (ptr) {}
のような書き方を見たことがある。
if (ptr != NULL)
のように書かなくてもよいのか。
if (ptr)
という書き方と、
if (ptr != NULL)
という書き方は、
ptr
がポインタである限り、全く同じ意味となります。従って、
if (ptr)
と書けば十分だし、プログラマーが
NULL
との比較であることを明確に意志表示するために
if (ptr != NULL)
と書いても何の問題もありません。どちらでも同じです。
しかし、このようなプログラムは大変危険です。そのメモリは他のプログラム が別の用途に使うかもしれません。メモリ保護機能を使ったOSの場合、自プログ ラムが使える範囲外のメモリをアクセスしようとしたら、そこでプログラムを異 常終了する場合もあります。これにより、他のプログラムへの被害を最小限にす ることができるのです。
静的変数や外部変数は、特に初期化の値を指定しなければ0という値になってい ます。になっています。このままでポインタの指す先に代入しようとすれば、 処理系によっては「null pointer assignment」というエラーメッセージが出る か、プログラムが異常終了するでしょう。
自動変数は初期化されないので、とんでもない所を指している可能性がありま す。最もやっかいなのは、そのプログラムが参照可能な領域を、たまたま指して いる場合です。実行時にはエラーが発生しないが、全く関係なさそうな変数の値 がいつの間にか変化している、というような症状になるでしょう。
このトラブルを解決することは簡単です。ポインタを使う前には、かならず正 しいオブジェクトを指すような値にすることです。
printf("%s¥n", NULL);
printf
の書式制御文字列である%sに対応する型は
char[]
であり、すなわち期待されている引数は文字配列です。従って、それに対応する
引数は、どこかに実体があることが期待されており、
NULL
を与えた結果は保証されないと考えるべきです。
しかし、処理系によっては、このようなコードを実行すると、画面に(null)の ような表示を行う場合もあります。
関数を呼び出す時に、もしプロトタイプ宣言を用いずに
NULL
ポインタの代わりに0を使ったら、コンパイラはそれがポインタだと認識できない
ので、0という整数であると解釈したコードを出すでしょう。これはスモールモデ
ルのMS-DOSの場合は、整数が16ビットであるため、たまたまうまく動作するでし
ょう。しかし、ラージモデルの関数ライブラリは、ポインタが32ビットであると
いう前提で作られていますから、呼び出し側でセットしなかった残りの16ビット
の値に対して、とんでもない解釈をしてしまうかもしれません。
あるいは、ポインタを戻すような関数、例えば
malloc
に対するプロトタイプ宣言がない場合には、その戻り値は
int
であるとみなされることになります。この値は16ビットの大きさで、前述のよう
に残りの16ビット分の内容が問題になります。
これを解決する方法は、プロトタイプ宣言を忘れずに行うことです。プログラ
ムが複数のモジュールに分かれているなら、
malloc
を使っているモジュールが
stdlib.h
をインクルードしているかどうか確認してください。心配なら、一旦オブジェク
ト(.obj)を全部削除してから再コンパイルしましょう。
near
とか
far
というキーワードは何か。ANSI Cには見当たらない。
near
や
far
の概念はここでは説明しませんので、x86系のCPUの説明書、あるいはアセンブラ
の解説書を見てください。
MS-DOS用のコンパイラなら、特に制限のない限り、コンパイル、リンク時にメ
モリモデルを指定することができるものが殆どです。この場合は、小規模なプロ
グラムならスモールモデル、巨大なプログラムならラージモデルを指定すれば、
プログラム中で
near
や
far
を意識してコードを書く必要はほとんどありません。
一般に、スモールモデルでコンパイルできるプログラムは、ラージモデルの指 定でコンパイルしても動作しますが、コードのサイズは多少増加し、処理は遅く なります。
long int
のサイズが4バイト、
short int
のサイズが2バイトの処理系を使っている。これらのオブジェクトを指すポイン
タを使いたい。ポインタが次の要素を指すようにするには、そのサイズに対応し
た値(4や2)を足さなければならないのか。
従って、ポインタがどのような大きさのオブジェクトを指していても、今指し ているオブジェクトの次を指すようにしたければ、1を足せばよいのです。その結 果、ポインタの実際の値は2増えるかもしれないし、4増えるかもしれません。
次のコードは、
lp1
と
lp2
の値の差はどうであれ、
lp2 - lp1
という演算の結果は、添え字の引き算の結果、すなわち4 (7-3)という値になる
はずです。
#include <stdio.h> int main(void) { long al[10]; long *lp1; long *lp2; lp1 = &al[3]; /* 4番目の要素を指す */ lp2 = &l[7]; /* 8番目の要素を指す */ printf("差は%dです¥n", lp2 - lp1); return 0; }
++
や
--
の演算子を作用すると、何が起きるのか。
char
へのポインタがあるが、それが指す先は
int
であることが処理上分かっているとする。ポインタを次の
int
の要素を指すようにしたい。しかし、このように書いたがうまくいかない。
((int *)p)++
++
を行うことができません。ただし、このようなコードをコンパイルできるように
独自に拡張したコンパイラもあるようです。
ANSI Cの仕様に従ったコードを書きたいなら、次のようにするのが明快です。
p += sizeof(int);あるいは、こうすることもできます。
p = (char *)((int *)p + 1);(参考 comp.lang.c FAQ 4.5)
char *strrev(char *str) { char *head; char *tail; int len; head = str; tail = head + strlen(str) - 1; while (head < tail) { char c; c = *head; *head++ = *tail; *tail-- = c; } return str; }
このプログラムは、たまたま
str
の長さが0である場合には、
tail
は元のオブジェクトの先頭要素の一つ前(があれば)を指すことになります。
tail
にそのような値を代入すること自体は問題ありません。問題は、
while
の条件判断の部分の
(head < tail)
という比較です。
head
はオブジェクトの要素を指していないので、仕様によりその結果は
未定義
ということになります。
次のようなコードにすれば、一応この問題は解決しますが、このようなトリッ
キーを書くよりは、むしろ、
strlen(str)
が1以下の場合には、何もせずにそのまま
str
を戻してしまうようなif文をコードに追加した方が簡単だし明解です。0以下では
なく1以下でそのままリターンさせるのは、ささやかな処理の節約です。
このコードは、ポインタの比較をする場合に、ポインタがオブジェクトの最後 の次の要素を指していてもよい、ということを利用しています。
head = str; tail = head + strlen(str); /* 文字列終端のNULを指す */ while (head + 1 < tail) { /* +1は、なくても動作する */ char c; c = *head; *head++ = *--tail; *tail = c; }特に、ポインタを文字列の最後から先頭に向かって移動する時に、うっかりす ることがあります。試しに、文字列
str
の中に指定した文字
c
があれば、最も後ろに見つかった位置のポインタを戻し、見つからない場合には
NULL
を戻す関数、
char *rev_search(char *str, char c);
を書いてみてください。先頭からサーチして最後に見つかったポインタを戻す方
法もありますが、文字列を全てサーチすることになり、効率がよくありません。
文字列の最後から先頭に向かってサーチすれば、最初に見つかった所で処理を中
断できます。
注意すべき点は、文字列の先頭を超えた位置にポインタを変化させないことと、 そして、文字列の長さが0の時にも、変な位置を指さないようにすることです。万 一そのような値になったとしても、比較や代入をしなければ、C言語としては間違 いではないのですが、現実的に、比較も代入もできない値をセットしても意味は ありません。
char *rev_search(char *str, char c) { char *s; s = str + strlen(str); while (s > str) { s--; if (*s == c) return s; } return NULL; }