ポインタの話(おまけ)

作成:

前回までポインタなお話でしたが、 間に挟めなかったちょっとしたことについて解説

ストリング(文字列)

アルバイトでTA(ティーチングアシスタント&テクニカルアシスタント: 詰まるところプログラムとかを大学生相手に教えるお仕事)なんてやってる私ですが、 やはり初心者にとってC言語における文字列ってのはハードルが高いらしい。 一度理解してしまえばどうってことはないんですが・・・

C言語では一文字を表すのは char 型、文字列はその集合なので char 型の配列として表現されます。 しかし、配列は長さを保持できないし、配列の長さと文字列の長さも異なる場合が多いため、文字の羅列だけでは情報が足りません。 そこで、Cでは終端文字(ヌル文字もしくはナル文字、NUL 文字(NULL ではない))を使っています、 この文字が現れるまでを一つの文字列として扱かわれるわけです。
終端文字として扱われるのは文字コード 0 です。 直接0を用いても処理上問題ないのですが、整数としての0との違いを明確に書くため、 '\0' と表現されます。
というわけで、用意する配列は実際の文字の長さ+終端文字分を用意しなければならないのです。
知ってました? 私は時々忘れてえらい目に遭います(笑)
文字列の終わりが'\0'であることを知っていれば、 文字列全体に対して処理を行うときの継続条件(終了条件)をどう書けばいいかが分かるかと思います。

文字列の分割

さて、文字列の実態はchar型の配列で終端が'\0'であるということが分かると、 文字列を分割するということが簡単にできることに気がつくでしょう。
そう、途中で'\0'を挿入してやればいいのです。 そうすると、それまでの文字列が'\0'までになり、 その次を指すポインタが途中から終端までの文字列を表現することになります。
以下に簡単な例を示します。

char str[] = "hogehoge fugafuga";
char *next;
for( i = 0; str[i] != '\0'; i++ ){
  if(str[i] == ' '){
    str[i] = '\0';
    next = str + i + 1;
  }
}

この作業ではじめの文字列 "hogehoge fugafuga" を空白部分で2つの文字列に分割しています。 str が "hogehoge" 、next が "fugafuga" を表すようになります。
このように指定したトークンで文字列を分割する関数 ( char *strtok(char *s, const char *delim); ) が標準で用意されていますが、こいつがどうやって実装されているかがこれで分かると思います。
蛇足までに書いておくと、 こいつは内部でstatic変数を使って実装されていたりするなど、ちょっとあれな仕様なので、 ここら辺の知識を使って自分で実装した方がよいかも。(私はそうしてます)

文字列定数

ところで、文字定数の実態について考えたことがあるでしょうか? 文字定数ってのはプログラムにハードコードされた文字列です。

printf("hogehoge");

こういうやつです
実は、この文字定数はポインタなんです。意外と知らない人が多い(!)

考え方としては、どこかで char 型配列が確保されていて、その配列を差しているという感じで、 配列を差すものは、いわゆる先頭ポインタな訳で、文字定数はポインタということになるわけです。

const char __hoge[] = "hogehoge"
printf(__hoge);

こう書くとちょっとイメージが湧きやすいかもしれませんね。(const がついてるのは定数だから)


文字定数がポインタであることが分かると、 第12回のサンプルコードで出てきた下のような書き方もありってのが分かると思います。

if(bf.bfType != *(WORD*)"BM")

これは、 BMP ファイルの先頭にファイルフォーマット認識のための BM という文字があるかどうかを確認する部分です。 ヘッダー側は2バイト分をバイナリで読み込んでしまっているので、 BM という文字を2バイトバイナリで評価するために BM という文字が格納されたchar型ポインタをWORD型(unsigned short 型)ポインタにキャストし、その値を評価しています。 (キャストをバイナリデータの解釈の変更に使う例です。 どちらも元は文字列で等しいかどうかだけの判定なので、この部分に限ってはエンディアンの違いの影響を受けません。 まあ、コードを見た人にわかりづらいのであまり使わない方がいいかもしれません)

ポインタのポインタと2次元配列の違い

ポインタのポインタと2次元配列というものは、記述方法などがほとんど同じであるため、 この2つが同じものであると思ってしまいがちですが、実態は全く異なります。
2次元配列は1次元配列を特殊なアクセスができるようにしたもので、実態は1次元配列とほぼ同じものです。

void hoge(int **a, int b[][5]){
  a[2][3] = b[2][3];
}

という記述は a と b どちらも同じように見えますが、解釈のされ方が異なります。
a[2][3] は *(*(a+2)+3) という風に解釈されます。 つまり (a[2])[3] という感じで、まず、ポインタのポインタの添え字表現によってポインタを取り出し、 そのポインタに対する添え字表現で実際の値にアクセスしています。
一方、b[2][3] は *(b+(2*5)+3) のように解釈されます。 2次元配列は1次元配列をいくつかに区切ってアクセスできるようにしたものといったイメージです。 それゆえ、2次元配列の宣言には必ずいくつづつ区切るかを指定しないといけません(引数の int b[][5] っていう表現)。

そう言うわけで、「ポインタのポインタを2次元配列で受け取る」みたいな記述ができないってことが分かると思います。
ちなみに、 a[2] とすると a[2][0] のポインタ、つまり3行目の先頭ポインタが得られますが、 b[2]としても b+(2*5) という風に解釈され、同様に b[2][0] のポインタが得られます。
こういった意味ではポインタのポインタと2次元配列はほぼ同等なだけに混乱する人が多いみたいですね。

C言語における0

ちょっとポインタの話からはずれてきますが、
C言語では0というのは特別な意味で用いられます。(他の言語では通用しない場合が多いので注意)

終端文字

一つは、今回のはじめでいった終端文字。文字コード 0 で配列に格納された文字の終端の判定に使われます。

真偽値 FALSE

もう一つは、条件判定におけるFALSE、 C言語では bool 型がなく0以外の整数が TRUE 、0が FALSE として扱われます。 (bool 型としての TRUE としては 1 が用いられる場合が多いようです。(!0) を表示してみると1になる処理系がほとんど)
C言語標準では TRUE や FALSE という値は存在しないのですが、

#define TRUE  1
#define FALSE 0

ってな感じで定数定義してよく使われます。
注意してほしいのが FALSE については1対1対応ですが TRUE については1対1対応ではない点です。 すなわち、真偽値が用いられる場面で、0を使えば必ずFALSEとして判定されますし、 FALSE を表す値は 0 となります。 しかし、 TRUE については言語仕様上の定義は 0 以外の数ということなので、 1 を使えば必ず TRUE として判定されますが、 TRUE を表す値は 1 とは限りません。よって、ある条件を TRUE と比較すると正しく動作しない場合があります。

if( isHoge() != FALSE ){/* (真)正しい */}
if( isHoge() == FALSE ){/* (偽)正しい */}
if( isHoge() == TRUE ) {/* (真)正しいとは限らない */}
if( isHoge() != TRUE ) {/* (偽)正しいとは限らない */}

よって、あえて比較する場合は FALSE との比較を行う必要がありますが、 そもそも真偽値として整数型が使用できるためこの比較自体に意味がないこと、 そして FALSE と比較を行う場合 !(not) の使い方が判定の真偽と逆になることから可読性の観点から

if( isHoge() ) {/* (真)正しい */}
if( !isHoge() ){/* (偽)正しい */}

と、値そのままを使用するのが分かりやすいと思います。 (そのために真偽値を返す関数とかは英語風に読めるように書くなど工夫すればより分かりやすい(この名前の付け方ってなんていうんだっけ?))
ま、この書き方は自由なので、これはあくまで私の趣味。
でも、こういった風に書く人はそれなりにいると思うんで、そういうコードをみたときに混乱しないようにってぐらいで。

同様に、フラグ判定とかも真偽値の実体を利用して、

#define FLAG_A 0x01
#define FLAG_B 0x02
...
if( State & FLAG_B )   {/* フラグが立っているとき */}
if( !(State & FLAG_B) ){/* フラグが寝ているとき */}

ってな感じで記述したります。というかこういう判定が便利なように真偽値が定義されたとも・・・実際のところは知りませんが。蛇足まで。

SUCCESS

条件判定は上記のように FALSE を 0 と定義されていますが。 戻り値の返し方としては真か偽かだけでなく、成功か失敗かで返す場合があります。 この場合、成功した場合はそれ以上の情報は必要ないですが、 失敗した場合はその理由等を含めて返したい場合があります。
そのため、戻り値として成功時 0 をもどし、 失敗時それ以外の値を返し失敗の理由を示す系があります。
単純に成功か失敗かを返す場合でもこれに習い

#define FAILURE  -1
#define SUCCESS   0
#define EXIT_FAILURE 1
#define EXIT_SUCCESS 0

といった具合に、 SUCCESS として 0 、 FAILURE として 0 以外を返す系が多いようです。
しかし、こちらは真偽値と違って言語仕様上の規定はなく、 暗黙の了解でこういうことになっている系が多いというだけの話なため、 これとは異なる系があったとしてもおかしくないので注意してください。
ちなみに、 main 関数も戻り値を省略したり void 型として定義可能ですが正確には成功時 0 を返すように記述します。 (そうしないとスクリプトで成功判定ができない)

NULL

そして、NULL ポインタ。 NULL ポインタはそのポインタがどこも指していないことを表す特殊なポインタの値です。 他の言語ではポインタのようなものを宣言しただけで NULL で初期化されたりすることもありますが、 C言語では初期化されないので、自分で明示的に NULL を代入する必要があります。
ところで、この NULL というのはプリプロセッサ #define によって定義されたマクロです。

#ifndef NULL
#ifdef  __cplusplus
#define NULL    0
#else
#define NULL    ((void *)0)
#endif
#endif

こんな感じで定義されています。
C++ の場合は純粋な 0 で、Cの場合は void 型のポインタにキャストされた 0 を使っています。
C では void 型ポインタは任意のポインタに変換できるのですが、 C++ では void 型ポインタは明示的にキャストしないと他の型のポインタに変換できないのでこうなってます。 キャストを行わなくても、文脈上ポインタが使われる場面において 0 が記述されると、 それを NULL ポインタとして扱うことが言語仕様上保証されているらしい。
とはいえ、プログラムの可読性をあげるためにも NULL ポインタが必要な場面では NULL を使う方がいいです。
あと、ハードウェア的にメモリアドレス 0 が NULL と扱われているとは限らないそうです。 (っていうかソフトウェア側の人間がそんなことを知る必要はないわけですが)
ちなみに、整数としての 0 を NULL ポインタとして使おうとした場合、 通常の整数型( int )とポインタのサイズが異なる環境、 たとえば最近出てきたx86-64などの64bit環境での、LP64(longとポインタが64bit)やLLP64(long longとポインタが64bit)と呼ばれるスタイルでは、 int型とポインタ型のサイズが異なるのでちょっと注意が必要だったりします。



2686851 Today: 702 Yesterday: 881
Twitter
広告