初級C言語Q&A(3)

初出: C MAGAZINE 1995年8月号
Updated: 1996-03-12

[1つ前] [1つ後] [質問一覧] [記事一覧] [ホームページ]


ポインタ

前回は文字列を特集しましたが、文字列と密接に関係するのがポインタです。 初心者にとっては鬼門と呼ばれることもあるポインタですが、C言語を使いこなす には必要不可欠な概念でもあります。

 経験的には、プログラムが思った通りに動作しない場合、ポインタの使い方を 間違っている確率は非常に高いようです。ポインタを十分理解することが、トラ ブルを少なくする近道の一つです。


Q 【ポインタ】

 ポインタとは何か。

 それを詳しく説明すると他のQ&Aを書く頁がなくなってしまいます。:-)

 C言語に関する入門書の殆どが、ポインタとは何かというテーマで解説した章を 含んでいるはずなので、それらを読んでください。簡単なイメージとしては、実 体そのものではなく、実体がどこかにあるという情報を間接的に持つと理解する ことができます。


Q 【NULL】

 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 が次のリスト要素を指すようにする */
    }
}

Q 【0とNULL】

 NULL ポインタの代わりに0を使ったコードを見たことがある。なぜ NULL の代わりに0を使ってもよいのか。

 ポインタが現われるべき所に0という値が現われた場合、コンパイラはそれをヌ ルポインタと解釈する仕様になっています。 if (p != 0) のような表現が現われると、コンパイラは比較の左辺がポインタである場合には、 右辺もポインタであると考え、従って0をヌルポインタとして解釈することになり ます。

 ポインタを初期化したり代入する場合は、左辺にポインタがあることによって、 コンパイラはポインタの処理をしなければならないことを理解し、もし右辺に0が 現われたらそれをヌルポインタとして解釈します。比較の場合は、演算子の左右 いずれかにポインタが現われたら、もう片方に現われた0をヌルポインタとして解 釈します。


Q 【関数呼び出しの0】

 関数呼び出しの引数としてヌルポインタを書く場合、0と書いてはいけないと言 われた。

 関数呼び出しの引数の型は、プロトタイプ宣言がある限り、コンパイラが適切 な型に変換することができますが、そうでなければ、その関数がどのような型の 引数を期待しているのか知ることができないため、呼び出しの所に与えられた変 数や定数の型をそのまま使って関数を呼び出すようなコードを生成します。

 この場合、0と書くと、コンパイラはそれをヌルポインタではなく整定数の0と 解釈してしまいます。これはヌルポインタとは異なった値や大きさを持つかもし れません。この失敗を避けるためには、 NULL を使うか、あるいは次のようにキャストを使うことによって、引数がポインタで あることを明示的にコンパイラに知らせる必要があります。

   foo((char *) 0);
 関数の引数が可変個の場合も、引数にポインタを与えるときは、呼び出し側で 型を明示しておく必要があります。有名な例が、UNIXのシステムコールの execl です。
  execl("/bin/sh", "sh", "-c", "ls", (char *)0);
(参考 comp.lang.c FAQ 5.2)

Q 【NUL】

 NULNULL の違いは何か。

NUL というのは、値が0である文字コードを意味しており、ポインタとは何の関係もあ りません。null文字と表現することもあります。 NULL というのはどこも指していないポインタを意味します。従って、これらは全く別 の概念です。

 ただし、処理系によっては、たまたま NULNULL が同じ値になってしまう場合があって、たまたま間違ったままでも動作すること があります。もちろん、それは望ましいコードとは言えないのですが。


Q 【NULとNULL】

 次のコードはどこがいけないのか。
char *strcpy(char *dst, char *src)
{
    char *tmp;

    tmp = dst;
    while ((*src++ = *dst++ ) != NULL)
        ;

    return tmp;
}

 引数の型を見て分かるように、  (*src++ = *dst++) という式の型は char になるはずです。これはポインタではないので、 NULL と比較するのはナンセンスです。おそらく、このコードを書いた人は、 NULLNUL を間違えたのでしょう。 strcpy は、文字列のコピーをする関数、すなわち、ポインタが NUL を指している所まで(それを含めて)内容をコピーする関数です。

Q 【ヌルストリング】

 ヌルストリングとは何か。

文字が一つもない文字列のことをヌルストリングと表現することがあります。 これは "" と書いた場合に該当します。 strlen の引数にすると、0という値が戻ってくるはずです。

 すなわち、ヌルストリングとは、いきなり文字列の終了となる文字列、言い換 えれば、文字数が0個の文字配列の実体を指しているポインタであると解釈できま す。これはヌルポインタとは異なります。ヌルポインタは、どこも指していない ポインタです。


Q 【NULLの読み方】

 NULL は何と発音するのか。「ナル」と発音すべきだと言われた。

nullという英語は日本語として「ナル」と喋った時に近い音に聞こえるでしょ う。ただ、私の経験では、「ヌル」と言って通じなかったことはありません。 NULL はアルファベットで表現されているが日本語でありかつヌルと発音するのだ、と 強引に解釈しても、実害はないと思われます。 NULL に対応する日本語が「ヌル」であって、英語でどう発音しようが無関係だ、と開 き直ることもできそうです。

JISでは NULL に対して「空ポインタ定数」という表現を使っています。


Q 【if (ptr)】

 ポインタ ptr に対して、 if (ptr) {} のような書き方を見たことがある。 if (ptr != NULL) のように書かなくてもよいのか。

 if (ptr) という書き方と、 if (ptr != NULL) という書き方は、 ptr がポインタである限り、全く同じ意味となります。従って、 if (ptr) と書けば十分だし、プログラマーが NULL との比較であることを明確に意志表示するために if (ptr != NULL) と書いても何の問題もありません。どちらでも同じです。

Q 【null pointer assignment】

 プログラムをコンパイルしても何も問題はなかったが、実行したら「null pointer assignment」というエラーメッセージが出てしまった。

 ヌルポインタというのは、どこも指していないポインタとして使われるはずで すから、そこへ代入することは理屈の上ではありえない話ですが、実際は、プロ グラムのバグによって、そこにうっかり代入してしまうこともあります。その結 果どうなるかは 未定義 です。プログラムがそこで異常終了するかもしれません。MS-DOSの場合、上記の ようなメッセージが出ることがあります。

Q 【初期化されていないポインタ】

 あるプログラムをMS-DOSで使って何も問題が発生しなかったのに、UNIXで同じ ソースをコンパイルして実行したらプログラムが異常終了してしまった。コンパ イル時に警告を出すようにしたら、初期化されていないポインタが使われている というメッセージが出た。

 ポインタはどこか正しい実体を指す必要があります。一般に、ポインタはメモ リのどこかを指すようなアドレスが格納されるように実装されていますが、論理 的に可能なメモリのアドレス範囲に比べて、本当にメモリが入っているか、ある いはスワップのような技法で使うことができる領域は、一部分に過ぎません。従 って、たまたまポインタが指している先が、本当にアクセス可能なメモリであっ た場合には、偶然プログラムが動作してしまう場合もあります。

 しかし、このようなプログラムは大変危険です。そのメモリは他のプログラム が別の用途に使うかもしれません。メモリ保護機能を使ったOSの場合、自プログ ラムが使える範囲外のメモリをアクセスしようとしたら、そこでプログラムを異 常終了する場合もあります。これにより、他のプログラムへの被害を最小限にす ることができるのです。

 静的変数や外部変数は、特に初期化の値を指定しなければ0という値になってい ます。になっています。このままでポインタの指す先に代入しようとすれば、 処理系によっては「null pointer assignment」というエラーメッセージが出る か、プログラムが異常終了するでしょう。

 自動変数は初期化されないので、とんでもない所を指している可能性がありま す。最もやっかいなのは、そのプログラムが参照可能な領域を、たまたま指して いる場合です。実行時にはエラーが発生しないが、全く関係なさそうな変数の値 がいつの間にか変化している、というような症状になるでしょう。

 このトラブルを解決することは簡単です。ポインタを使う前には、かならず正 しいオブジェクトを指すような値にすることです。


Q【書式制御文字とNULL】

 次のコードは正しいか。
    printf("%s¥n", NULL);

 printf の書式制御文字列である%sに対応する型は char[] であり、すなわち期待されている引数は文字配列です。従って、それに対応する 引数は、どこかに実体があることが期待されており、 NULL を与えた結果は保証されないと考えるべきです。

 しかし、処理系によっては、このようなコードを実行すると、画面に(null)の ような表示を行う場合もあります。


Q 【メモリモデルを変えたら動かない】

 MS-DOSで作っているプログラムが大きくなったので、今までスモールモデルで コンパイルしていたが、ラージモデルでコンパイルするように変更したところ、 プログラムが暴走するようになってしまった。同じコードをスモールモデルでコ ンパイルすると動作するのだが。

 メモリモデルの変更はポインタに対して大きく影響します。メモリモデルによ って、ポインタを表現するサイズが変化するからです。具体的には、スモールモ デルでは、ポインタは16ビットの大きさですが、ラージモデルの場合は32ビット 必要です。

 関数を呼び出す時に、もしプロトタイプ宣言を用いずに NULL ポインタの代わりに0を使ったら、コンパイラはそれがポインタだと認識できない ので、0という整数であると解釈したコードを出すでしょう。これはスモールモデ ルのMS-DOSの場合は、整数が16ビットであるため、たまたまうまく動作するでし ょう。しかし、ラージモデルの関数ライブラリは、ポインタが32ビットであると いう前提で作られていますから、呼び出し側でセットしなかった残りの16ビット の値に対して、とんでもない解釈をしてしまうかもしれません。

 あるいは、ポインタを戻すような関数、例えば malloc に対するプロトタイプ宣言がない場合には、その戻り値は int であるとみなされることになります。この値は16ビットの大きさで、前述のよう に残りの16ビット分の内容が問題になります。

 これを解決する方法は、プロトタイプ宣言を忘れずに行うことです。プログラ ムが複数のモジュールに分かれているなら、 malloc を使っているモジュールが stdlib.h をインクルードしているかどうか確認してください。心配なら、一旦オブジェク ト(.obj)を全部削除してから再コンパイルしましょう。


Q 【near/far】

  near とか far というキーワードは何か。ANSI Cには見当たらない。

 これはIntelのx86系CPUに特有のキーワードです。このCPUを使ったマシン用の コンパイラは、これらのキーワードを独自に拡張して使うことができるものが殆 どです。 nearfar の概念はここでは説明しませんので、x86系のCPUの説明書、あるいはアセンブラ の解説書を見てください。

 MS-DOS用のコンパイラなら、特に制限のない限り、コンパイル、リンク時にメ モリモデルを指定することができるものが殆どです。この場合は、小規模なプロ グラムならスモールモデル、巨大なプログラムならラージモデルを指定すれば、 プログラム中で nearfar を意識してコードを書く必要はほとんどありません。

 一般に、スモールモデルでコンパイルできるプログラムは、ラージモデルの指 定でコンパイルしても動作しますが、コードのサイズは多少増加し、処理は遅く なります。


Q 【ポインタの演算】

 long int のサイズが4バイト、 short int のサイズが2バイトの処理系を使っている。これらのオブジェクトを指すポイン タを使いたい。ポインタが次の要素を指すようにするには、そのサイズに対応し た値(4や2)を足さなければならないのか。

 ポインタに整数値を加減する場合は、整数変数の計算ではなく、ポインタとし て特殊な計算を行います。すなわち、コンパイラは、指定した整数値だけ前後の オブジェクトを指すように、ポインタの値を変更する、という意味に解釈します。

 従って、ポインタがどのような大きさのオブジェクトを指していても、今指し ているオブジェクトの次を指すようにしたければ、1を足せばよいのです。その結 果、ポインタの実際の値は2増えるかもしれないし、4増えるかもしれません。


Q 【ポインタ同士の引き算】

 ポインタからポインタが引き算されるようなコードを見た。この引き算にはど のような意味があるのか。

 ポインタ同士の引き算は、両者が同一の配列オブジェクトを指していれば、そ れぞれが指している要素の添え字の差が結果として得られます。

 次のコードは、 lp1lp2 の値の差はどうであれ、 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;
}

Q 【++、--】

 ポインタに対して ++-- の演算子を作用すると、何が起きるのか。

 これらの演算子が、変数に対して1を加算したり減算するということに代わりは ありません。ただし、ポインタに対して1を加算するということは、値に1を加え るのではなく、一つ先の要素を指すような値にするという意味になります。従っ て、これらの演算子を作用させた結果、ポインタは次の(あるいは前の)要素を指 すようになります。

Q 【キャスト】

char へのポインタがあるが、それが指す先は int であることが処理上分かっているとする。ポインタを次の int の要素を指すようにしたい。しかし、このように書いたがうまくいかない。
        ((int *)p)++

 キャストは型を変換する演算子ですが、その結果は右辺値というものになりま す。右辺値に対しては仕様上代入はできませんから、 ++ を行うことができません。ただし、このようなコードをコンパイルできるように 独自に拡張したコンパイラもあるようです。

 ANSI Cの仕様に従ったコードを書きたいなら、次のようにするのが明快です。

    p += sizeof(int);
 あるいは、こうすることもできます。
    p = (char *)((int *)p + 1);
(参考 comp.lang.c FAQ 4.5)

Q 【strrev】

 このプログラムはどこがいけないのか。
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;
}

 ポインタ同士を比較する場合には、それぞれが共通のオブジェクトを指してい るか、あるいはそのオブジェクトの最後の要素の次があると仮定して、その箇所 を指している場合のみ意味を持ちます。ANSI Cでは、これ以外の場合にポインタ を比較した結果は 未定義 という仕様になっています。例えば、その途端にプログラムが暴走してもおかし くないということです。

 このプログラムは、たまたま 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;
}

(C) 1995-1996 Phinloda, All rights reserved
無断でこのページへのリンクを貼ることを承諾します。問い合わせは不要です。
内容は予告なく変更することがあります。